使用动态运行时为C#枚举添加数据绑定属性






4.98/5 (56投票s)
本文介绍了一个非常轻量级的枚举扩展库,它利用 .NET 4.0 中的动态类型,提供了一种简单的方法来为枚举字段添加元属性。
引言
本文介绍了一个非常轻量级的 enum
扩展库,它利用 .NET 4.0 中的动态类型,提供了一种简单的方法来为 enum
字段添加元属性。虽然代码和演示主要用于 WPF,但您应该可以非常轻松地在 Silverlight(相当确定会起作用)或 Windows Forms(对此不太确定)中使用相同的类,前提是您必须使用 .NET 4.0 或更高版本。
设计目标
- 对于拥有多个
enum
类型的现有大型代码库,使用这些扩展应该毫不费力。 - WPF 数据绑定应能处理这些自定义属性,并支持可自定义的绑定属性。
- 现有属性/字段应需要最少的代码更改,甚至无需更改。
- 无缝访问底层
enum
(这意味着我不希望用一个占主导地位的包装器类来隐藏enum
)。 - 轻量级接口,便于自定义扩展属性。
- 最小化的包装器开销——同一个
enum
类型不应有多个包装器实例。
我希望我在实现中达到了所有这些设计目标。我将快速介绍这个库的用法,然后深入探讨实现细节。
代码用法
考虑以下我添加了一些自定义属性的枚举类。
public enum InterestingItems
{
[InterestingItemImage("Images/Bell.png")]
[StandardExtendedEnum("InStock", false)]
Bell,
[InterestingItemImage("Images/Book.png")]
[StandardExtendedEnum("InStock", true)]
Book,
[EnumDisplayName("Plus sign")]
[InterestingItemImage("Images/Plus.png")]
[StandardExtendedEnum("InStock", false)]
Plus,
[EnumDisplayName("Stop Watch")]
[InterestingItemImage("Images/StopWatch.png")]
[StandardExtendedEnum("InStock", true)]
StopWatch
}
EnumDisplayName
是一个属性,允许您为枚举字段指定自定义显示名称(我知道有无数类似的实现)。由于这是一个非常常见的需求,所以此扩展已包含在库中(毕竟只有几行代码)。StandardExtendedEnum
也是一个包含的扩展类,它允许您添加自定义的可绑定属性,在此示例中,我添加了一个名为 InStock
的 bool
类型属性。InterestingItemImage
是一个特定于演示项目的自定义扩展类,它允许您将图像与 enum
字段关联起来。这是 InterestingItemImage
的实现代码,我将在下一节讨论细节,尽管我认为代码是不言自明的(这始终是一个重要的设计目标)。
[AttributeUsage(AttributeTargets.Field)]
public class InterestingItemImageAttribute
: Attribute, INamedExtendedEnumAttribute
{
private string path;
public InterestingItemImageAttribute(string path)
{
this.path = path;
}
public string GetPropertyName()
{
return "InterestingImage";
}
public object GetValue()
{
return new ImageSourceConverter().ConvertFromString(
String.Format(
"pack://application:,,,/EnumExtensionsDemoApp;component/{0}", path));
}
}
在此示例中,我实现了 INamedExtendedEnumAttribute
接口,但如果我不想提供自定义属性名称,我只需实现 IExtendedEnumAttribute
接口(只有一个 GetValue
方法)。现在,这是我在数据上下文类(在这个简单的示例中,它碰巧也是视图类)中设置绑定属性的方式。
public MainWindow()
{
InitializeComponent();
this.InterestingItemsList = Enum.GetValues(
typeof(InterestingItems)).Cast<InterestingItems>().Select(
x => (ExtendedEnum<InterestingItems>)x);
}
public object InterestingItemsList { get; private set; }
我使用 Enum.GetValues
获取枚举值,然后使用 LINQ 的 Select
和显式转换将其转换为 ExtendedEnum<>
集合。现在,这是一个 SelectedItem
属性,它将从绑定到某个 WPF 控件(在演示中是列表框)的集合中获取选定的 enum
字段。
private InterestingItems selectedItem = InterestingItems.Plus;
public ExtendedEnum<InterestingItems> SelectedInterestingItem
{
get
{
return this.selectedItem;
}
set
{
if (this.selectedItem != value)
{
this.selectedItem = value;
this.FirePropertyChanged("SelectedInterestingItem");
}
}
}
那里有一件非常重要的事情是,支持字段是原始的 enum
类型。存在隐式转换(双向),因此您可以透明地在 enum
和扩展类之间进行转换。现在看看 XAML。
示例 1
<ListBox Width="200" Height="100" Grid.Row="1" Grid.Column="0"
ItemsSource="{Binding InterestingItemsList}"
SelectedItem="{Binding SelectedInterestingItem, Mode=TwoWay}" />
这是最简单的示例之一,您在这里获得的唯一额外功能是它尊重显示名称属性(如果提供了)。这可能是该库最常见的用法之一。
示例 2
<ListBox Width="200" Height="100" Grid.Row="1" Grid.Column="1"
ItemsSource="{Binding InterestingItemsList}"
DisplayMemberPath="EnumDisplayName"
SelectedItem="{Binding SelectedInterestingItem, Mode=TwoWay}" />
哇!那里发生了什么?在前面的示例中,当提供了显示名称时就会获取它,而在未提供时,它会使用默认的 enum
名称。这是因为前面的示例依赖于默认的 ToString
实现。但在上面的示例中,我显式指定了 DisplayMemberPath
属性。这意味着我告诉 WPF 特别查找该属性(在我们的例子中是属性),但在演示枚举中,有两个字段没有显示名称属性,因此它们将显示为空白。这只是一个需要注意的“陷阱”。利用 ToString
实现可以轻松规避它,因此不太可能成为一个阻碍。
示例 3
<ListBox Width="450" Height="140" Grid.Row="2" Grid.ColumnSpan="2"
ItemsSource="{Binding InterestingItemsList}"
SelectedItem="{Binding SelectedInterestingItem, Mode=TwoWay}" >
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal" Margin="2">
<Image Source="{Binding InterestingImage}"
Stretch="UniformToFill" Width="24" Height="24"
Margin="0,0,10,0" />
<TextBlock VerticalAlignment="Center" Width="75"
Text="{Binding}" Foreground="Blue" FontSize="13" />
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<TextBlock Margin="0,0,3,0">In stock:</TextBlock>
<CheckBox VerticalAlignment="Center"
IsChecked="{Binding InStock}" />
</StackPanel>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
在此示例中,我绑定到了所有三个自定义属性,对于显示属性,我依赖于 ToString
,通过绑定到整个对象来实现,这就是为什么您看不到任何空白条目的原因。
直接在代码中使用
这是一个展示如何直接在代码中访问和使用动态属性的示例。
public enum Allowed
{
[EnumDisplayName("As Required")]
AsRequired,
Always,
Never,
}
public enum Color
{
[StandardExtendedEnum("IsAllowed", Allowed.AsRequired)]
Red,
[StandardExtendedEnum("IsAllowed", Allowed.Always)]
Green,
[StandardExtendedEnum("IsAllowed", Allowed.Never)]
Blue
}
class Program
{
static void Main(string[] args)
{
dynamic color = ExtendedEnum<Color>.GetValue(Color.Red);
// Dyamically access IsAllowed
Allowed allowedState = color.IsAllowed;
// Access Allowed enum normally
Console.WriteLine(allowedState); // AsRequired
// Access Allowed enum dynamically
// The following prints As Required
Console.WriteLine(ExtendedEnum<Allowed>.GetValue(allowedState));
}
正如您所看到的,动态属性本身可以是一个 enum
,而这个 enum
本身又具有动态属性。所以,是的,疯狂是没有止境的!
实现细节
该类的核心是扩展属性需要实现的两个接口,最基本(也是强制性的)接口是 IExtendedEnumAttribute
。
public interface IExtendedEnumAttribute
{
object GetValue();
}
这个接口允许您提供与自定义属性关联的值。动态属性名将默认为属性的名称(去掉“Attribute”后缀)。如果您也想自定义名称,那么请使用 INamedExtendedEnumAttribute
接口。
public interface INamedExtendedEnumAttribute : IExtendedEnumAttribute
{
string GetPropertyName();
}
我在库中包含了两个实现,第一个是最基本的 EnumDisplayNameAttribute
类。
[AttributeUsage(AttributeTargets.Field)]
public class EnumDisplayNameAttribute
: DisplayNameAttribute, IExtendedEnumAttribute
{
public EnumDisplayNameAttribute()
{
}
public EnumDisplayNameAttribute(string displayName)
: base(displayName)
{
}
public object GetValue()
{
return this.DisplayName;
}
}
然后是灵活且仍然非常简单的 StandardExtendedEnumAttribute
,它允许您快速添加属性值,而无需实现一个完整的类(尽管在需要自定义处理的情况下,如上面所示的图像类,您将必须编写一个自定义属性类)。
[AttributeUsage(AttributeTargets.Field)]
public class StandardExtendedEnumAttribute
: Attribute, INamedExtendedEnumAttribute
{
private string propertyName;
private object value;
public StandardExtendedEnumAttribute(string propertyName, object value)
{
this.propertyName = propertyName;
this.value = value;
}
public string GetPropertyName()
{
return propertyName;
}
public object GetValue()
{
return value;
}
最后是 enum
扩展类本身。
public class ExtendedEnum<T> : DynamicObject
{
private static Dictionary<T, ExtendedEnum<T>> enumMap =
new Dictionary<T, ExtendedEnum<T>>();
T enumValue;
private ExtendedEnum(T enumValue)
{
this.enumValue = enumValue;
ExtractAttributes();
}
public static ExtendedEnum<T> GetValue(Enum enumValue)
{
if (typeof(T) != enumValue.GetType())
{
throw new ArgumentException();
}
return GetValue((T)((object)enumValue));
}
private static ExtendedEnum<T> GetValue(T enumValue)
{
lock (enumMap)
{
ExtendedEnum<T> value;
if (!enumMap.TryGetValue(enumValue, out value))
{
value = enumMap[enumValue] = new ExtendedEnum<T>(enumValue);
}
return value;
}
}
private EnumDisplayNameAttribute enumDisplayNameAttribute;
private void ExtractAttributes()
{
var fieldInfo = typeof(T).GetField(enumValue.ToString());
if (fieldInfo != null)
{
foreach (IExtendedEnumAttribute attribute in
fieldInfo.GetCustomAttributes(typeof(IExtendedEnumAttribute), false))
{
string propertyName = attribute is INamedExtendedEnumAttribute ?
((INamedExtendedEnumAttribute)attribute).GetPropertyName() :
GetCleanAttributeName(attribute.GetType().Name);
properties[propertyName] = attribute.GetValue();
if (attribute is EnumDisplayNameAttribute)
{
enumDisplayNameAttribute = (EnumDisplayNameAttribute)attribute;
}
}
if (enumDisplayNameAttribute == null)
{
enumDisplayNameAttribute = new EnumDisplayNameAttribute(
((T)this).ToString());
}
}
}
private string GetCleanAttributeName(string name)
{
string nameLower = name.ToUpperInvariant();
return nameLower.EndsWith("ATTRIBUTE") ?
name.Remove(nameLower.LastIndexOf("ATTRIBUTE")) : name;
}
public static implicit operator T(ExtendedEnum<T> extendedEnum)
{
return extendedEnum.enumValue;
}
public static implicit operator ExtendedEnum<T>(Enum enumValue)
{
return GetValue(enumValue);
}
private Dictionary<string, object> properties = new Dictionary<string, object>();
public override bool TryGetMember(GetMemberBinder binder, out object result)
{
return properties.TryGetValue(binder.Name, out result);
}
public override bool TrySetMember(SetMemberBinder binder, object value)
{
return false;
}
public override IEnumerable<string> GetDynamicMemberNames()
{
return properties.Keys;
}
public override string ToString()
{
return enumDisplayNameAttribute.DisplayName;
}
}
该类继承自 DynamicObject
,通过反射提取属性,并通过重写 TryGetMember
、TrySetMember
和 GetDynamicMemberNames
方法将它们暴露给动态感知的调用站点。它还缓存扩展包装器,以便每个原始 enum
类型最多只有一个包装器实例。请注意,由于字典只会在构造函数中调用,因此无需锁定对其的写访问。因此,此类(在大多数常见用途中)是线程安全的。
如果您能给我一些反馈和批评,我将非常高兴,另外,请随意给我一些非凡的赞美!*笑*
历史
- 2011 年 3 月 15 日 - 文章和源代码首次发布。