2010/3/8

[Memo] 當Memcached 安裝成為 Windows Service 時如何指定參數

前陣子由於負責的網站Loading越來越重,加上前端Web Server Load Balance 有N台主機,在原本只有.NET Cache當作Cache的情況下,每個網頁仍需要Query N次資料才能夠全部進Cache。這個情況下只好求助Memcached這樣的Cache Server機製,好讓多台Web Server能夠共用Cache資料。達到『一人吃N人補』的境界。

Memcached 應該算是目前還滿流行的Cache Server,除了功能強大之外使用上也算簡單。安裝部份則只要下二行參數

memcached -d install
memcached -d start 

就能夠將memcached安裝並啟動成為Windows Service。不過有一個比較垢病的地方是無法指定服務啟動時的參數,導致memcached啟動時只能使用預設的最大64MB的記憶體。

就目前為止查到的解決這個問題的方法只有一種,進Regstry修改。

修改的路徑為:
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\memcached 下的 ImagePath中的值,將原來『"N:xxxx\xxx\xxx\memcached.exe" -d runservice』後方下參數(e.g. –m1024),像這樣
『"N:xxxx\xxx\xxx\memcached.exe" -d runservice -m1024』。

memcached-1 修改完成後,重新啟動即可。

Memcached相關文章:

2010/3/6

[Tips] Runtime JavaScript and Css 壓縮方式

上回有提到過壓縮JavaScript來加快網頁載入速度的方式,而實際導入專案中使用的方式有許多種;一般來說我們在開發期間會以未壓縮的js&css檔案進行開發工作,以方便修改及Debug工作,等到了測試期間才會改用壓縮格式的檔案來測試。所以通常會採用的作法是在Debug和Release version時Include不同的檔案來達成,並透過類似MSBuild的方式在Build Release版本時將.js的檔案做壓縮,產出類似xxx-min.js這樣的壓縮檔案。

產出不同版本檔案的方式其實很直覺,但是Clark覺得這樣還不夠彈性,由於YUI Compressor for .Net版本也和YUI Compressor一樣支援css檔案的壓縮,自然就可以考慮如果是在輸出js及css檔案時即時做壓縮。不過前提是即時壓縮需要搭配一些機製來減少Server的負擔,例如Client Side 和 Server Side 的Cache。不然光是應付每個User Request的js檔案就讓你的Server吃不消了!

Clark是以Handler的方式實作即時壓縮的功能,只要頁面在include js時指定"Scripts/js.ashx?path=jquery-1.3.2.js",handler就會將該js檔讀入並使用YUI Compressor for .Net做壓縮,並吐回壓縮過的檔案到Client端。為了針對Debug and Release version而吐出不同的js,使用#if DEBUG來區分版本。

以下是程式碼:

js.ashx.cs:

/// <summary>
/// js.ashx.cs
/// </summary>
public class js : IHttpHandler
{
    public void ProcessRequest(HttpContext context)
    {
        String filePath = context.Request["path"];
        String fullPath = Path.Combine(Path.GetDirectoryName(context.Request.PhysicalPath), filePath);
        String ext = Path.GetExtension(fullPath);
        if (String.IsNullOrEmpty(ext)  ext.ToLower() != ".js") return;
        String orgJS = "";
        //TODO: 將原始資料存入Cache,不必每次做載入
        using (FileStream fs = new FileStream(fullPath, FileMode.Open))
        {
            StreamReader reader = new StreamReader(fs);
            orgJS = reader.ReadToEnd();
            reader.Close();
        }
        //TODO: 將壓縮過的資料存入Cache,不必每次壓縮
#if DEBUG
        String outputJS = orgJS;
#else
        String outputJS = JavaScriptCompressor.Compress(orgJS);            
#endif
        //TODO: 判斷Browser是否支援GZip, 支援則將資料做GZip壓縮等..
        context.Response.ContentType = "text/javascript";
        context.Response.ContentEncoding = Encoding.UTF8;
        context.Response.ExpiresAbsolute = DateTime.Today.AddYears(1);
        context.Response.Write(outputJS);
    }
 
    public bool IsReusable
    {
        get
        {
            return true;
        }
    }
}

css.ashx.cs:

/// <summary>
/// css.ashx.cs
/// </summary>
public class css : IHttpHandler
{
 
