WPF 中绑定和使用友好枚举






4.97/5 (73投票s)
在 WPF 中绑定和使用友好枚举。
引言
作为 .NET 开发者,我们知道并且可能已经使用过 `Enum`。对于那些之前没有使用过 `enum` 的人,MSDN 对它们的介绍是这样的:
"enum 关键字用于声明一个枚举,这是一种包含一组命名常数(称为枚举数列表)的独立类型。每个枚举类型都有一个基础类型,可以是除 char 之外的任何整型。"
所以当我们使用 `enum` 时,实际上我们可以做一些像这样的事情:
enum Hardware {DiskDrive=1, Keyboard, GraphicsCard, Monitor};
这都没问题,但是想象一下,如果我们想在一个列表中显示 `enum` 的列表,并且我们希望有更具描述性的值,比如更友好的名称,但仍然在需要时保留选定的基础 `enum` 值。
本文将展示如何使用 WPF 完成以下操作:
- 绑定到 `enum` 值的枚举
- 为 `enum` 显示友好的名称,以改善用户体验
绑定到枚举值的枚举
我们可能想做的第一件事是显示所有可能的 `enum` 值列表,以便可以在此列表中选择当前值,或者允许用户选择一个新的 `enum` 值。使用以下技术很容易实现这一点:
<ObjectDataProvider x:Key="foodData"
MethodName="GetValues"
ObjectType="{x:Type sys:Enum}">
<ObjectDataProvider.MethodParameters>
<x:Type TypeName="local:FoodTypes" />
</ObjectDataProvider.MethodParameters>
</ObjectDataProvider>
然后我们可以像这样在 XAML 中使用它进行绑定:
<ComboBox x:Name="cmbFoodType"
ItemsSource="{Binding Source={StaticResource foodData}}"
....
....
</ComboBox>
在这里,我声明了一个演示 `enum`(如果您能暂时忽略 `LocalizableDescriptionAttribute`,稍后会详细介绍):
public enum FoodTypes : int
{
Pizza = 1,
Burger = 2,
SpagBol = 3
}
这将产生以下结果:

为枚举显示友好的名称,以改善用户体验
现在这只完成了工作的一半,但从用户的角度来看,一些更具描述性的文本实际上可以改善用户体验,那么我们可以为此做些什么呢?好消息是,反射和特性提供了答案。我们可以用一个特殊的特性来装饰我们的 `enum`,即一个派生的 `LocalizableDescriptionAttribute`,它继承自 `DescriptionAttribute`,可以如下使用:
public enum FoodTypes : int
{
[LocalizableDescription(@"Pizza", typeof(Resource))]
Pizza = 1,
[LocalizableDescription(@"Burger", typeof(Resource))]
Burger = 2,
[LocalizableDescription(@"SpagBol", typeof(Resource))]
SpagBol = 3
}
我应该指出,本文的原始内容使用了 `EnumMember`,正如一些读者指出的那样,它无法针对不同文化进行本地化。幸运的是,其中一位读者是才华横溢的 Uwe Keim,他给了我一些处理 `enum` 本地化的代码。以下是 `LocalizableDescriptionAttribute` 的代码。谢谢 Uwe。
using System;
using System.Resources;
using System.Reflection;
using System.Globalization;
using System.ComponentModel;
namespace FriendlyEnumValues
{
/// <summary>
/// Attribute for localization.
/// </summary>
[AttributeUsage(AttributeTargets.All,Inherited = false,AllowMultiple = true)]
public sealed class LocalizableDescriptionAttribute : DescriptionAttribute
{
#region Public methods.
// ------------------------------------------------------------------
/// <summary>
/// Initializes a new instance of the
/// <see cref="LocalizableDescriptionAttribute"/> class.
/// </summary>
/// <param name="description">The description.</param>
/// <param name="resourcesType">Type of the resources.</param>
public LocalizableDescriptionAttribute
(string description,Type resourcesType) : base(description)
{
_resourcesType = resourcesType;
}
#endregion
#region Public properties.
/// <summary>
/// Get the string value from the resources.
/// </summary>
/// <value></value>
/// <returns>The description stored in this attribute.</returns>
public override string Description
{
get
{
if (!_isLocalized)
{
ResourceManager resMan =
_resourcesType.InvokeMember(
@"ResourceManager",
BindingFlags.GetProperty | BindingFlags.Static |
BindingFlags.Public | BindingFlags.NonPublic,
null,
null,
new object[] { }) as ResourceManager;
CultureInfo culture =
_resourcesType.InvokeMember(
@"Culture",
BindingFlags.GetProperty | BindingFlags.Static |
BindingFlags.Public | BindingFlags.NonPublic,
null,
null,
new object[] { }) as CultureInfo;
_isLocalized = true;
if (resMan != null)
{
DescriptionValue =
resMan.GetString(DescriptionValue, culture);
}
}
return DescriptionValue;
}
}
#endregion
#region Private variables.
private readonly Type _resourcesType;
private bool _isLocalized;
#endregion
}
}
这里的基本思想是,这个 `LocalizableDescriptionAttribute` 允许你传入一个键和一个资源类型来查找,所以键值将索引到资源文件并获取资源文件的值。这在附加的演示代码中包含的小资源文件中有所展示。

