在WPF中使用變通方法實現枚舉類型的XAML綁定


問題緣起

WPF的分層結構為編程帶來了極大便利,XAML綁定是其最主要的特征。在使用綁定的過程中,大家都普遍的發現枚舉成員的綁定是個問題。一般來說,枚舉綁定多出現於與ComboBox配合的情況,此時我們希望實現的目標有:

  1. 建立選擇項與ItemsSource的對應關系;
  2. 自動獲取用於ItemsSource的枚舉源;
  3. 自定義下拉框中顯示的內容。

對於目標1,考慮最簡單的模式,即枚舉的定義采用從0開始的連續整數,可以使用IValueConverter接口來實現從枚舉到整型的雙向轉換,以使得枚舉成員綁定到SelectedIndex上。

有些朋友提出使用靜態ObjectDataProvider資源作為ItemsSource的來源,這種方式可實現枚舉成員的直接綁定,不需要值轉換,其缺點是對於每一個枚舉類型都要添加一個提供者,當項目較大、枚舉類型多時使用起來很不方便。

考慮到枚舉的比較是值類型比較,Broculos想到了比較聰明的方法同時實現了目標1和目標2:定義一個返回枚舉兄弟成員的標記拓展(MarkupExtension)。使用時向標記拓展中提供枚舉類型即可。相比於上一種方法,該方法更加簡單明了,只是當在XAML中提供枚舉類型時,需要引用其命名空間,而XAML中的命名空間引用缺少自動完成機制,有時需要搜索一番。

當然,上面所有解答都沒有實現目標3。網友ding.li使用代碼方式對下拉框內容進行設置,雖然實現了目標3,但違背了目標2。

Mgen通過定義一個提供附加屬性的類實現了所有3個目標。在Selector要素中,設置EnumSelector.EnumType和EnumSelector.BindingPath附加屬性來指定ItemsSource和SelectedItem,而非直接設置要素的相應屬性。該方法實際上發展了標記拓展的思路,為實現目標3而進行了較復雜的設計。這是非常好的實現方案,缺點是違反直觀感受。

變通方案

本文介紹使用封裝枚舉類型的方法同時實現3個願望。主要思路是,在XAML中盡可能少的寫入代碼,通過上下文綁定直接設置下拉框的ItemsSource,SelectedItem,以及顯示內容。

注意到DisplayMemberPath和Binding拓展的Path屬性,要同時由上下文提供多個屬性用來綁定,分別是:作為ItemsSource源的集合、作為SelectedItem的實例、及作為“DisplayMember”的字符串。顯然直接使用枚舉實例無法提供所需屬性成員,因此設計封裝類型並在其中定義成員如下:

public class EnumShell<T>
{
    public T Instance { get; }
    public string Description {get;}
    public EnumShell[] Brothers {get;}
}

這樣在XAML中的下拉框代碼綁定可寫為:

<ComboBox ItemsSource="{Binding Path=Brothers}" SelectedItem="{Binding}" DisplayMemberPath="Description"/>

可以通過EnumShell實例上下文獲取所有必要信息。

下面簡述該類型的實現。

泛型類EnumShell<T>的構造函數接受類型T的參數,該參數是特定枚舉類型的實例,因此T即為某個枚舉類型。該參數由Instance屬性保存,並從枚舉類型獲取到所有可用的枚舉值,均封裝為EnumShell<T>實例,並作為數組由Brothers屬性公開。Description屬性獲取枚舉值定義的DescriptionAttribute特定指定的文本,作為顯示的內容。

考慮到ComboBox的項比較實際是EnumShell<T>實例的比較,因此不能單純的使用其構造函數來得到Brothers集合,解決方法是定義一個靜態的字典用於保存EnumShell<T>實例,使得通過一個枚舉值永遠得到唯一的一個EnumShell<T>實例。

完整的代碼如下所示:

    public abstract class EnumShell
    {
        static Dictionary<string, EnumShell> pool = new Dictionary<string, EnumShell>();

        public static EnumShell<T> GetShell<T>(T Instance)
        {
            var key = typeof(T).ToString() + Instance.ToString();
            if (pool.ContainsKey(key))
            {
                return (EnumShell<T>)pool[key];
            }
            else
            {
                var nsh = new EnumShell<T>(Instance);
                pool.Add(key, nsh);
                return nsh;
            }
        }
    }

    public class EnumShell<T> : EnumShell
    {
        internal EnumShell(T Instance)
        {
            this.Instance = Instance;
        }

        public T Instance { get; set; }

        public Type EnumType { get { return typeof(T); } }

        public string Description
        {
            get
            {
                string strValue = Name;
                FieldInfo fieldinfo = Instance.GetType().GetField(strValue);
                Object[] objs = fieldinfo.GetCustomAttributes(typeof(DescriptionAttribute), false);
                if (objs == null || objs.Length == 0)
                {
                    return strValue;
                }
                else
                {
                    DescriptionAttribute da = (DescriptionAttribute)objs[0];
                    return da.Description;
                }
            }
        }

        public string Name { get { return Instance.ToString(); } }
        public string FullName { get { return EnumType.ToString() + Name; } }

        public T[] BrotherInstances { get { return (T[])Enum.GetValues(this.EnumType); } }

        public EnumShell<T>[] Brothers { get {
            return BrotherInstances.Select(i => EnumShell.GetShell(i)).ToArray();
        } }
    }

定義抽象的EnumShell類型作為基類型,可以在定義實體類型時不指明泛型類型參數,由此支持任意枚舉類型的取值;定義靜態的GetShell泛型函數,將新的EnumShell實例注冊添加到全局字典;將EnumShell<T>構造函數的可見性進行限制,避免了自行實例化導致字典中沒有注冊的情況;EnumShell類公開了其他屬性成員,以方便各種XAML綁定的情況。

使用時,將實體類型中的原枚舉屬性替換為EnumShell或EnumShell<T>屬性,並使用EnumShell.GetShell進行實例化賦值。如,有枚舉定義:

public enum Cup
{
    [Description("very nice")]
    A,
    [Description("nice")]
    B,
    [Description("another kind of nice")]
    C
}

定義某實體類型,用EnumShell表示該枚舉:

public class SecretWeapon
{
    public SecretWeapon(){
        this.Cup = EnumShell.GetShell<Cup>(Cup.A);
    }

    public EnumShell<Cup> Cup { get; set; }
}

這樣定義后即可使用,當需要執行switch分支時,可用Instance屬性獲取被封裝的真正的枚舉值。

結語

本文介紹的封裝方法實現枚舉綁定,適用於自定義實體類型的情況,對於直接使用第三方實體類型的情況,則無法直接使用,必須對實體類型本身進行再次封裝,而這大大降低了便利性。

其實在早些時候有傳言說C# 5.0會支持拓展屬性,試想這要是真的,對枚舉進行綁定將是輕而易舉的事情,真的希望C#能夠實現這一功能。

本文中的EnumShell類型在Heroius.Extension.WPF庫中包含。更多可免費使用的程序集引用,請使用Heroius的Nuget服務源:http://heroius.com:8686/app/nuget。詳細信息請訪問http://heroius.com:8686/app


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM