自訂類別的屬性與欄位如何列舉並提供控制項作為繫結來源?

我在「 陣列詳論」與「Enum詳論」兩篇文章中曾經介紹過如何將陣列以及 Enum 項目當作繫結控制項的資料來源。然而,無論是陣列或是 Enum 項目, 它們都有設定和使用上的巨大限制。其中, Enum 的每個項目都必須是常值, 換句話說, 項目的值無法動能控制。而陣列的值雖然可以動態變更, 但無法提供設計時的 Intellisense 支援...

我在「 陣列詳論」與「Enum詳論」兩篇文章中曾經介紹過如何將陣列以及 Enum 項目當作繫結控制項的資料來源。然而,無論是陣列或是 Enum 項目, 它們都有設定和使用上的巨大限制。其中, Enum 的每個項目都必須是常值, 換句話說, 項目的值無法動能控制。而陣列的值雖然可以動態變更, 但無法提供設計時的 Intellisense 支援。

 

建立自訂類別

如果你想綜合所有的優點, 你就必須使用自訂類別, 如下例:

public class clsPeriodForQuery
{
    public int 今天 { get { return 0; } }
    public int 今天跟昨天 { get { return 1; } }
    public int 最近三天 { get { return 2; } }
    public int 這個禮拜 { get { return (int)DateTime.Now.DayOfWeek; } }
    public int 最近兩個禮拜 { get { return 這個禮拜 + 7; } }
    public int 最近三個禮拜 { get { return 這個禮拜 + 14; } }
    public int 最近四個禮拜 { get { return 這個禮拜 + 21; } }
    public int 這個月 { get { return DateTime.Now.Day - 1; } }
    public int 最近兩個月
    {
        get
        {
            DateTime today = DateTime.Now;
            TimeSpan span = today - new DateTime(today.Year, today.AddMonths(-1).Month, 1);
            return span.Duration().Days - 1;
        }
    }
    public int 最近半年
    {
        get
        {
            DateTime today = DateTime.Now;
            TimeSpan span = today - new DateTime(today.Year, today.AddMonths(-6).Month, 1);
            return span.Duration().Days;
        }
    }
    public int 今年 { get { return DateTime.Now.DayOfYear - 1; } }
    public int 最近一年
    {
        get
        {
            DateTime today = DateTime.Now;
            TimeSpan span = (new DateTime(today.Year, 12, 31) - new DateTime(today.Year, 1, 1));
            return span.Duration().Days - 1;
        }
    }
}

以上這個類別內含「今天」、「今天跟昨天」、「最近三天」、「這個禮拜」、「最近兩個禮拜」、「最近三個禮拜」、「最近四個禮拜」、「這個月」、「最近兩個月」、「最近半年」、「今年」與「最近一年」等項目, 其值就是距離今天的天數。如同你可以看見的, 在以上各個項目中, 有些是常值(Constant), 有些必須經過運算才能得到。像這種結構, 你無法以 Enum 來表示。當然你可以改用陣列或集合(Collection)物件, 但是在設計時期卻無法列舉。但如果設計成自訂類別, 就可以達成所有的功能。

實作 IListSource 以作為資料來源

但是, 一般的自訂類別是無法直接提供給繫結控制項作為資料來源的。要讓一個自訂類別當作資料來源, 你的類別必須實作 IListSource、IEnumerable 或 IDataSource 才行。實作 IListSource 大概是最簡單、最容易的; 以下我展示一個已經寫好可用的範例:

using System.Reflection;
using System.ComponentModel;
...