既然我们知道 `enum` 可以这样做,那么如何在 XAML 中将其用作 `ComboBox` 呢?嗯,幸运的是,还有另一个 WPF 技巧可以提供帮助,那就是 `IValueConverter`。让我们看看修改后的 XAML:
<Window.Resources>
<local:EnumToFriendlyNameConverter x:Key="enumItemsConverter"/>
</Window.Resources>
<StackPanel>
<!-- Enum Combobox picker -->
<StackPanel Orientation="Vertical" Margin="2" Grid.Row="0" Grid.Column="0" >
<Label Height="Auto" Content="Food Types"/>
<ComboBox x:Name="cmbFoodType"
ItemsSource="{Binding Source={StaticResource foodData}}"
....
....
<ComboBox.ItemTemplate>
<DataTemplate>
<Label Content="{Binding Path=.,Mode=OneWay,
Converter={StaticResource enumItemsConverter}}"
Height="Auto"
Margin="0"
VerticalAlignment="Center"/>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</StackPanel>
</StackPanel>
其中 `EnumToFriendlyNameConverter` 如下所示:
/// <summary>
using System;
using System.Windows.Data;
using System.Globalization;
using System.Reflection;
using System.Runtime.Serialization;
namespace FriendlyEnumValues
{
/// <summary>
/// This class simply takes an enum and uses some reflection to obtain
/// the friendly name for the enum. Where the friendlier name is
/// obtained using the LocalizableDescriptionAttribute, which holds the localized
/// value read from the resource file for the enum
/// </summary>
[ValueConversion(typeof(object), typeof(String))]
public class EnumToFriendlyNameConverter : IValueConverter
{
#region IValueConverter implementation
/// <summary>
/// Convert value for binding from source object
/// </summary>
public object Convert(object value, Type targetType,
object parameter, CultureInfo culture)
{
// To get around the stupid WPF designer bug
if (value != null)
{
FieldInfo fi = value.GetType().GetField(value.ToString());
// To get around the stupid WPF designer bug
if (fi != null)
{
var attributes =
(LocalizableDescriptionAttribute[])
fi.GetCustomAttributes(typeof
(LocalizableDescriptionAttribute), false);
return ((attributes.Length > 0) &&
(!String.IsNullOrEmpty(attributes[0].Description)))
?
attributes[0].Description
: value.ToString();
}
}
return string.Empty;
}
/// <summary>
/// ConvertBack value from binding back to source object
/// </summary>
public object ConvertBack(object value, Type targetType,
object parameter, CultureInfo culture)
{
throw new Exception("Cant convert back");
}
#endregion
}
}
实际的魔法是通过使用一些反射来实现的。所以如果你需要在 XBAP 中运行它,你需要确保它在 `FullTrust` 模式下运行。
拼图的最后一步是确保选定的值能够传回可能使用 `enum` 值之一的源对象。我正在使用一个简单的测试设置,包括一个 `ViewModel` 和一个测试类。从附加的演示代码中应该很清楚。
总之,确保测试类接收实际 `enum` 值而不是它不知道如何处理的友好名称的部分,仅仅是 XAML 中更多的数据绑定。如下所示:
<ComboBox x:Name="cmbFoodType"
ItemsSource="{Binding Source={StaticResource foodData}}"
SelectedItem="{Binding Path=TestableClass.FoodType, Mode=TwoWay}" Height="Auto">
<ComboBox.ItemTemplate>
<DataTemplate>
<Label Content="{Binding Path=.,Mode=OneWay,
Converter={StaticResource enumItemsConverter}}"
Height="Auto"
Margin="0"
VerticalAlignment="Center"/>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
这里现在包括了一个到 `SelectedItem` 的绑定,这是一个到测试类中实际 `enum` 值的 `TwoWay` 绑定。
所以把所有东西放在一起,我们现在有一个绑定的 `ComboBox`,它向用户显示友好的值,但为选定项在绑定对象中维护正确的 `enum` 值。

这是测试类的选定值,请注意,它是正确的 `enum` 值。

我认为这在一定程度上改善了用户体验,希望它能帮助到你,正如它帮助了我一样。
替代方法
自从我写了这篇文章以来,传奇人物 Andrew Smith(Infragistics……(连 Josh Smith 都称他为大师))给我发了一封电子邮件,其中提供了一种替代方法,他创建了一个 `MarkupExtension` 来实现与此相同的功能,所以你可能想在他的博客上看看。该帖子可以通过链接访问:http://agsmith.wordpress.com/2008/09/19/accessing-enum-members-in-xaml/,谢谢 Andrew。