The Framework Designing (3) – Configurable Application

前一篇中,我們設計了GridViewHandler及FormViewHandler,讓商業邏輯可以由主程式中抽離,放置於外部來動態選擇要載入那些商業邏輯,就該例而言,這個設計除了將原本該

置於Data Layout的商業邏輯與UI扯上關聯外,其實並無其它設計較為不當之處,而將商業邏輯與UI扯上關聯這點,其實也是為了讓範例更加簡單易懂而特意設計的,要將這種設計

移置Data Layout裡也很簡單。

The Framework Designing (3) – Configurable Application

 

 

/黃忠成

 

 

 

re-designing or not?

 

  前一篇中,我們設計了GridViewHandler及FormViewHandler,讓商業邏輯可以由主程式中抽離,放置於外部來動態選擇要載入那些商業邏輯,就該例而言,這個設計除了將原本該

置於Data Layout的商業邏輯與UI扯上關聯外,其實並無其它設計較為不當之處,而將商業邏輯與UI扯上關聯這點,其實也是為了讓範例更加簡單易懂而特意設計的,要將這種設計

移置Data Layout裡也很簡單。

  但,這個設計有一個設想未周全之處,那就是每個Element、也就是每個Table都只能掛載一個Handler,這點在開發初期並不會造成困擾,但在中後期當同系列的產品變多時,

就會造成如圖1的困擾。

 

圖1

如圖1所示,Product A、B、C屬於同一系列的產品,透過Handler來客製屬於該客戶的商業邏輯,但由於前篇所設計的延展點受限於每個Table只能掛載一個Handler,所以在產品線展開時,

就會造成Handler中包含過多的重複程式碼,以圖1來說,每個Handler都擁有設定Company Name預設值的能力,但因為不同的產品有著不同數量的預設值需求,所以此處為了適應,

只好將Company Name預設值的能力重複出現在之後延展的Handler裡,最好的設計應該如圖2。 


圖2

  圖2中假設當一個Table可以掛載多個Handler後的情況,在這種設計下,每個Handler都有各自的任務,可以透過組合來完成某個客戶的需求,這也是具延展性應用程式的主要設計架構。

  圖2將原本GridViewHandler、FormViewHandler設計上的缺點揭露無遺,這使得我們必須思考是否要重新設計GridViewHandler及FormViewHandler,這也是當設計Framework或

系統架構時設計師最常遇到的問題,如果這個缺點在設計之初即察覺到,那麼只要重新修改設計概念即可,但有些時候這類問題會出現在產品開發的中後期,此時要更改原設計所需

付出的成本很大,因為得逐一調整開發完成的部分。

  當修改原設計須付出的代價是無法或難以承受之時,此時就得往外科手術的方向著手,以本例而言,雖然每個Table只能掛載一個Handler,但這並不構成我們無法在一個Table掛載

Handler的限制,因為只要將掛載的的這一個Handler設計成可以呼叫多個子Handler即可,簡略的說就是將延伸點往下放到Handler上,如圖3所示。

 

圖3

在圖3中,原本CustomerExt是掛載至GridViewHandler的單一Handler,但如果這樣做就會造成圖1的問題,所以圖3中加入了一個SaveExtDispatcher,其掛載至GridViewHandler,

並將呼叫下放至其動態掛載的CustomerExt1、CustomerExt2上,這種做法就可以達到圖2的架構,這是Dispatcher樣式的應用。

  乍看之下,這樣的設計要達到圖2的架構似乎很簡單,不過裡頭潛在一個問題,讓我們先思考SaveExtDispatcher的設計。在SaveExtDispatcher中,會動態的由一個組態檔讀入要

掛載的Handler,並將來自GridViewHandler的呼叫一一下放給這些Handler,實作上,SaveExtDispatcher一定會擁有類似於GridViewHandler中讀取組態檔及動態載入的程式碼,

一切都沒有問題,直到將SaveExtDispacther掛到GridViewHandler中時,組態檔便會把問題點出來。

 

<appSettings>

    <addkey="Customers"value="SaveExtDispatcher.CustomerExtDispatcher, SaveExtDispatcher"/>

  </appSettings>