    public void ProcessRequest(HttpContext context)
    {
        String filePath = context.Request["path"];
        String fullPath = Path.Combine(Path.GetDirectoryName(context.Request.PhysicalPath), filePath);
        String ext = Path.GetExtension(fullPath);
        if (String.IsNullOrEmpty(ext)  ext.ToLower() != ".css") return;
        String orgCss = "";
        //TODO: 將原始資料存入Cache,不必每次做載入
        using (FileStream fs = new FileStream(fullPath, FileMode.Open))
        {
            StreamReader reader = new StreamReader(fs);
            orgCss = reader.ReadToEnd();
            reader.Close();
        }
        //TODO: 將壓縮過的資料存入Cache,不必每次壓縮
#if DEBUG
        String outputCss = orgCss;
#else
        String outputCss = CssCompressor.Compress(orgCss);
#endif
        //TODO: 判斷Browser是否支援GZip, 支援則將資料做GZip壓縮等..
        context.Response.ContentType = "text/css";
        context.Response.ContentEncoding = Encoding.UTF8;
        context.Response.Write(outputCss);
    }
 
    public bool IsReusable
    {
        get
        {
            return false;
        }
    }
}

Default.aspx

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Default.aspx.cs" Inherits="WebSite._Default"%>
 
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
 
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title>Css & JavaScript Compressor Test</title>
    <!-- 借用ZenGarden的css sample. 所有要include的css檔都丟給 css.ashx處理 -->
    <link type="text/css" rel="stylesheet" href="Styles/css.ashx?path=zengarden-sample.css" />
    <!-- 所有include的js檔都丟給js.ashx處理 -->
    <script type="text/javascript" src="Scripts/js.ashx?path=jquery-1.3.2.js"></script>
   1:  
   2:     <script type="text/javascript">
   3:         $(function () {
   4:             //test for jquery
   5:             $('#pageHeader h1').fadeOut().fadeIn();
   6:         });
   7:     
</script>
</head>
<body>
    <form id="form1" runat="server">
    <div id="pageHeader">
        <h1>
            Css And Javascript Test
        </h1>
    </div>
    </form>
</body>
</html>

這樣一來,只要在include css & js時都照此方式來處理,那在Release Version時,Client就會自動改用壓縮過的js和css檔,而不需要另外做設定,也不會產出多個檔案減少檔案管理上的負擔。

※以上的範例僅為Demo壓縮的概念,若要架構在Production Site上還有一些功能需要去實作,以免造成Server Loading過重,以及沒有Client端Cache導致User Experience不佳等問題。

2010/3/3

[Memo] 『無法辨認的逸出序列』的另一種可能性

Compile不知道幾千次以上的Web Project突然蹦出一則錯誤 Unrecognized escape sequence (中文訊息:無法辨認的逸出序列)。打開出現錯誤的Souce Code,發現都是在中文字的後方才會出現這樣的問題;照MSDN上的解釋為"在字串中反斜線 (\) 的後方出現未預期的字元",不過光從Editor上看來該行根本沒有出現"\"的字元。

遇到這個問題,加上錯誤出現在中文字串後面,所以直覺認為是Encoding問題。在另存檔案看了一下檔案的Encoding方式為big5。由於之前沒有仔細在看是否所有檔案都是以utf-8編碼,所以猜想難道是什麼動作改了檔案的編碼方式?!先不管是否這樣,在將檔案改以utf-8格式存檔以後,該行的錯誤已經沒出現了,但是其他行陸續出現這個問題,顯示錯誤的檔案不止一個,這樣改下去不是辦法。

在看了一下Subversion中的Log之後,確認近期並沒有動到該檔案,本地的檔案時間也一樣停留在上次修改的時間,所以證實並非檔案編碼遭到修改而造成。

回頭想想這幾天修改了什麼設定,想到昨天為了測試某個Open Source Project而將根目錄改成該Project的目錄。但是心想遇到問題的Project是以虛擬目錄的方式存在,應該不會被影響才對!為了證實問題,將目錄改回原來的C:\inetpub\wwwroot下,重新Compile一次…鏘鏘!錯誤訊息不見了,Compile完成!

既然知道了問題,本著求知的精神將Web.config打開來看,發現有一段設定相當可疑..

<globalization
        requestEncoding="utf-8"
        responseEncoding="utf-8"
        fileEncoding="utf-8"
/>

fileEncoding 指定為utf-8,但是剛剛出現錯誤的Project中不少檔案是以big5編碼的,所以問題應該是虛擬目錄繼承了根目錄的設定,使用utf-8當作檔案的編碼格式。回頭將Project中的Web.config加了一行。