public class clsPeriods :  IListSource
{
    public int 今天 { get { return 0; } }
    public int 今天跟昨天 { get { return 1; } }
    public int 最近三天 { get { return 2; } }
    public int 這個禮拜 { get { return (int)DateTime.Now.DayOfWeek; } }
    public int 最近兩個禮拜 { get { return 這個禮拜 + 7; } }
    public int 最近三個禮拜 { get { return 這個禮拜 + 14; } }
    public int 最近四個禮拜 { get { return 這個禮拜 + 21; } }
    public int 這個月 { get { return DateTime.Now.Day - 1; } }
    public int 最近兩個月
    {
        get
        {
            DateTime today = DateTime.Now;
            TimeSpan span = today - new DateTime(today.Year, today.AddMonths(-1).Month, 1);
            return span.Duration().Days - 1;
        }
    }
    public int 最近半年
    {
        get
        {
            DateTime today = DateTime.Now;
            TimeSpan span = today - new DateTime(today.Year, today.AddMonths(-6).Month, 1);
            return span.Duration().Days;
        }
    }
    public int 今年 { get { return DateTime.Now.DayOfYear - 1; } }
    public int 最近一年
    {
        get
        {
            DateTime today = DateTime.Now;
            TimeSpan span = (new DateTime(today.Year, 12, 31) - new DateTime(today.Year, 1, 1));
            return span.Duration().Days - 1;
        }
    }

    public clsPeriods() { } // Constructor

    bool IListSource.ContainsListCollection
    {
        get { return false; }
    }

    IList IListSource.GetList()
    {
        BindingList<pair> list = new BindingList<pair>();
        PropertyInfo[] pInfos = typeof(clsPeriods).GetProperties();
        foreach (PropertyInfo info in pInfos)
        {
            string text = info.Name;
            clsPeriodscls = new clsPeriods();
            object obj = info.GetValue(cls, null);
            int value = 0;
            if (int.TryParse(obj.ToString(), out value))
                list.Add(new pair(text, value));
        }
        return (IList) list;
    }
}

public class pair
{
    string _Key;
    int _Value;
    public string Key
    {
        get { return _Key; }
        set { _Key = value; }
    }
    public int Value
    {
        get { return _Value; }
        set { _Value = value; }
    }
    public pair() { }
    public pair(string k, int v) {
        Key = k;
        Value = v;
    }
}

在上例中, 我必須另外建立名為 pair 的自訂類別, 它只是一個單純用來存放 Key/Value Pair 的容器而已。在 VS2008/.Net Framework 3.5 中, 這種類別的寫法可以再簡化一點:

public class pair
{
    public string Key { set; get; } // Only on VS2008
    public int Value { set; get; } // Only on VS2008
    public pair() { }
    public pair(string k, int v) {
        Key = k;
        Value = v;
    }
}

此外, 為了在執行時期取出設計時期加入的類別資訊 (在這裡指的是類別的 Properties 項目資訊), 所以我們必須用到 Reflection 命名空間下的方法, 所以請記得加上 using System.Reflection; 指令。同時, 為了實作 IListSource, 我們也必須引用 ComponentModel 命名空間。

實作 IListSource 介面時, 你必須同時實作 ContainsListCollection 這個 Property, 並實作 GetList() 方法以傳回一個 IList 實體, 如範例中所示。

現在, 我們已經能將這個自訂類別當作繫結控制項的資料來源了:

clsPeriods cls = new clsPeriods();
ddlPeriods.DataSource = cls;
ddlPeriods.DataTextField = "Key";
ddlPeriods.DataValueField = "Value";

ddlPeriods.DataBind();

透過這種方式, 我們就能夠使用自訂類別以取代 Enum 以提供可以動態變更內容的繫結來源。

 

更簡單的做法

 

在上面的範例中, 為了提供像 DropDownList 之類具有 TextField 與 ValueField 的控制項以作為資料來源, 我們又另外製作了稱為 pair 的自訂型別。從好的方向想, 這種做法提供了很大的彈性, 你可以以這個模型為基礎, 繼續發展更多的應用。不過話說回來, 我原來只是想稍為增強 Enum 功能而已, 我並不想因為這個目的, 而需要特別去維護一個型別。我難道不能簡單的使用 .Net 已經提供的既有型別嗎?