看出問題了嗎? 遷就於原本架構,每個Table都必須要有一個獨立的SaveExtDispatcher,所以,[會動態的由一個組態檔讀入要掛載的Handler,並將來自GridViewHandler的呼叫

一一下放給這些Handler]這件事會出現在每一個Table所掛載的獨立SaveExtDispatcher中。

  解決這個問題的方法很簡單,就直接把會重複的部分提出來寫成共用的Library即可,本例中就是DataHandlerDispacther.dll。

 

DataHandlerDispatcher.cs (in DataHandlerDispatcher project)

using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;

using System.IO;

using System.Xml.Linq;

using System.Reflection;

using SimpleFramework;


namespace DataHandlerDispatcher

{

    public abstract class DataHandlerDispatcher:IDataHandler

    {

        private List _innerHandlers = null;


        protected abstract string GetConfigurationFileName();


        private void Initialize(string configurationFile)

        {

            string appPath = AppDomain.CurrentDomain.BaseDirectory + "\\" + configurationFile;

            if(!File.Exists(appPath))

                return;

            XDocument doc = XDocument.Load(appPath,LoadOptions.None);

            var result = from s1 in doc.Descendants("handler") select s1;

            _innerHandlers = new List();

            foreach (var item in result)

            {

                if (item.Attribute("type") != null)

                {

                    string[] partAssem = item.Attribute("type").Value.Split(',');

                    IDataHandler _instance = TypeLoader.CreateInstance(partAssem[1],

partAssem[0], "SimpleFramework.IDataHandler") as IDataHandler;

                    _innerHandlers.Add(_instance);

                }

            }

        }


        public void BeforeSave(System.Collections.Specialized.IOrderedDictionary values)

        {

            if (_innerHandlers == null)

                Initialize(GetConfigurationFileName());

            if (_innerHandlers != null)

            {

                foreach (var item in _innerHandlers)

                    item.BeforeSave(values);

            }

        }

    }

}

接著只要讓SaveExtDispatcher繼承自此類別即可。

 

SaveExtDispatcher.cs (in SaveExtDispatcher project)

using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;

using DataHandlerDispatcher;


namespace SaveExtDispatcher

{

    public class CustomerExtDispatcher:DataHandlerDispatcher.DataHandlerDispatcher

    {

        protected override string GetConfigurationFileName()

        {

            return "SaveExtDispatcher.config";

        }

    }

}

SaveExtDispatcher會讀取執行目錄下的SaveExtDispatcher.config,本例中如下所示。

 

SaveExtDispatcher.config(in web/winform project)

<?xmlversion="1.0"encoding="utf-8"?>

<handlers>

  <handlertype="SaveExt.CustomerExt, SaveExt"/>

<!--

  <handlertype="SaveExt2.CustomerExt, SaveExt2"/>

--!>

</handlers>

最後只要將SaveExtDispatcher掛到GridViewHandler/FormViewHandler即可,你可以於CreateStep_1目錄中找到整個完整範例。

 

writing configuration section handler

 

  當然,前節的做法是不得已下的產物,改寫架構才是根本的解決之道,問題的根源是前例就簡使用了只能夠指定單值的appSetting,透過.NET Framework的

Configuration Framework,我們可以自訂一個專屬的Section來儲存Handlers的設定,如下所示。

 

app.config

<?xmlversion="1.0"encoding="utf-8"?>

<configuration>

  <configSections>

    <sectionGroupname="simpleFramework">

      <sectionname="dataHandlers"

 type="SimpleFramework.Configuration.DataHandlerConfigurationHandler, SimpleFramework"

               allowLocation="true"   allowDefinition="Everywhere"/>

    </sectionGroup>

  </configSections>

  <connectionStrings>

    <addname="WindowsFormsApplication1.Properties.Settings.NorthwindConnectionString"

        connectionString="Data Source=127.0.0.1;Initial Catalog=Northwind;Integrated Security=True"

        providerName="System.Data.SqlClient"/>

  </connectionStrings>

  <simpleFramework>

    <dataHandlers>

      <items>

        <dataItemkey="Customers">

          <handlers>

            <handlertype="SaveExt.CustomerExt, SaveExt"/>

          </handlers>

        </dataItem>

      </items>

    </dataHandlers>

  </simpleFramework>

  <appSettings>

  </appSettings>

</configuration>