<!-- 如果是線上環境,千萬別直接照著修改,請先確認自已的encoding方式 -->
<globalization fileEncoding="" />

不指定fileEncoding的值,讓Compiler直接以檔案的BOM記錄的編碼方式來當作檔案編碼,重新Compile一次,問題解決!

結論:

  1. 虛擬目錄中如果未指定Encoding方式,會直接繼承根目錄"指定"的Encoding。
  2. 若未指定fileEncoding的值,Compiler會以檔案的BOM值來作為檔案Encoding格式。
  3. 光從錯誤訊息不一定能直接看出問題,但是可以懷疑造成問題的可能性!

.NET Reflector Pro 讓你遇到問題不再"不知所謂"

在Coding的不歸路上,隨著專案越來越大,歷史越來越久遠,用到的Library也越多,這個時候每當到問題時,常常發生的一個情況是在Debug到某個Library的method時由於無法再追下去(沒有Source Code或者就算有也不一定能確定版本是否正確)。這個時候常常需要用到.NET Reflector 加上"充份的相像力"來查看Library的method裡頭哪裡有可能會出問題。而越是分散式的寫法,在看Code時就更需要來回切換在不同的Class裡頭。

而這種刻苦的情節在Clark發現了.NET Reflector Pro提供了Debug Assembly這樣的好物之後將不再會發生了!幾乎所有的Library都可以被Decompile然後攤在陽光下,讓你 一步一步的追出問題的根源,或者瞭解它的實作方式,來提供做為問題分析的依據。

不過這套工作是需要付費的,請花195美元來享用它的功能。但大家可以先下載並執行最新版本的.NET Reflector 來將.NET Reflector Pro自動安裝到Visual Studio 2008 or Visual Sutdio 2010上做15天的試用。

Debug Code or Assembly 的方式官網已經說明得很清楚了,所以就不再多做說明,有圖有真相。

 ReflectorPro

2010/2/23

[Tips] HttpUtility.ParseQueryString 使用注意事項

之前在黑暗執行緒上看到這篇文章,知道了有PrseQueryString這個工具,由於一般在網頁上如果遇到參數的需求,不外乎以Request[xx]方式就可以讀出需要的參數,所以這樣的需求應該比較少會遇到。不過偶然情況下在最近的專案中需要用到一個將 "var1=1&var2=2&..” 這樣將 url 傳遞過來的參數做拆解的功能。由於該key-value傳是存在 url 的某個參數下(如"demo.aspx?type=a&keyList=…"的keyList中),所以直接使用Request當然就無法讀到需要的值了。

遇到這個情況,如果自已寫一個拆解的method當然不是問題,不過直覺想起HttpUtility.ParseQueryString應該可以解決這個需求,所以直接使用了這個元件來解決問題。但是隔沒多久就有User反應功能有問題;在debug之後,發現一旦遇到傳遞的key-value中含有plus(『+』)字元時,就會變成空白。回頭研究了一下確認,證實ParseQueryString會將未UrlEncode過的plus字元轉換成空白。

針對此一情況,一種的解法是在使用ParseQueryString前事先將plus轉換成"%2b”,如此一來ParseQueryString即可正確解決變成空白的問題。

//keylist = HttpUtility.UrlEncode("var1=1&var2=2&total=1+2");
//http://localhost/ParseTest.aspx?type=parse&keylist=var1%3d1%26var2%3d2%26total%3d1%2b2
String query = Request["keylist"];
Response.Write(HttpUtility.ParseQueryString(query)["total"]);
//output: 1 2
Response.Write(HttpUtility.ParseQueryString(query.Replace("+","%2b"))["total"]);
//output: 1+2

不過由於這種修補方式不夠正式,所以雖然可以解決遇到"+"時出現錯誤的問題,但仍不建議這麼使用。由於需求的功能較單純,為確保讀出的資料都可以如實的取回來,最後還是改用自行解析的方式來處理。

public static NameValueCollection ParseQueryString(String query)
{
    NameValueCollection nvList = new NameValueCollection();
    String[] fieldList = query.Split('&');
    foreach (String field in fieldList)
    {
        int eqIndex = field.IndexOf('=');
        //=存在且不在第1個和最後一個字元
        if (eqIndex > 0 && eqIndex != (field.Length-1))
        {
            String name = field.Substring(0, eqIndex);
            String value = field.Substring(eqIndex + 1);
            nvList.Add(name, value);
        }
    }
    return nvList;
}

結論:
HttpUtility.ParseQueryString對於一般"編碼"過的QueryString的折解沒有問題,但如果遇到QueryString中的類QueryString的情況,由於類QueryString中的各個value無法先行編碼,所以一旦被HttpUtility.ParseQueryString解讀時就可能會出現"+"字元被解成空的問題,這個時候就無法再偷懶了,請自行解析吧!

2010/2/19

[Memo] 以命令列工具 appcmd 管理 IIS7

之前的一篇文章 命令列管理IIS 提到如何使用Command Line來管理IIS,更精確來講應該是『命令列管理IIS6』。由於該指令只存在於IIS6,所以不管是IIS5 or IIS7 都無法透過該指令來進行管理。有鑑於此,特別另外寫一篇來記錄IIS7的命令列管理方式以求一個完整性。

由於IIS7已改為統一使用 appcmd.exe 來管理IIS ,所以不再需要像IIS6一樣分成幾種Script來進行管理動作,省去不少麻煩。另外一個特殊的功能是將查輸出結果以XML格式回傳;這個功能搭配上 pipe 的功能,就可以上一個 appcmd 的輸出結果設定當成第二個 appcmd 的輸入來使用,增加指令使用上的變化性和彈性。舉個例子,如果要將目前正在停止中的”多個”網站重新啟動可以執行以下指令:

REM 於目錄 %windir%\system32\inetsrv 下執行
appcmd list site /state:Stopped /xml | appcmd start site /in

其中2個指令以 『|(pipe)』連接,前半部使用 "/xml" 指定將目前正在停止中的網站以XML格式輸出,後半部加上 "/in" 表示將前半部XML輸出資料當成輸入,並將該輸入中所列的網站啟動。
反之,當然也可以將所有正在執行中的網站停止

appcmd list site /state:Started /xml | appcmd start site /in
有了這樣彈性的使用方式,只要再熟悉使用的參數,就可以將部份設定工作批次來進行,節省不少時間。
以下列出幾種常用的指令當作參考:
REM 列出網站(SITE)支援的命令
appcmd site -?
 
REM 列出所有網站 
appcmd list site
 
REM 列出網站的List命令的參數及說明(其他功能的說明方式依此類推)
appcmd list site -?
 
REM 建立網站到8088 port下,並指定對應到d:\temp目錄下
appcmd add site /name:"My Test Site" /id:99 /bindings:http://*:8088 /physicalPath:"d:\temp\"
 
REM 列出所出電腦上所有虛擬目錄 
appcmd list vdir
 
REM 列出指定網站下的所有虛擬目錄
appcmd list vdir "預設網站/"
 
REM 建立虛擬目錄
appcmd add vdir /app.name:"My Test Site/"  /path:/MyVdir /physicalPath:"d:\temp\MyVdir"
 
REM 刪除虛擬目錄
appcmd delete vdir "My Test Site/MyVdir"
 
REM 刪除網站
appcmd delete site /site.name:"My Test Site"

由於指令用法甚多,所以僅列出較常用的幾種當作範例,以便了解參數的給法的方式為何;更多詳細的內容以及進階的需求就需要再參考MSDN上的說明了。


MSDN參考資源:http://technet.microsoft.com/zh-tw/library/cc772200(WS.10).aspx

2010/2/12

[Tips] 壓縮JavaScript檔案以加快網頁載入速度

隨著jQuery的使用率越來越普遍,網頁上許多UI上的功能大家都會透過jQuery plug-in來解決。在這個情況下,頁似乎都免不了會在網頁上include不少 .js & .css 檔案。而在考慮 performance issue 的情況下,網頁上引入的檔案應該是越小、越少越好,這樣不但能夠加快User載入頁面的速度,也能夠減少網站的流量及connection數量,尤其對流量大的網站來說,差距應該就更加明顯。

大型網站如Google, Yahoo一定都會做的一件事情,就是盡可能的減少js & css檔案的size,以提供User最快的服務速度以及使用者體驗。雖然大概沒有多少人有機會經營這樣大型的網站,不過即使對一般中小型網站來說,頻寬仍然是重要的一項資源,而對User來講,速度慢的網站始終讓人無法忍受。

