1. [Azure] Multi-Tenancy Application #1, 設計概念

     

    對,各位你沒看錯,我的部落格在隔了一年半之後又有新文章了 XD

    好久沒寫了,這年頭什麼東西都流行加個 "微" ... BLOG 有微博,Messenger 有微信... MV有微電影... 連我們在做的數位學習也出現 "微型課件" ... 什麼都微型化的後果,就是越來越懶的 POST 這種 "大型" 的文章在 BLOG 上了.. 常常想到一些東西,還沒成熟到一個完整概念,就貼到 FB 上,越來越速食的結果就越來越沒顧到 BLOG... Orz…

    不過,世界果然是在往M型的方向發展,雲端到現在已經是個成熟的技術 & 概念了,WEB APP的開發也越來越大型化,用了 Azure 當 PaaS (Platform as a Service) 後,要開發大型的 Web Application 門檻也不像過去那麼高。這次要寫的,就是 SaaS (Software as a Service) 被廣為流傳的設計概念: 多租戶 (Multi-Tenancy) 的設計方式。

    其實說明 SaaS 或是 Multi-Tenancy 的文章一大堆,完全輪不到我這種文筆程度的人來寫 XD,一樣,我只針對特別的地方下筆。Multi-Tenancy 顧名思義,就是讓一個 Application 能做適當的切割,"分租" 給多個客戶使用。跟過去一個 Application 就服務一個客戶不一樣,Multi-Tenancy 先天就是設計來服務多個客戶用的,也因為當年 SalesForce 的成功而聲名大噪。

    回到系統的角度來思考,要設計一個漂亮的 Multi-Tenancy (Web) Application, 還真的是個不小的挑戰... 沒錯,我就吃過這種苦頭 XD,大概六年前因為工作上的關係,就已經有這樣的設計架構了,不過當年一切都要自己來,因此什麼釘子都碰過了。用現在的技術,有太多簡潔的作法,不過確看不到有太多的人在討論 (至少中文的沒有),就動起這念頭,想來寫幾篇.. 內容就定位在用當紅技術: Microsoft Azure + ASP.NET MVC4 當作底層開始吧~~

    先來探討一下幾種常見的 Multi-Tenancy 的設計方式。MSDN 有篇文章寫的很精闢,快七年前的文章了,當年靠著這篇給我不少靈感... 我就先來導讀一下,標一下幾個架構設計的重點 (底下圖片皆引用 MSDN 該篇文章):

     

    三種架構: Separated DB / Separate Schema / Shared Schema 比較:

    Aa479086.mlttntda02(en-us,MSDN.10).gif

    要把多個客戶塞進同一套系統內,最直接的問題就是資料隔離的 issue 了。暫時不管 application, 只管 data, 若對隔離及安全性要求越高,就要在越底層就做到隔離的機制。

     

    1. Separated DB

      Aa479086.mlttntda03(en-us,MSDN.10).gif
      最高隔離等級,就是每個客戶一個 database (Separated DB)。用這種模式,再怎麼粗心的工程師,也不會不小心把別的客戶的資料給秀出來。舉個例,系統內的A客戶,就會連到A資料庫,B客戶就用B資料庫。每個客戶的資料庫各自獨立,不會互相打架。

    2. Separate Schemas (Shared Database)

      Aa479086.mlttntda04(en-us,MSDN.10).gif
      這種作法比第一種 Separated DB 好一點,它共用同一個 DB,但是替每一個客戶建立一組 Tables。這種作法不需要那麼高的成本 (看過 $$ 就知道,資料庫很貴的….),多個客戶可以共用資料庫,不過因為 Schema 的層級隔離了,簡單的說不同客戶的 Table Name 是不一樣的,因此粗心的工程師造成客戶資料混在一起的機率也不算高,還有不錯的隔離機制。

      這方式實作有點辛苦,不過 SQL 2005 之後開始支援 Schema 這機制,實作上可以簡單的多。

    3. Shared Schema (Shared Database & Shared Schema)

      Aa479086.mlttntda05(en-us,MSDN.10).gif

      這作法最極端了,所有資料都放在同一個資料庫,同一組資料表... 靠的是一個客戶ID的欄位來區別。這種方式成本最低,設計也最簡易直覺,不過... 整個系統只要有一道 SQL query 寫錯 (漏掉 where TenantID = ‘MyAccount’),一切就完了,A客戶的資料就會出現在B客戶的報表裡...


    當然,這篇是 MSDN 的文章,自然也提到了 Microsoft 的技術 (SQL server) 如何因應這三種不同的需求。這張表格是整篇文章的精華了,很清楚的講到三種模式,對各種問題的最佳處理方式。表格貼不過來 @@,我直接用截圖來說明:

    image

    簡單舉個例子,Extensibility Patterns 這欄說明的是如何擴充你的資料? 這裡指的擴充不是指效能,是只你要如何增加新的資料型態?

    Separate DB  or Shared DB 最簡單,就照一般的方法,開新的欄位就好。對於 Shared Schema 的系統來說就沒這麼容易了,只能預先開幾個備用欄位 (如每個 table 都開: column1, column2, column3 …. etc),不然就只能弄出像 NO SQL 那種 Name-Values 的方式來處理。不過,有經驗的開發者都知道,這樣搞下去,QUERY 實在很難寫...

    其實這篇文章講的很務實,從設計架構、開發、到系統上線的維護及調校都講的很清楚,若各位有在企業內部 (INTRANET) 的環境建立 Multi-Tenancy Application 的需求,只能用 Windows Server + SQL Server 的話,這篇文章很值得參考。不過,這篇文章的時空被景是 2006 年,當年沒有 Azure 這種東西... MVC 也沒今天那麼成熟... 現在要做同樣的事,你有更厲害的武器可以用...

    現在該怎麼做? 當然是等下一篇 XD

    2013/03/12 系列文章: Multi-Tenancy Application AZURE MSDN SQL 技術隨筆

  2. LINQ to Object #2, Indexes for Objects

    上一篇自己寫了很不成熟的範例,示範怎麼同時使用 LINQ to Object 的方式查詢物件,同時又能用到索引的機制。不過示範歸示範,總是要有上的了檯面的作法… 這就是本篇的目的: i4o (indexes for objects) 這套函式庫的應用!

    開始囉唆前,先來看看怎麼用吧! 接續上一篇的範例,我做了點調整,一方面把查詢的對像,從原本的 string 換成 Foo,另一方面也加上了第三種對照組: 使用 i4o 來建立索引。程式碼及執行結果如下:

    對三種不同的 collection 進行 Linq 查詢

    Stopwatch timer = new Stopwatch();
    
    // 建立資料集合 list1: 使用 List<Foo>, 沒有索引
    List<Foo> list1 = new List<Foo>();
    list1.AddRange(RndFooSeq(8072, 1000000));
    
    // 建立資料集合 list2: 使用 IndexedList, 自訂型別,針對 Foo.Text 及 Foo.Number 建立索引,Query 只支援 == 運算元
    IndexedList list2 = new IndexedList();
    list2.AddRange(list1);
    
    // 建立資料集合 list3: 使用 i4o library
    IndexableCollection<Foo> list3 = list1.ToIndexableCollection<Foo>();
    
    // 對 list1 進行 query
    Console.WriteLine("\n\n\nQuery Test (non-indexed):");
    timer.Restart();
    (from x in list1 where x.Text == "888365" select x.Text).ToList<string>();
    Console.WriteLine("Query time: {0:0.00} msec", timer.Elapsed.TotalMilliseconds);
    
    // 對 list2 建立索引,進行 query
    Console.WriteLine("\n\n\nQuery Test (indexed, dic):");
    timer.Restart();
    list2.ReIndex();
    Console.WriteLine("Build Index time: {0:0.00} msec", timer.Elapsed.TotalMilliseconds);
    timer.Restart();
    (from x in list2 where x.Text == "888365" select x.Text).ToList<string>();
    Console.WriteLine("Query time: {0:0.00} msec", timer.Elapsed.TotalMilliseconds);
    
    // 對 list3 建立索引,進行 query
    Console.WriteLine("\n\n\nQuery Test (indexed, i4o):");
    timer.Restart();
    list3.CreateIndexFor(i => i.Text).CreateIndexFor(i=>i.Number);
    Console.WriteLine("Build Index time: {0:0.00} msec", timer.Elapsed.TotalMilliseconds);
    timer.Restart();
    (from x in list3 where x.Text == "888365" select x.Text).ToList<string>();
    Console.WriteLine("Query time: {0:0.00} msec", timer.Elapsed.TotalMilliseconds);
    

    2011/10/30 .NET C# Tips 物件導向

  3. [CODE] LINQ to Object - 替物件加上索引!

    好久沒寫文章了,看了一下上一篇的日期… 嚇,再撐幾天就 10 個月了 =_=,  週末看了 darkthread 的這篇文章: 當心LINQ搜尋的效能陷阱,想說 LINQ to SQL, LINQ to Entity Framework, LINQ to … 都支援索引, 沒道理 LINQ to Object 不行啊, 一定只是沒人補上實作而已.. 動了這個念頭之後, 接下來的時間就都在研究這些…

    回到主題,年紀夠大的網友,大概都還聽過 Embedded SQL 吧? 當年用 C/C++ 要存取資料庫,實在不是件簡單的事… 光是那堆 initial database connection 的動作就搞死人了,因此有些開發工具廠商,就搞出了 Embedded SQL 這種內嵌在程式語言內的 SQL .. 貼一段 CODE,看起來大概像這樣:

    Embedded SQL sample code:

    /* code to find student name given id */
    /* ... */
    for (;;) {
        printf("Give student id number : ");
        scanf("%d", "id);
        EXEC SQL WHENEVER NOT FOUND GOTO notfound;
        EXEC SQL SELECT studentname INTO :st_name
                 FROM   student
                 WHERE  studentid = :id;
        printf("Name of student is %s.\n", st_name);
        continue;
    notfound:
        printf("No record exists for id %d!\n", id);
    }
    /* ... */
    

    有沒有跟現在的 LINQ 很像? CODE 就不多解釋了,這類的工具,大都是遵循這樣的模式: 透過編譯器 (或是 code generator / preprocessor),將 query language 的部份代換成一般的 data access code,最後編譯成一般的執行檔,執行起來就有你預期的效果…

    看到 darkthread 探討 LINQ to Object 效能問題之後,第一個反應就是:

    “該怎麼替 LINQ to Object 加上索引??”

    LINQ 這堆指令,最終是會被翻譯成 extension method, 而這些 extension method 是可以自訂的,就先寫了一個小程式試看看..

        public class IndexedList : List<string>
        {
        }
    
        public static class IndexedListLinqExtensions
        {
            public static IEnumerable<string> Where(this IndexedList context, Expression<Func<string, bool>> expr)
            {
                Console.WriteLine("My code!!!");
                foreach (string value in context.Where(expr)) yield return value;
            }
        }
    
        class Program
        {
            static void Main(string[] args)
            {
                IndexedList list1 = new IndexedList();
                // init list1 ...
    
                foreach (string value in (from x in list1 where x == "888365" select x)) {
                    Console.WriteLine("find: {0}", value);
                }
            }
        }
    

    程式很簡單,只是做個 POC,證明這段 CODE 會動而已。從 List<string> 衍生出一個新類別: IndexedList, 然後針對它定義了專屬的 Extension method: Where(...), 然後試著對 IndexedList 這集合物件,用 LINQ 查詢… 果然 LINQ 在執行 where x == “888365” 這語句時,會轉到我們自訂的 extension method !

    接下來的工作就不是那麼簡單了,我自認很愛挖這些 framework 的設計,又自認 C# 語法掌握能力也不差,結果我也是花了一些時間才摸清楚它的來龍去脈… 事情沒這麼簡單,所以我做了極度的簡化…

    首先,我只想做個 POC (Proof of concept), 證明可行就好, 完全沒打算把它做成可用的 library .. 因為在研究的過程中,早已找到有人在做這件事了 (i4o, index for objects)。為了讓 code 簡短到一眼就看的懂的程度,我的目標定在:

    1. 查詢對相只針對 List<string> 處理,不做泛型及自訂索引欄位。
    2. 查詢只針對 == 做處理。如下列 LINQ 語句的 where 部份: from x in list1 where x == "123456" select x
    3. 條件只限於 x == "123456", 只提供 == 運算元,只提供常數的比對,常數只能放在右邊…

    接下來就是索引的部份了,因為 where 只處理 == 這種運算,也不用排序了,採用 HashSet 就很夠用了,速度又快又好用,時間複雜度只有 O(1) .. 看起來很理想,於是剛才的 Sample Code 就可以改成這樣:

    IndexedList : 加上索引的 List<string> 實作

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Linq.Expressions;
    
    namespace IndexedLinqTest
    {
        public class IndexedList : List<string>
        {
            public readonly HashSet<string> _index = new HashSet<string>();
    
            public void ReIndex()
            {
                foreach (string value in this)
                {
                    this._index.Add(value);
                }
            }
        }
    
        public static class IndexedListLinqExtensions
        {
            public static IEnumerable<string> Where(this IndexedList context, Expression<Func<string, bool>> expr)
            {
                if (expr.CanReduce == false "" expr.Body.NodeType == ExpressionType.Equal)
                {
                    BinaryExpression binExp = (BinaryExpression)expr.Body;
                    string expectedValue = (binExp.Right as ConstantExpression).Value as string;
                    if (context._index.Contains(expectedValue)) yield return expectedValue;
                }
                else
                {
                    //return context.Where(expr);
                    foreach (string value in context.Where(expr)) yield return value;
                }
            }
        }
    }
    

    程式碼稍等再說明,先來看看怎麼用! darkthread 的作法真不錯用,借兩招來試試… 程式大概執行這幾個步驟:

    1. 準備 10000000 筆資料,用亂數打亂,建立 list1 (含索引, type: IndexedList) 及 list2 (不含索引, type: List<string>)
    2. 呼叫 list1.ReIndex(), 替 list1 重建索引 (HashSet), 記錄建立索引花費的時間
    3. 分別對list1, list2進行LINQ查詢,查出四筆指定的資料,計算查詢花費的時間

    直接來看看程式碼吧:

    測試 Indexed LINQ 的程式碼

    class Program
    {
        static void Main(string[] args)
        {
            Stopwatch timer = new Stopwatch();
    
            IndexedList list1 = new IndexedList();
            list1.AddRange(RndSeq(8072, 10000000));
    
            List<string> list2 = new List<string>();
            list2.AddRange(list1);
    
            timer.Restart();
            list1.ReIndex();
            Console.WriteLine("Build Index time: {0:0.00} msec", timer.Elapsed.TotalMilliseconds);
    
    
            Console.WriteLine("\n\n\nQuery Test (indexed):");
            timer.Restart();
    
            foreach (string value in (from x in list1 where x == "888365" select x)) 
                Console.WriteLine("find: {0}", value);
    
            foreach (string value in (from x in list1 where x == "663867" select x)) 
                Console.WriteLine("find: {0}", value);
    
            foreach (string value in (from x in list1 where x == "555600" select x)) 
                Console.WriteLine("find: {0}", value);
    
            foreach (string value in (from x in list1 where x == "888824" select x)) 
                Console.WriteLine("find: {0}", value);
    
            Console.WriteLine("Query time: {0:0.00} msec", timer.Elapsed.TotalMilliseconds);
    
            
            
            Console.WriteLine("\n\n\nQuery Test (non-indexed):");
            timer.Restart();
    
            foreach (string value in (from x in list2 where x == "888365" select x)) 
                Console.WriteLine("find: {0}", value);
    
            foreach (string value in (from x in list2 where x == "663867" select x)) 
                Console.WriteLine("find: {0}", value);
    
            foreach (string value in (from x in list2 where x == "555600" select x)) 
                Console.WriteLine("find: {0}", value);
    
            foreach (string value in (from x in list2 where x == "888824" select x)) 
                Console.WriteLine("find: {0}", value);
    
            Console.WriteLine("Query time: {0:0.00} msec", timer.Elapsed.TotalMilliseconds);
        }
    
        private static IEnumerable<string> SeqGenerator(int count)
        {
            for (int i = 0; i < count; i++) yield return i.ToString();
        }
    
        private static IEnumerable<string> RndSeq(int seed, int count)
        {
            Random rnd = new Random(seed);
            foreach (string value in (from x in SeqGenerator(count) orderby rnd.Next() select x)) yield return value;
        }
    }
    

    而程式執行的結果:

    貼一下參考配備 (這是炫耀文…),給大家體會一下速度…

    CPU: INTEL i7 2600K, INTEL Z68 主機板 RAM: 8GB OS: Windows 7 (x64)

    看起來藉著 HashSet 當索引,來加速 LINQ 查詢的目的已經達到了。不加索引需要 2147.83 ms, 而加上索引只需要 2.19ms … 兩者時間複雜度分別是 O(1) v.s. O(n), 要是資料的筆數再往上加,兩者的差距會更大…

    回過頭來看看程式碼吧! 關鍵在 LINQ 的 Extension 上面:

    支援索引的 LINQ extension: where( )

    public static IEnumerable<string> Where(this IndexedList context, Expression<Func<string, bool>> expr)
    {
        if (expr.CanReduce == false "" expr.Body.NodeType == ExpressionType.Equal)
        {
            BinaryExpression binExp = (BinaryExpression)expr.Body;
            string expectedValue = (binExp.Right as ConstantExpression).Value as string;
            if (context._index.Contains(expectedValue)) yield return expectedValue;
        }
        else
        {
            //return context.Where(expr);
            foreach (string value in context.Where(expr)) yield return value;
        }
    }
    

    line 1 的宣告,限制了只有對 IndexedList 型別的物件,下達 WHERE 語句的情況下,才會交由你的 extension method 執行。而 Microsoft 會把整個 where 語句,建構成 Expression 這型別的物件。

    前面我下的一堆條件,像是只能用 == 運算元,只針對字串常數比對等等,就都寫在 line 3 的判段式了。基本上只要不符合這些條件,就會跳到 line 12, 索引等於完全無用武之地了。

    要是符合使用索引的條件,則程式會進到 line 5 ~ line 7.  程式會透過 HashSet 去檢查要找的 data 是否存在於 List 內? 前面提過了,用 HashSet.Contains(...) 來檢查 (O(1)),效能遠比 List<string>.Contains(...) 的效能 (O(n) ) 好的多。

    實驗的結果到此為止,很明顯了,索引的確發揮出效果了,不過老實說,這段 code 真的沒什麼實用的價值… 哈哈,與其這樣繞一大圈,用 LINQ 很帥氣的查資料,還不如直接用程式找 HashSet 就好… 這只是證明 it works, 不繼續完成它,主要的原因是已經有人做了,而且做的比我好 XD! 需要的人可以參考看看 i4o 這套 library. 想體會這東西怎麼用,可以先看看作者的 BLOG,這篇是使用範例:

    http://staxmanade.blogspot.com/2008/12/i4o-indexspecification-for.html

    要下載的話,它的 project hosted 在 codeplex: http://i4o.codeplex.com/

    2011/10/25 C# 技術隨筆 物件導向

  4. [CODE] 可以輸出到 TextBox 的 TextWriter: TextBoxWriter!

    吃葡萄不吐葡萄皮... 對,這不是繞口令... 哈哈...。

    先祝各位 2011 新年快樂!!  這幾個月忙翻了, 一方面系統要移轉到 windows azure, 另一方面要改善原本系統的排程 (原本是 console app + scheduler task service), 現在要用 windows service 來取代, 突然之間之前覺的工作上用不到的 multi-threading 技巧, 現在都派上用場了...

    題外話先扯一下, 在 azure 因為架構上就已經把 web role (presentation layer) / worker role (business logic layer) / azure storage (data layer) 分的很清楚, 相對的每一層之間的 communication 時間就拉長了, 我們碰到的狀況就是 worker role 對 azure storage 有大量的 I/O 的話, 效能就很糟糕... 在這邊用了生產線的模式, 結果效能提升了十幾倍... 果然前輩說的沒錯: 架構對了,什麼事都變簡單了...

    另一個案例則是把原本靠 windows task scheduler 執行的排程,改成用自行開發的 windwos service ... 不過開發成 service 實在是不大好除錯, 再加上多執行緒先天也是除錯不易... 因此開發階段都寫成windows form, 提供 START / STOP / PAUSE / CONTINUE 等控制的功能,簡化前段的開發作業。

    azure / service 都用到了不少多執行緒的技巧, 改寫成 service 也用了老字號的 Cassini 解決另一部份的問題,這些都是值得介紹一下的部份,不過今天主題不在這,改天另起專欄再說。在改寫成 windows service 的過程中,需要把過去寫成 Console application 的 code 移到 service ,而過去這些程式都直接用 Console.Out 這個 TextWriter 輸出訊息到 DOS command prompt / log file, 移到 windows form 之後, 要輸出到 TextBox 就變的麻煩了點, 本來不想理它, 後來發現不好好處理這個問題還不行...

    如果不講究 code 好不好看,其實改一下程式就可以了,把原本的 Console.Out.WriteLine( ) 改成 TextBox.AppendText( ) 就好,但是...

    1. 原程式得改寫,不能直接用 TextWriter ... 另外, 很多現有的函式庫都接受 TextWriter, 不能靠 TextWriter 輸出的話程式會變很雜
    2. 只有 create control 的 UI thread 可以呼叫 AppendText( ) ...
    3. thread-safe 的問題: 輸出的訊息會交錯輸出, 無法閱讀
    4. 長時間連續執行,TextBox 的訊息會太多, 得要有 recycle 的機制
    5. 最好可以順便寫 LOG 檔...

    老實說,除了 (1) 之外,其它點都是自己寫個專用的 OutputLog method 就可以解決。不過考量到程式未來真正執行時,是在 service 的環境, 在那邊是沒有 TextBox 這種 UI control 的, 最終還是要透過 TextWriter 輸出到 log file, 或是透過 socket 輸出... 何況用 TextWriter 輸出 log 也比較符合抽象化的原則, 不需要去碰到太多輸出方式的細節...

    問題跟目的都搞清楚之後,接下來就是實作了。這次實作目標就是要想個辦法,用 TextWriter 的方式輸出 LOG 訊息,而這些訊息要自動呈現在 WebForm 的 TextBox control 上, 就好像在 DOS command prompt 裡執行 console application 一樣。先來想像一下 code 寫起來跟看起來是什麼樣子:

     

    [copy code]
       1:  private TextWriter output;
       2:  public Form1()
       3:  {
       4:      InitializeComponent();
       5:      this.output = TextWriter.Synchronized(new TextBoxWriter(this.textBox1));
       6:  }
       7:  private void Form1_Load(object sender, EventArgs e)
       8:  {
       9:      for (int i = 0; i < 300; i++)
      10:      {
      11:          this.output.WriteLine("{0}. Hello!!!", i);
      12:      }
      13:  }

     

     

    建立好 TextWriter 之後,後面只管用這 writer 輸出訊息,對應的 TextBox 自然就會像 terminal 般,不斷的跑出訊息出來...

    image

    基本要求達到了,這就是我要的功能。其實這東西上網 GOOGLE 一下, 類似的例子也有, 不過一來寫起來沒幾行, 二來功能跟我要的有點出入... 想想還是自己寫一個。先來看看這個 writer 是怎麼實作的?

     

    TextWriter 要實作並不難,不過不熟悉的話,也是得花些時間才能搞定。主要是 TextWriter 的 Writer( )WriterLine( ) 就共有 35 種不同的多載... HELP 沒有清楚的寫到這堆 method 之間的關係,一時也不知 override 那一個的效果是最好的...。一開始我是從最基本的 void TextWriter.Write(char value) 下手, 結果效能慘不忍睹... 輸出一行字就要一秒鐘, 好像回到了當年DOS時代加上倚天中文後, 按個DIR字是一行一行跑出來的光景... Orz, 後來花了點時間追蹤這卅幾個 method 的先後關係,才改了這個效能 OK 的版本:

    [copy code]
       1:  public class TextBoxWriter : TextWriter
       2:   {
       3:       private TextBox _textbox;
       4:       public TextBoxWriter(TextBox textbox)
       5:       {
       6:           this._textbox = textbox;
       7:       }
       8:       public override void Write(char value)
       9:       {
      10:           this.Write(new char[] { value }, 0, 1);
      11:       }
      12:       public override void Write(char[] buffer, int index, int count)
      13:       {
      14:           if (this._textbox.InvokeRequired)
      15:           {
      16:               this._textbox.Invoke((Action<string>)this.AppendTextBox, new string(buffer, index, count));
      17:           }
      18:           else
      19:           {
      20:               this.AppendTextBox(new string(buffer, index, count));
      21:           }
      22:       }
      23:       private void AppendTextBox(string text)
      24:       {
      25:           this._textbox.AppendText(text);
      26:       }
      27:       public override Encoding Encoding
      28:       {
      29:           get { return Encoding.UTF8; }
      30:       }
      31:   }

     

    其中補充說明一下, 在 line 14 ~ 21 為何要大費周章的, 把要呼叫 AppendTextBox( ) 的動作, 放到 delegate, 再交由 TextBox 控制項自行 Invoke 呼叫 ? 這是源自 windows form 的一條規範, 要是你寫的程式有多執行緒的應用,一定要知道這鐵律:

    UI thread 的限制: 只有 create control 的 thread 可以更動 control 的狀態。

    如果不遵守這個規則,執行時就會被警告:

    image

    解法就參考 line 14 ~ 21 的部份就好,說明的話 darkthread 這篇講的很清楚, 我就不客氣直接引用了 :D

    http://blog.darkthread.net/blogs/darkthreadtw/archive/2007/09/29/tips-about-ui-thread-limitation.aspx

     

     

     

    不過,這樣只是達到基本需求而已,還有其它的問題... (3 ~ 5), 太久沒寫文章了,沒想到一篇寫不完... 請待下回分解 :D

    2011/01/03 .NET ASP.NET C# 多執行緒 技術隨筆

  5. Extension Methods 的應用: Save DataSet as Excel file...

    好久沒寫東西了,趁著還記得 C# 怎麼寫的時後多補幾篇 =_= 要靠程式輸出Excel已經是個FAQ級的問題了,看過我之前文章的大概都知到,我很懶的寫那種FAQ級的東西,不是說寫了沒用,而是太多人寫,一定有寫的比我好的... 到最後連我自己忘了都會去查別人寫的,那我寫了還有啥用? 所以當然是有些特別的東西,才會想寫這篇...  我碰到的問題是,程式要處理的都是 DataSet 物件,不過 DataSet 物件是從 Excel 來的,處理完也要能回存回 Excel ..., 只不過為了先把 POC 作完,現在是以 DataSet 原生的 XML 格式代替。 其實以儲存的角度來看,XML很好用。不過要教會客戶編輯XML可是個大挑戰啊... 像 Excel 這樣有個表格還是比較容易上手一點。原本的程式長的像這樣:  

    原本的程式 (處理 dataset xml)[copy code]
                DataSet ds = new DataSet();
                ds.ReadXml("data.xml");
                // do something
                ds.WriteXml("data.xml");
    
  我對程式碼的要求其實還蠻龜毛的,常常光為了一個變數名字取的好不好,就要想半天... 對於程式的結構寫起來漂不漂亮也是。上面的程式,要把 XML 換成 EXCEL 其實很簡單,把 ReadXml( ) 換成對等的載入 Excel 的程式碼,WriteXml( ) 也換成對等的輸出 Excel 就可以收工了。不過看起來就是不順眼,因為被太多細節干擾了,未來回過頭來看自己寫的 code, 如果一眼望去不能馬上看出這段 code 在做什麼,那就是不及格的作品了... 所以,我想要的 code 最好能像這樣:  
期望的程式 (處理 excel 檔)[copy code]
            DataSet ds = new DataSet();
            ds.ReadExcel("data.xls");
            // do something
            ds.WriteExcel("data.xls");
  看起來酷多了,這段程式很清楚的說明他在幹嘛... load excel, processing... and save excel... 繼承的作法,想都不用想就不考慮了。因為我的程式要嘛就直接用 System.Data.DataSet, 不然就是配合 XSD 讓 visual studio 替我 generate typed dataset 的類別... 這些情況下要用到我自訂的衍生類別都不方便... 於是我就把念頭動到 C# 的 extension ... 要怎麼把 DataSet 存成 Excel, 這就是典型的 FAQ 級的問題了,請各位 GOOGLE 一下就有了,不然文後我也附了幾個參考連結...。這裡的重點是 C# extension, 它很神奇的能讓你不需要重新編譯,也不需要拿到原始碼,就能 "擴充" 原本類別的能力。靠這樣的機制,我就能夠改造 .NET Framework 內建的 DataSet, 把它改造成我想要的樣子,如上面的範例用的 ReadExcel( ) / WriteExcel( )。 C# Extension 是我慣用的說法,其實它正統的名字是 Extension Methods, 是 .NET 3.0 之後提供的功能,不只 C#,VB.NET 也支援。它能讓你在現有的 class 上 "附加" 新的 method ...  是的,沒錯,它的能力有限,只能增加 "method", 而且只能是 instance method, 不能是 class ( static ) method, 也不能是 property 或 field ... 不過這樣也足夠了,先來看看這種 code 要怎麼寫:
ExtensionMethods 示範[copy code]
    public static class NPOIExtension
    {
        public static void ReadExcel(this DataSet ds, string inputFile)
        {
            //do something
        }

        public static void WriteExcel(this DataSet ds, string outputFile)
        {
            //do something
        }
    }
  其實這又是另一種 Microsoft 提供的 Syntex Sugar 而已.. 注意到關鍵在那邊了嗎? 其實 "extension methods" 只不過是普通的 static method 而已,關鍵就在它的第一個參數,型別就是要附加的 class, 而宣告時要再額外加個 "this" modifier 來標示它,意思就是在裡面的 code, 這個參數就把它當作 "this" 來用 ... 換個角度看,其實這只是編譯器幫我們動一點手腳而已,好讓我們的 code 看起來會漂亮一點。用 extension methods 寫的 code, 其實跟這樣的寫法是完全同等的:
不用 extension methods 的寫法[copy code]
        DataSet ds = new DataSet();
        NPOIExtension.ReadXml(ds, "data.xml");
        // do something
        NPOIExtension.WriteXml(ds, "data.xml");



     public static class NPOIExtension
    {
       public static void ReadExcel(DataSet context, string inputFile)
        {
            // do something
        }
       public static void WriteExcel(DataSet context, string outputFile)
        {
            // do something
        }
    }
  少了 this 的用法,看起來就沒這麼神奇了。就是一般的 static method 而已。我喜歡 C# 就是喜歡它有很多這類的 syntax sugar, 可以滿足我寫程式想把程式碼弄的漂漂亮亮的慾望... extension methids 就是一例,還有像 yield return, attribute 等等也是個經典... 這樣的改變讓我想起,以前公司新進工程師我都會教一堂課,就是講物件是怎麼一回事? 最早是 C / C++ 版,後來改用 javascript 來講這門課。講的就是一步一步示範如何由 procedure oriented language 轉變到 object oriented language 的過程。C++ 是個最清楚的例子,它靠 C 的 function pointer + struct, 用指標指向 function, 把 data 及 function 包裝在 struct 裡,就變成一個 object. (所以 C++ 的 struct 才會跟 class 這麼像啊)。而繼承的問題,則是在 struct 裡動一點手腳,用 virtual table 的方式,讓你寫起 code 來 "好像" 真的有物件導向這麼回事。其實 C++ 的 method, 也只是個普通的 function, 只不過它第一個參數一定叫作 "this" 而已... 扯遠了,整篇的目的只是要講,用 Extension Method 可以把 code 包的很漂亮,而且也真的可以動而已... 哈哈,最後貼一下完整的 source code, 證明我沒有唬爛... 程式我調一下,我只實作了 WriteExcel( ) 的部份,從 DataSet XML 讀進來,存成 Excel, 單純的一個轉檔程式。而轉換的過程中是這樣對應的: 一個 DataSet 就代表一個 EXCEL workbook,而一個 DataTable, 則對應到 EXCEL sheet。DataTable 的 Row / Column, 當然就是對應到 EXCEL Row 跟 Cell 了:
完整的 source code[copy code]
    public class Program
    {
        static void Main(string[] args)
        {
            DataSet ds = new DataSet();
            ds.ReadXml("data.xml");
            // do something
            ds.WriteExcel("data.xls");
        }
    }
    
    public static class NPOIExtension
    {
        public static void WriteExcel(this DataSet context, string outputFile)
        {
            HSSFWorkbook workbook = new HSSFWorkbook();

            foreach (DataTable table in context.Tables)
            {
                Sheet sheet = workbook.CreateSheet(table.TableName);

                Row headerRow = sheet.CreateRow(0);
                for(int cpos = 0; cpos < table.Columns.Count; cpos++)
                {
                    Cell cell = headerRow.CreateCell(cpos);
                    cell.SetCellType(CellType.STRING);
                    cell.SetCellValue(table.Columns[cpos].ColumnName);
                }

                int rpos = 0;
                foreach (DataRow row in table.Rows)
                {
                    rpos++;
                    Row sheetRow = sheet.CreateRow(rpos);
                    for (int cpos = 0; cpos < table.Columns.Count; cpos++)
                    {
                        object value = row[cpos];
                        Cell cell = sheetRow.CreateCell(cpos);
                        cell.SetCellValue((value == null) ? (null) : (value.ToString()));
                    }
                }
            }

            if (File.Exists(outputFile)) File.Delete(outputFile);
            FileStream fs = File.OpenWrite(outputFile);
            workbook.Write(fs);
            fs.Close();
        }
    }
  對啦,這真的是全部的 code ... 開個 console project 就可以跑了。我就說我很少貼超過百行的 code ... 哈哈 順帶一提,NPOI 這個 open source project 做的真不錯,可以在 pure .net 環境下就能處理 excel ... 這個範例就是用 NPOI 寫的,有興趣的朋友可以參考看看!         ------------- Reference:
  1. MSDN:  Extension Methods (C# Programming Guide) http://msdn.microsoft.com/en-us/library/bb383977.aspx
  2. NPOI (in CodePlex.com) http://npoi.codeplex.com/
  3. MSDN 學習園地的 NPOI 介紹文章 (中譯版) 在 Server 端存取 Excel 檔案的利器:NPOI Library http://msdn.microsoft.com/zh-tw/ee818993.aspx
  4. 另一套類似NPOI 的函式庫: Koogra Koogra Excel BIFF/XLSX Reader Library http://koogra.sourceforge.net/
  5. 直接用 OpenXML format 的範例 http://www.dotblogs.com.tw/mis2000lab/archive/2008/04/24/3454.aspx
  6. Microsoft Open XML SDK 1.0 / 2.0 for Microsoft Office 1.0 (http://www.microsoft.com/downloads/en/details.aspx?FamilyId=AD0B72FB-4A1D-4C52-BDB5-7DD7E816D046&displaylang=en) 2.0 (http://www.microsoft.com/downloads/en/details.aspx?FamilyID=c6e744e5-36e9-45f5-8d8c-331df206e0d0&DisplayLang=en)
後記: 本來有寫一段,是整理到底有幾種方法,可以在 .NET 環境下輸出 EXCEL 的... 不過後來決定沒放在本文,就挪到 reference ... 簡單的比較,需要的可以看看
  1. 直接用 Excel 提供的物件 這種方式就是透過程式去操作 Excel 。優點是只要 Excel 有提供的功能,大概都做的到。不過也因為功能太多,用起來很複雜。這方法致命的缺點就是效能。它相當於你真的開啟 Excel 然後命令它做事,想像一下,要能很順暢的執行 Excel 的機器,都不會太差。單機的 windows form 程式還好,如果碰到 server side 的程式,如 ASP.NET 這種,只要同時有五個人點這個功能,你的 server 大概就跑不動了...。
  2. 透過 Jet odbc / oledb driver 這種方式沒有第一種的缺點,不過也沒有它的優點。既然是透過 odbc 這類存取資料庫的 driver 來存取,那麼你也只能把 Excel 檔當成某種資料庫來看待。處理資料還不成問題,要套用公式或是輸出畫面就辦不到了。這方法還有個大缺點,Jet driver 沒有 x64 的版本 .... windows 2008 r2 之後就沒有 x86 版了,這方法不是長久之計...
  3. 輸出 HTML, 用 Excel 開啟 這方法很簡單,只要輸出 HTML table, 再調整 content type, 讓 browser 叫 excel 出來幫你開啟就好了。老實說我一直覺的這招有點投機 XD,好處是很簡單,缺點是對 EXCEL 的掌控程度很低,大概就是幫你把資料貼進去的程度而已,不能用公式,也不能輸出多個 sheet 。
  4. Open XML Format 直接輸出 Office 2007  開始,就放棄用了十幾年的 Ole structed storage compound file 二進位檔的格式,改以 XML 為主 (當然還是支援舊版的格式啦)。只要你知道它的 schema, 不需要太複雜的步驟就可以直接輸出 .xlsx 了。 熟 XML 的話,甚至是寫寫 XSLT 就可以生的出來。不過這還蠻考驗你對 XML / Open XML 的掌握程度... 算是小有門崁的作法。
  5. 透過 NPOI / Koogra 這類 3rd party 含式庫 這些含式庫都可以不需要安裝龐大的 Excel, 就有處理 excel 檔的能力。跟 Open XML 不同的是,這些 library 能夠處理到比較頭痛的 2003 及之前版本的 binary file, 相容性比 XML 來的好。

2010/11/06 .NET C# MSDN Tips 有的沒的 技術隨筆