DataHandlerConfigurationHandler的原始碼如下。

 

DataExtensionConfigurationHandler.cs(in SimpleFramework)

using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;

using System.Configuration;


namespace SimpleFramework.Configuration

{

    public class DataHandlerConfigurationHandler:ConfigurationSection

    {

        [ConfigurationProperty("items", IsDefaultCollection = true)]

        [ConfigurationCollection(typeof(DataHandlerSectionElementCollection),

AddItemName = "dataItem")]

        public DataHandlerSectionElementCollection Items

        {

            get

            {

                return (DataHandlerSectionElementCollection)this["items"];

            }

        }


        public static List GetHandlers(string key)

        {

            DataHandlerConfigurationHandler handler =               (DataHandlerConfigurationHandler)System.Configuration.ConfigurationManager.GetSection(

"simpleFramework/dataHandlers");

            foreach (DataHandlerSectionElement item in handler.Items)

            {

                if (item.Key == key && item.Handlers.Count > 0)

                    return item.Handlers.GetHandlers();

            }

            return null;

        }

    }


    public class DataHandlerSectionElement : ConfigurationElement

    {

        [ConfigurationProperty("key", IsRequired = true, IsKey = true)]

        public string Key

        {

            get

            {

                return (string)this["key"];

            }

        }


        [ConfigurationProperty("handlers", IsDefaultCollection = true)]

        [ConfigurationCollection(typeof(DataHandlerElementCollection), AddItemName = "handler")]

        public DataHandlerElementCollection Handlers

        {

            get

            {

                return (DataHandlerElementCollection)this["handlers"];

            }

        }

    }


    public class DataHandlerSectionElementCollection : ConfigurationElementCollection

    {

        protected override ConfigurationElement CreateNewElement()

        {

            return new DataHandlerSectionElement();

        }


        protected override object GetElementKey(ConfigurationElement element)

        {

            return ((DataHandlerSectionElement)element).Key;

        }

    }


    public class DataHandlerElement : ConfigurationElement

    {

        [ConfigurationProperty("type", IsRequired = true, IsKey = true)]

        public string Type

        {

            get

            {

                return (string)this["type"];

            }

        }


        public IDataHandler GetHandler()

        {

            if (!string.IsNullOrEmpty(Type))

            {

                string[] partAssem = Type.Split(',');

                return TypeLoader.CreateInstance(partAssem[1], partAssem[0],

"SimpleFramework.IDataHandler") as IDataHandler;

            }

            return null;

        }

    }


    public class DataHandlerElementCollection : ConfigurationElementCollection

    {

        protected override ConfigurationElement CreateNewElement()

        {

            return new DataHandlerElement();

        }


        protected override object GetElementKey(ConfigurationElement element)

        {

            return ((DataHandlerElement)element).Type;

        }


        public List GetHandlers()

        {

            List result = new List();

            foreach (DataHandlerElement item in this)

                result.Add(item.GetHandler());

            return result;

        }

    }

}

隨著DataExtensionConfigurationHandler的加入,GridViewHandler與FormViewHandler也要做些修改來適應。

 

GridViewHandler.cs

using System;

using System.Collections.Generic;

using System.Collections.Specialized;

using System.Linq;

using System.Text;

using System.Windows.Forms;

using System.Configuration;


namespace SimpleFramework.WinForms

{

    public class GridViewHandler

    {

        private List _instances;

        private DataGridView _view;


        public void Initialize(string key, DataGridView view)

        {

            view.RowValidating += new DataGridViewCellCancelEventHandler(view_RowValidating);

            _view = view;

            _instances =

SimpleFramework.Configuration.DataHandlerConfigurationHandler.GetHandlers(key);

        }


        void view_RowValidating(object sender, DataGridViewCellCancelEventArgs e)

        {

            OrderedDictionary values = new OrderedDictionary();

            foreach (DataGridViewCell item in _view.Rows[e.RowIndex].Cells)

            {

                if(item.Value is DBNull)

                    values.Add(_view.Columns[item.ColumnIndex].DataPropertyName, null);

                else

                    values.Add(_view.Columns[item.ColumnIndex].DataPropertyName, item.Value);

            }

            foreach (var item in _instances)

                item.BeforeSave(values);

            foreach (DataGridViewCell item in _view.Rows[e.RowIndex].Cells)

                item.Value = values[_view.Columns[item.ColumnIndex].DataPropertyName];

        }

    }

}