對於js & css這樣的靜態檔案壓縮,還可以分出2種階段,分別為Gzip壓縮及檔案內容的壓縮(去除不必要的空白字元、註解或替換JavaScript變數名稱等);Gzip需要Browser能夠支援Gzip解壓縮時才能夠使用這種壓縮檔案的方式,而檔案內容的壓縮的方式則不受Browser的限制,所以是必定可行的一種壓縮方式。

而關於js檔的內容的壓縮工具,Google,Microsoft,Yahoo分別都有提供了對應的工具,以下列出這三種工具供大家參考,基本上三種工具都有一定的品質,至於哪個工具好用的話除了以瘦身的比例來比較之外,也不外乎是青菜蘿蔔各有所好,不必爭論誰比誰好了。

  • Closure Compiler

    Google提供的工具,提供Comand-line及Online方式來進行分析、重寫(rewrite)、壓縮的動作(視使用的模式而定),目前已廣泛應用在Google Maps, Google Docs等服務上。該工具使用Java開發,目前在Codeplex上已經有以一個Closure Compiler 為基礎使用.NET開發出來的非官方版本的UI Compiler .NET,不過目前還在Beta階段。
  • Microsoft Ajax Minifier
    Microsoft開發出來提供給Ajax Library使用的工具,提供Command-line式的使用介面,以及MSBuild task的方式將檔案壓縮的功能整合在Build動作上來達到流程自動化的功能。
  • YUI Compressor

    使用提供針對css&js檔案壓縮的Command-line工具,也是使用Java進行開發的OpenSource Project;目前在Codeplex上也有非官方的.NET版 YUI Compressor for .Net 可以用,而且也支援MSBuild task功能。由於它多提供了一種針對css檔案壓縮的功能,所以對Clark來說可用性相對的較高一些。

以上三種工具大家都可以選擇自已喜好的來用,不過考慮針對css檔處理的部份來說的話,Clark會選擇使用YUI Compressor,使用統一的工具在管理上也較為方便。

而當有了工具以後,該怎麼用又是另一個問題了!由於大家在開發時期如果直接include壓縮過的js檔案,容易造成控管及debug上的不便(Closure Compiler倒是可以搭配Closure Inspector + FireBug來避開這個問題),所以Clark自已也會選擇在Build時期來產出壓縮過的檔案,減少控管上的問題。

2010/2/10

[Code] 讀取及匯入Excel (XLS) 檔案

前陣子遇到一個需求,需要一個功能是將資料庫中的資料轉入Excel檔中讓PM編輯,之後再從Excel轉回到資料庫中。在一般情況下通常會考慮使用.csv檔案來將資料匯入或匯出。不過由於資料庫的內容無法控制,資料中可能含和.csv的分隔方式有衝突的字元(如tab or 『,』),且若存在.csv檔案中,若有多個table則需要產出多個.csv,對於使用及管理不易;在考慮使用的便利性之下,決定以標準的.xls檔案搭配活頁簿的方式來區分不同的資料表來當作資料轉換的媒介。

原本認為這樣的功能應該有許多便利的Library可以使用,但是卻在不斷的 Try and Error 到也花了不少時間在不對的工具上頭。現在Clark回過頭來整理一下用過幾個工具下來自已的心得供大家參考。

  • Excel object model
    直接使用Microsft Excel的COM元件來建立或讀取Excel的檔案。條件為需要先行安裝Excel才能夠使用這個COM 元件的功能。使用官方的元件的好處是相容性較高,不過由於在做匯入匯出的時候系統會先開啟一個Excel的Instance來做這些工具,所以相當耗系統資源,且資料一多的情況下轉檔的速度相當慢。由於Clark匯出的檔案格式較簡單,只有表格和活頁簿,所以其實並不需要使用到這種方式。
  • NativeExcel
    需要付費購買的元件,由於在試用版的Library中都會強制在檔案中加上未註冊的訊息,所以對於測試來講相當不方便,不過轉檔的速度比使用Excel object model的方式快相當多。另一個缺點是在讀取檔案時偵測的有資料的Range(有用到的Column數)時有時會造成誤判,導致無法正確匯出所有的欄位(由於已經有一段時間了,所以不確定是我的用法錯誤還是它的功能確實有問題,不過在使用上的確有些不便)。
  • NPOI
    話說『天下沒有白吃的午餐』,但實際的情況卻是OpenSource的Library解決了Clark的問題。NOPI原來是以Java實作的 Excel, doc, ppt 讀/寫檔案的Library後來被porting到.NET上。它的轉檔速度快,且在判斷使用的行、列上沒有誤判問題。

