問題緣起
WPF的分層結構為編程帶來了極大便利,XAML綁定是其最主要的特征。在使用綁定的過程中,大家都普遍的發現枚舉成員的綁定是個問題。一般來說,枚舉綁定多出現於與ComboBox配合的情況,此時我們希望實現的目標有:
- 建立選擇項與ItemsSource的對應關系;
- 自動獲取用於ItemsSource的枚舉源;
- 自定義下拉框中顯示的內容。
對於目標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。