FormViewExt.cs

using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;

using System.Web.UI.WebControls;

using System.Configuration;

using System.Reflection;

using SimpleFramework;


namespace SimpleFramework.Web

{

    public class FormViewHandler

    {

        private List _instances;


        public void Initialize(string key, FormView view)

        {

            _instances =

SimpleFramework.Configuration.DataHandlerConfigurationHandler.GetHandlers(key);

            view.ItemInserting += new FormViewInsertEventHandler(view_ItemInserting);

            view.ItemUpdating += new FormViewUpdateEventHandler(view_ItemUpdating);

        }


        void view_ItemInserting(object sender, FormViewInsertEventArgs e)

        {

            foreach (var item in _instances)

                item.BeforeSave(e.Values);

        }


        void view_ItemUpdating(object sender, FormViewUpdateEventArgs e)

        {

            foreach (var item in _instances)

                item.BeforeSave(e.NewValues);

        }

    }

}

CreateStep_2中可找到此例的原始碼。

 

using dispatcher pattern

 

  照上節的設計,GridViewHandler/FormViewHandler都要做出修改,那有沒有更簡單的方法呢?有的,只要應用先前用過的Dispatcher pattern即可。

 

DataExtensionConfigurationHandler.cs(in SimpleFramework)

using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;

using System.Configuration;


namespace SimpleFramework.Configuration

{

    public class HandlerDispatcher : IDataHandler

    {

        private List _innerHandlers = null;


        public HandlerDispatcher(List handlers)

        {

            _innerHandlers = handlers;

        }


        public void BeforeSave(System.Collections.Specialized.IOrderedDictionary values)

        {

            foreach (var item in _innerHandlers)

                item.BeforeSave(values);

        }

    }


    public class DataHandlerConfigurationHandler:ConfigurationSection

    {

        [ConfigurationProperty("items", IsDefaultCollection = true)]

        [ConfigurationCollection(typeof(DataHandlerSectionElementCollection), AddItemName = "dataItem")]

        public DataHandlerSectionElementCollection Items

        {

            get

            {

                return (DataHandlerSectionElementCollection)this["items"];

            }

        }


        public static IDataHandler GetHandler(string key)

        {

            DataHandlerConfigurationHandler handler =

               (DataHandlerConfigurationHandler)System.Configuration.ConfigurationManager.GetSection(

"simpleFramework/dataHandlers");

            foreach (DataHandlerSectionElement item in handler.Items)

            {

                if (item.Key == key && item.Handlers.Count > 0)

                    return item.Handlers.GetHandler();

            }

            return null;

        }

    }


    public class DataHandlerSectionElement : ConfigurationElement

    {

        [ConfigurationProperty("key", IsRequired = true, IsKey = true)]

        public string Key

        {

            get

            {

                return (string)this["key"];

            }

        }


        [ConfigurationProperty("handlers", IsDefaultCollection = true)]

        [ConfigurationCollection(typeof(DataHandlerElementCollection), AddItemName = "handler")]

        public DataHandlerElementCollection Handlers

        {

            get

            {

                return (DataHandlerElementCollection)this["handlers"];

            }

        }

    }


    public class DataHandlerSectionElementCollection : ConfigurationElementCollection

    {

        protected override ConfigurationElement CreateNewElement()

        {

            return new DataHandlerSectionElement();

        }


        protected override object GetElementKey(ConfigurationElement element)

        {

            return ((DataHandlerSectionElement)element).Key;

        }

    }


    public class DataHandlerElement : ConfigurationElement

    {

        [ConfigurationProperty("type", IsRequired = true, IsKey = true)]

        public string Type

        {

            get

            {

                return (string)this["type"];

            }

        }


        public IDataHandler GetHandler()

        {

            if (!string.IsNullOrEmpty(Type))

            {

                string[] partAssem = Type.Split(',');

                return TypeLoader.CreateInstance(partAssem[1], partAssem[0],

"SimpleFramework.IDataHandler") as IDataHandler;

            }

            return null;

        }

    }


    public class DataHandlerElementCollection : ConfigurationElementCollection