NPOI是這次Clark推薦的針對讀/寫 Excel檔案工具,有句話說『程式碼就是最好的說明文件』,所以一定要po一下Clark的使用方法供大家參考,也可以節省下找Sample Code的時間。

public class NPOIExcelHelper : ISpreadsheetHelper
 {
     #region ISpreadsheetHelper Members
 
     public void SaveDataSetToExcel(System.Data.DataSet dsSource, string filePath, CallBackEvent.UpdateProgressCallBack callBack)
     {
         //建立一個工作簿
         HSSFWorkbook workbook = new HSSFWorkbook();
 
         //為每table建立一個活頁簿WorkSheet
         for (int i = 0; i < dsSource.Tables.Count; i++)
         {
             //callback部份用來判斷目前讀到的table百分比,供顯示進度表之用
             if (callBack != null)
                 callBack((int)(((float)i / (float)dsSource.Tables.Count) * (float)100));
 
             System.Data.DataTable dt = dsSource.Tables[i];
 
             //建立活頁簿
             HSSFSheet sheet = workbook.CreateSheet(dt.TableName);
 
             //為避免日期格式被Excel自動換掉,所以設定 format 為 『@』 表示一率當成text來看
             HSSFCellStyle textStyle = workbook.CreateCellStyle();
             textStyle.DataFormat = HSSFDataFormat.GetBuiltinFormat("@");
             
             //用column name 當成標題列
             List<String> columns = new List<string>();
             for (int colIndex = 0; colIndex < dt.Columns.Count; colIndex++)
             {
                 string name = dt.Columns[colIndex].ColumnName;
                 HSSFCell cell = sheet.CreateRow(0).CreateCell(colIndex);
                 cell.SetCellValue(name);
                 cell.CellStyle = textStyle;
                 columns.Add(name);
             }
 
             //建立資料列
             for (int row = 0; row < dt.Rows.Count; row++)
             {
                 DataRow dr = dt.Rows[row];
                 for (int col = 0; col < columns.Count; col++)
                 {
                     string data = dr[columns[col]].ToString();
                     HSSFCell cell = sheet.CreateRow(row+1).CreateCell(col);
                     cell.SetCellValue(data);
                     cell.CellStyle = textStyle;
                 }
             }
         }
         if (callBack != null) callBack(100);
 
         //寫入檔案
         FileStream file = new FileStream(filePath, FileMode.OpenOrCreate);
         workbook.Write(file);
         file.Close();
     }
 
     public System.Data.DataSet ReadExcelToDataSet(string filePath, CallBackEvent.UpdateProgressCallBack callBack)
     {
         //開啟要讀取的Excel檔案
         FileStream file = new FileStream(filePath, FileMode.Open);
         //讀入Excel檔
         HSSFWorkbook workbook = new HSSFWorkbook(file);
         file.Close();
 
         DataSet dsSource = new DataSet();
         //為每個WorkSeeh建立出一個table
         for (int sheetIndex = 0; sheetIndex < workbook.NumberOfSheets; sheetIndex++)
         {
             HSSFSheet sheet = workbook.GetSheetAt(sheetIndex);
             //建立一個新的table
             DataTable dtNew = dsSource.Tables.Add(workbook.GetSheetName(sheetIndex));
 
             HSSFRow row = sheet.GetRow(0);
             //讀取第0列當作column name
             for (int columnIndex = 0; columnIndex < row.LastCellNum; columnIndex++)
             {
                 DataColumn dc = new DataColumn(row.GetCell(columnIndex).ToString());
                 dtNew.Columns.Add(dc);
             }
 
             int rowId = 1;
             //第一列以後為資料,一直讀到最後一行
             while (rowId <= sheet.LastRowNum)
             {
                 DataRow newRow = dtNew.NewRow();
                 //讀取所有column
                 for (int colIndex = 0; colIndex < dtNew.Columns.Count; colIndex++)
                 {
                     newRow[dtNew.Columns[colIndex]] = sheet.GetRow(rowId).GetCell(colIndex).ToString();
                 }
 
                 dtNew.Rows.Add(newRow);
 
                 rowId++;
             }
 
             //dsSource.Tables.Add(dtNew);
         }
         return dsSource;
     }
 
     #endregion
 }