.Net 有很多很好用的集合型別可以使用, 如果你不想自己創建一個自訂型別的話, 我們可以從現有的 .Net 型別中挑選一個來使用。以下是使用 SortedList 型別的範例:

using System.Reflection;
...

public class clsPeriods
{
    public int 今天 { get { return 0; } }
    public int 今天跟昨天 { get { return 1; } }
    public int 最近三天 { get { return 2; } }
    public int 這個禮拜 { get { return (int)DateTime.Now.DayOfWeek; } }
    public int 最近兩個禮拜 { get { return 這個禮拜 + 7; } }
    public int 最近三個禮拜 { get { return 這個禮拜 + 14; } }
    public int 最近四個禮拜 { get { return 這個禮拜 + 21; } }
    public int 這個月 { get { return DateTime.Now.Day - 1; } }
    public int 最近兩個月
    {
        get
        {
            DateTime today = DateTime.Now;
            TimeSpan span = today - new DateTime(today.Year, today.AddMonths(-1).Month, 1);
            return span.Duration().Days - 1;
        }
    }
    public int 最近半年
    {
        get
        {
            DateTime today = DateTime.Now;
            TimeSpan span = today - new DateTime(today.Year, today.AddMonths(-6).Month, 1);
            return span.Duration().Days;
        }
    }
    public int 今年 { get { return DateTime.Now.DayOfYear - 1; } }
    public int 最近一年
    {
        get
        {
            DateTime today = DateTime.Now;
            TimeSpan span = (new DateTime(today.Year, 12, 31) - new DateTime(today.Year, 1, 1));
            return span.Duration().Days - 1;
        }
    }
}

public SortedList slPeriods
{
    get
    {
        SortedList list = new SortedList();
        PropertyInfo[] pInfos = typeof(clsPeriodForQuery).GetProperties();
        foreach (PropertyInfo info in pInfos)
        {
            string text = info.Name;
            clsPeriodForQuery cls = new clsPeriodForQuery();
            object value = info.GetValue(cls, null);
            list.Add(value, text);
        }
        return list;
    }

在上面範例中, 我只是很簡單的使用 Reflection 命名空間下的功能, 把 clsPeriods 的 Properties 擷取出來並填入一個 SortedList 物件並回傳而已。由於使用的是 SortedList 物件, 我們取出列舉數值的時候, 它都是自動排列的。所以這個範例和上個範例不同, 我們取到的值都是按天數多寡排過順序的。同時, 由於我在塞入 SortedList 的 Text 與 Value 欄位對調過了, 所以它被用作資料繫結控制項的資料來源時, DropDownList 的欄位也必須對調:

ddlPeriods.DataSource = slPeriods;
ddlPeriods.DataTextField = "Value";
ddlPeriods.DataValueField = "Key";

ddlPeriods.DataBind(); 

如果你不希望排序的話, 你可以把 SortedList 型別改成 HashTable 型別, 它就不會排序了。

總結

以上兩個範例都用在 TextField 與 ValueField 不一樣的場合。如果你不需要這樣做, 你可以無需自訂 pair 型別, 使用字串即可。.Net 就是這樣, 對於同樣一種需求, 你可能可以使用兩百種做法來實現, 就看你最熟悉哪種做法, 或是哪種做法最有效率。

一般而言, 寫死在程式裡面的東西效率通常最好, 但是也最沒有彈性; 如果可以寫進資料庫, 那麼它的效率最低, 但是很容易修改。在本文範例中, 其實你也可以把列表由資料庫直接產生(使用 Stored Procedure 來撰寫程式邏輯), 然後使用 ADO.NET 工具讀取出來並填入容器物件。但如此一來, 如果資料繫結動作很頻繁, 難免會影響到效能。

折衷之道, 就是如範例般寫在自訂類別裡面。這個自訂類別可以放在外部的類別庫(Class Library)專案裡面, 如果有更改, 也不會導致網站專案需要重新編譯, 如此彈性也會比較大。


Dev 2Share @ 點部落