    {

        protected override ConfigurationElement CreateNewElement()

        {

            return new DataHandlerElement();

        }


        protected override object GetElementKey(ConfigurationElement element)

        {

            return ((DataHandlerElement)element).Type;

        }


        public IDataHandler GetHandler()

        {

            List result = new List();

            foreach (DataHandlerElement item in this)

                result.Add(item.GetHandler());

            return new HandlerDispatcher(result);

        }

    }

}

整個架構如圖4。

你可以在CreateExt_Step4找到完整的範例程式碼。

 

configuration providers

 

  當提出Simple Framework這種可抽換式的架構時,我常聽到的回應多半不是這些東西怎麼實作出來,而是組態檔所放置的位置,例如不想放在app.config/web.config裡,

想放在例如Database、另一個Server、或是由Web Service取得等等的需求,但實際上要達到這些需求並不難,比起設計一整個可抽換式架構來說,這簡直就是小兒科,

本文末尾將Simple Framework修改成如圖5所示。

圖5

簡單的說,將讀取組態檔的部分應用抽換式架構,平移至一個外部的Assembly中,形成可抽換式的組態檔來源。

 

DataExtensionConfigurationHandler.cs(in SimpleFramework)

using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;

using System.Configuration;


namespace SimpleFramework.Configuration

{

    public interface IDataHandlerConfigurationProvider

    {

        IDataHandler GetHandler(string providerConnectionString, string key);

    }


    public class HandlerDispatcher : IDataHandler

    {

        private List _innerHandlers = null;


        public HandlerDispatcher(List handlers)

        {

            _innerHandlers = handlers;

        }


        public void BeforeSave(System.Collections.Specialized.IOrderedDictionary values)

        {

            foreach (var item in _innerHandlers)

                item.BeforeSave(values);

        }

    }


    public class DataHandlerConfigurationHandler:ConfigurationSection

    {

        [ConfigurationProperty("items", IsDefaultCollection = true)]

        [ConfigurationCollection(typeof(DataHandlerSectionElementCollection),

AddItemName = "dataItem")]

        public DataHandlerSectionElementCollection Items

        {

            get

            {

                return (DataHandlerSectionElementCollection)this["items"];

            }

        }


        [ConfigurationProperty("provider")]

        public string Provider

        {

            get

            {

                return (string)this["provider"];

            }

        }


        [ConfigurationProperty("providerConnectionString")]

        public string ProviderConnectionString

        {

            get

            {

                return (string)this["providerConnectionString"];

            }

        }


        public static IDataHandler GetHandler(string key)

        {

            DataHandlerConfigurationHandler handler =

               (DataHandlerConfigurationHandler)System.Configuration.ConfigurationManager.GetSection(

"simpleFramework/dataHandlers");

            if (!string.IsNullOrEmpty(handler.Provider))

            {

                string[] partAssem = handler.Provider.Split(',');

                IDataHandlerConfigurationProvider provider =

                    (IDataHandlerConfigurationProvider)TypeLoader.CreateInstance(partAssem[1], partAssem[0],

                             "SimpleFramework.Configuration.IDataHandlerConfigurationProvider");

                return provider.GetHandler(handler.ProviderConnectionString, key);

            }


            foreach (DataHandlerSectionElement item in handler.Items)

            {

                if (item.Key == key && item.Handlers.Count > 0)

                    return item.Handlers.GetHandler();

            }

            return null;

        }

    }


    public class DataHandlerSectionElement : ConfigurationElement

    {

        [ConfigurationProperty("key", IsRequired = true, IsKey = true)]

        public string Key

        {

            get

            {

                return (string)this["key"];

            }

        }


        [ConfigurationProperty("handlers", IsDefaultCollection = true)]

        [ConfigurationCollection(typeof(DataHandlerElementCollection),

AddItemName = "handler")]

        public DataHandlerElementCollection Handlers

        {

            get

            {

                return (DataHandlerElementCollection)this["handlers"];

            }

        }

    }


    public class DataHandlerSectionElementCollection : ConfigurationElementCollection

    {

        protected override ConfigurationElement CreateNewElement()

        {

            return new DataHandlerSectionElement();

        }


        protected override object GetElementKey(ConfigurationElement element)

