65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.98/5 (56投票s)

2011年3月16日

CPOL

5分钟阅读

viewsIcon

164331

downloadIcon

1044

本文介绍了一个非常轻量级的枚举扩展库,它利用 .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 也是一个包含的扩展类,它允许您添加自定义的可绑定属性,在此示例中,我添加了一个名为 InStockbool 类型属性。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,通过反射提取属性,并通过重写 TryGetMemberTrySetMemberGetDynamicMemberNames 方法将它们暴露给动态感知的调用站点。它还缓存扩展包装器,以便每个原始 enum 类型最多只有一个包装器实例。请注意,由于字典只会在构造函数中调用,因此无需锁定对其的写访问。因此,此类(在大多数常见用途中)是线程安全的。

如果您能给我一些反馈和批评,我将非常高兴,另外,请随意给我一些非凡的赞美!*笑*

历史

  • 2011 年 3 月 15 日 - 文章和源代码首次发布。
© . All rights reserved.