        {

            return ((DataHandlerSectionElement)element).Key;

        }

    }


    public class DataHandlerElement : ConfigurationElement

    {

        [ConfigurationProperty("type", IsRequired = true, IsKey = true)]

        public string Type

        {

            get

            {

                return (string)this["type"];

            }

        }


        public IDataHandler GetHandler()

        {

            if (!string.IsNullOrEmpty(Type))

            {

                string[] partAssem = Type.Split(',');

                return TypeLoader.CreateInstance(partAssem[1], partAssem[0], "SimpleFramework.IDataHandler") as IDataHandler;

            }

            return null;

        }

    }


    public class DataHandlerElementCollection : ConfigurationElementCollection

    {

        protected override ConfigurationElement CreateNewElement()

        {

            return new DataHandlerElement();

        }


        protected override object GetElementKey(ConfigurationElement element)

        {

            return ((DataHandlerElement)element).Type;

        }


        public IDataHandler GetHandler()

        {

            List result = new List();

            foreach (DataHandlerElement item in this)

                result.Add(item.GetHandler());

            return new HandlerDispatcher(result);

        }

    }

}

CreateExt_Step5目錄中就是應用此架構的範例,你可於其中找到XmlConfigurationProvider Project,其將會由另一個XML檔案中讀取組態檔。

 

XmlProvider.cs(in XmlConfigurationProvider project)

using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;

using System.Xml.Linq;

using System.Reflection;

using System.IO;

using SimpleFramework;

using SimpleFramework.Configuration;


namespace XmlConfigurationProvider

{

    public class XmlProvider:IDataHandlerConfigurationProvider

    {

        public SimpleFramework.IDataHandler GetHandler(string providerConnectionString, string key)

        {

            XDocument doc = XDocument.Load(AppDomain.CurrentDomain.BaseDirectory+"\\"+providerConnectionString);

            var keyRoot = (from s1 in doc.Descendants("dataItem") where s1.Attribute("key").Value == key select s1).FirstOrDefault();

            if (keyRoot != null)

            {

                var items = keyRoot.Descendants("handlers").FirstOrDefault().Descendants("handler");

                List list = new List();

                foreach (var item in items)

                {

                    string[] partAssem = item.Attribute("type").Value.Split(',');

                    list.Add((IDataHandler)TypeLoader.CreateInstance(partAssem[1], partAssem[0], "SimpleFramework.IDataHandler"));

                }

                return new HandlerDispatcher(list);

            }

            return null;

        }

    }

}

以下為web.config的組態內容。

 

<?xmlversion="1.0"?>

 

<!--

  For more information on how to configure your ASP.NET application, please visit

  http://go.microsoft.com/fwlink/?LinkId=169433

  -->

 

<configuration>

  <configSections>

    <sectionGroupname="simpleFramework">

      <sectionname="dataHandlers"type="SimpleFramework.Configuration.DataHandlerConfigurationHandler, SimpleFramework"

               allowLocation="true"   allowDefinition="Everywhere"/>

    </sectionGroup>

  </configSections>

    <connectionStrings>

        <addname="NorthwindConnectionString"

connectionString=

"Data Source=127.0.0.1;Initial Catalog=Northwind;Integrated Security=True"

            providerName="System.Data.SqlClient"/>

    </connectionStrings>

  <simpleFramework>

    <dataHandlers

provider="XmlConfigurationProvider.XmlProvider, XmlConfigurationProvider"providerConnectionString="handler.config"/>

  </simpleFramework>

    <system.web>

        <compilationdebug="true"targetFramework="4.0"/>

    </system.web>

</configuration>

handler.config的內容如下:

 

<?xmlversion="1.0"encoding="utf-8"?>

<items>

  <dataItemkey="Customers">

    <handlers>

      <handlertype="SaveExt.CustomerExt, SaveExt"/>

    </handlers>

  </dataItem>

</items>

 

本文後記

 

  說實在的,當你掌握了動態載入Assembly及設計組態檔的技巧後,要設計一個簡單的可抽換式架構就不難了,難得是當這些元件載入後彼此間的互動,

這又是另一個層次的問題了,未來本系列文章會持續地討論這些。

範例下載: http://code6421.myweb.hinet.net/Framework/Arch_2.zip