Click here to Skip to main content
11,641,708 members (57,458 online)
Click here to Skip to main content

Adding data-bindable attributes to C# enums using the dynamic runtime

, 16 Mar 2011 CPOL 76.5K 847 98
Rate this:
Please Sign up or sign in to vote.
This article is about a very light-weight enum extension library that takes advantage of dynamic types in .NET 4.0 to provide a simple way to add meta attributes to an enum field.

Introduction

This article is about a very light-weight enum extension library that takes advantage of dynamic types in .NET 4.0 to provide a simple way to add meta attributes to an enum field. While the code and demo are primarily intended for WPF usage, you should be able to trivially use the same classes in Silverlight (reasonably sure it'll work) or Windows Forms (not as sure of this one), with the restriction that you have to use .NET 4.0 or higher.

Design Goals

  • It should be effortless to use the extensions with large existing code bases that have several enum types.
  • WPF data-binding should work on these custom attributes, with customizable bindable properties.
  • Existing properties/fields should require minimal or no code changes.
  • Trouble-free access to the underlying enum (meaning I didn't want to hide the enum with a dominant wrapper class).
  • Thin interfaces to facilitate custom extension attributes.
  • Minimal wrapper overhead - there should not be be multiple wrapper instances for the same enum type.

I hope I have achieved all these design goals in my implementation. I'll quickly talk about how this library can be used and then jump into the implementation details.

Code usage

Consider the following enum class to which I have added some custom attributes.

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 is an attribute that allows you to specify a custom display name to an enum field (and I know there are dozens of such implementations already available). Since it's such a common requirement, this extension comes with the library (only a few lines of code anyway). StandardExtendedEnum is also an included extension class that lets you add a custom data-bindable property, and in this example I've added a property called InStock of type bool. InterestingItemImage is a custom extension class specific to the demo project which allows you to associate an image with an enum field. Here's how the code for InterestingItemImage has been implemented and I'll discuss the details in the next section although I think the code is self explanatory (which is always an important design goal).

[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));
    }
}

In this example I've implemented the INamedExtendedEnumAttribute interface although if I did not want to provide a custom property name, I could have just implemented the IExtendedEnumAttribute interface (just a single GetValue method). Now here's how I set up the bindable properties in my datacontext class (which in my simple example also happens to be the view class).

public MainWindow()
{
  InitializeComponent();

  this.InterestingItemsList = Enum.GetValues(
      typeof(InterestingItems)).Cast<InterestingItems>().Select(
          x => (ExtendedEnum<InterestingItems>)x);
}

public object InterestingItemsList { get; private set; }

I get the enumeration values using Enum.GetValues and then convert that into an ExtendedEnum<> collection using LINQ Select and an explicit cast. Now here's a SelectedItem property that will fetch the selected enum field back from the collection that's bound to some WPF control (in the demo, a listbox).

private InterestingItems selectedItem = InterestingItems.Plus;

public ExtendedEnum<InterestingItems> SelectedInterestingItem
{
    get
    {
        return this.selectedItem;
    }

    set
    {
        if (this.selectedItem != value)
        {
            this.selectedItem = value;
            this.FirePropertyChanged("SelectedInterestingItem");
        }
    }
}

One very important thing there is that the backing field is of the original enum type. There are implicit conversions in place (two way) so you can convert between an enum and the extension class transparently. And now take a look at the XAML.

Example 1

<ListBox Width="200" Height="100" Grid.Row="1" Grid.Column="0"
       ItemsSource="{Binding InterestingItemsList}" 
       SelectedItem="{Binding SelectedInterestingItem, Mode=TwoWay}" />

This is one of the simplest examples and the only extra functionality you get here is that this respects the display name attributes (when they are provided). This may probably be one of the most common uses of this library.

Example 2

<ListBox Width="200" Height="100" Grid.Row="1" Grid.Column="1"
   ItemsSource="{Binding InterestingItemsList}" 
   DisplayMemberPath="EnumDisplayName"
   SelectedItem="{Binding SelectedInterestingItem, Mode=TwoWay}" />

Woah! What happened there? In the previous example, the display name was fetched when one was provided and where one was not, it used the default enum name. That's because the previous example relies on the default ToString implementation. But in the above example, I have explicitly specified the DisplayMemberPath property. This means I am telling WPF to specifically look for that property (in our case it'll be an attribute), but in the demo enumeration, two fields do not have display name attributes and so they'll show up blank. This is just one of those gotchas that you need to be careful about. It's easily circumvented by taking advantage of the ToString implementation, so it's unlikely to be a show stopper.

Example 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>

I've bound to all three custom attributes in this example, and for the display attribute, I relied on ToString by binding to the whole object which is why you don't see any blank entries there.

Direct usage in code

Here's an example showing how to directly access and use dynamic attributes in code.

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));
    }

As you can see, the dynamic property can itself be an enum which itself has dynamic properties. So yeah, there's no end to the madness here!

Implementation details

At the core of the class are the two interfaces that the extension attributes need to implement, and the most basic (and mandatory) interface is IExtendedEnumAttribute.

public interface IExtendedEnumAttribute
{
    object GetValue();
}

This interfaces lets you provide the value associated with a custom attribute property. The dynamic property name will default to the name of the attribute (minus the Attribute suffix). If you want to customize the name too, then use the INamedExtendedEnumAttribute interface.

public interface INamedExtendedEnumAttribute : IExtendedEnumAttribute
{
    string GetPropertyName();
}

I've included two implementations in the library, the first one is the most basic EnumDisplayNameAttribute class.

[AttributeUsage(AttributeTargets.Field)]
public class EnumDisplayNameAttribute 
     : DisplayNameAttribute, IExtendedEnumAttribute
{
    public EnumDisplayNameAttribute()
    {
    }

    public EnumDisplayNameAttribute(string displayName)
        : base(displayName)
    {
    }

    public object GetValue()
    {
        return this.DisplayName;
    }
}

And then there's the flexible and yet mostly simple StandardExtendedEnumAttribute which lets you quickly add attribute property values without having to implement a full class for it (although in scenarios where you need custom processing, as in the image class I showed above, you will have to write a custom attribute class).

[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;
    }

And finally the enum extension class itself.

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;
  }
}

The class derives from DynamicObject, extracts the attributes via reflection, and exposes them to dynamic-aware call-sites by overriding the TryGetMember, TrySetMember, and GetDynamicMemberNames methods. It also caches the extended wrappers so that you will always have at maximum one wrapper instance per original enum type. Notice how there's no need to lock write-access to the dictionary since it'll only be called from the constructor. So this class is thread safe (for most common purposes).

I will be delighted if you could give me some feedback and criticism, and by the way feel free to shower extraordinary compliments on me! *grin*

History

  • March 15, 2011 - Article and source code first published.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Nish Nishant
United States United States
Nish Nishant is a Software Architect/Consultant based out of Columbus, Ohio. He has over 15 years of software industry experience in various roles including Lead Software Architect, Principal Software Engineer, and Product Manager. Nish is a recipient of the annual Microsoft Visual C++ MVP Award since 2002 (13 consecutive awards as of 2014).

Nish is an industry acknowledged expert in the Microsoft technology stack. He authored
C++/CLI in Action for Manning Publications in 2005, and had previously co-authored
Extending MFC Applications with the .NET Framework for Addison Wesley in 2003. In addition, he has over 140 published technology articles on CodeProject.com and another 250+ blog articles on his
WordPress blog. Nish is vastly experienced in team management, mentoring teams, and directing all stages of software development.

Contact Nish : You can reach Nish on his google email id voidnish.

Website and Blog

You may also be interested in...

Comments and Discussions

 
GeneralMy vote of 5 Pin
Chris Maunder18-Jun-15 6:27
adminChris Maunder18-Jun-15 6:27 
GeneralRe: My vote of 5 Pin
Nish Nishant21-Jul-15 5:20
sitebuilderNish Nishant21-Jul-15 5:20 
GeneralMy vote of 5 Pin
bambuz26-Aug-11 3:24
memberbambuz26-Aug-11 3:24 
GeneralMy vote of 5 Pin
HimanshuJoshi19-Apr-11 14:41
memberHimanshuJoshi19-Apr-11 14:41 
GeneralRe: My vote of 5 Pin
Nishant Sivakumar9-May-11 15:10
mvpNishant Sivakumar9-May-11 15:10 
GeneralGood article Pin
CIDev11-Apr-11 9:17
memberCIDev11-Apr-11 9:17 
GeneralRe: Good article Pin
Nishant Sivakumar11-Apr-11 9:21
mvpNishant Sivakumar11-Apr-11 9:21 
GeneralMy vote of 5 Pin
Abhinav S9-Apr-11 4:04
mvpAbhinav S9-Apr-11 4:04 
GeneralRe: My vote of 5 Pin
Nishant Sivakumar9-Apr-11 6:15
mvpNishant Sivakumar9-Apr-11 6:15 
GeneralMy vote of 5 Pin
Sunasara Imdadhusen8-Apr-11 20:42
memberSunasara Imdadhusen8-Apr-11 20:42 
GeneralRe: My vote of 5 Pin
Nishant Sivakumar9-Apr-11 2:20
mvpNishant Sivakumar9-Apr-11 2:20 
GeneralNeat idea Nish Pin
Sacha Barber28-Mar-11 0:06
mvpSacha Barber28-Mar-11 0:06 
GeneralRe: Neat idea Nish Pin
Nishant Sivakumar28-Mar-11 1:10
mvpNishant Sivakumar28-Mar-11 1:10 
GeneralMy vote of 5 Pin
Mika Wendelius18-Mar-11 9:40
memberMika Wendelius18-Mar-11 9:40 
GeneralRe: My vote of 5 Pin
Nishant Sivakumar18-Mar-11 10:36
mvpNishant Sivakumar18-Mar-11 10:36 
GeneralMy vote of 5 Pin
Jani Giannoudis18-Mar-11 6:44
memberJani Giannoudis18-Mar-11 6:44 
GeneralRe: My vote of 5 Pin
Nishant Sivakumar18-Mar-11 7:54
mvpNishant Sivakumar18-Mar-11 7:54 
GeneralMy Vote of 5 Pin
RaviRanjankr18-Mar-11 3:41
memberRaviRanjankr18-Mar-11 3:41 
GeneralRe: My Vote of 5 Pin
Nishant Sivakumar18-Mar-11 3:42
mvpNishant Sivakumar18-Mar-11 3:42 
QuestionLocalization Pin
Jani Giannoudis17-Mar-11 12:36
memberJani Giannoudis17-Mar-11 12:36 
AnswerRe: Localization Pin
Nishant Sivakumar17-Mar-11 17:09
mvpNishant Sivakumar17-Mar-11 17:09 
GeneralMy vote of 5 Pin
quentininsa17-Mar-11 10:14
memberquentininsa17-Mar-11 10:14 
GeneralRe: My vote of 5 Pin
Nishant Sivakumar17-Mar-11 10:28
mvpNishant Sivakumar17-Mar-11 10:28 
GeneralMy vote of 5 Pin
Petr Pechovic17-Mar-11 3:45
memberPetr Pechovic17-Mar-11 3:45 
GeneralRe: My vote of 5 Pin
Nishant Sivakumar17-Mar-11 3:48
mvpNishant Sivakumar17-Mar-11 3:48 
GeneralMy vote of 5 Pin
Marcelo Ricardo de Oliveira17-Mar-11 3:42
mvpMarcelo Ricardo de Oliveira17-Mar-11 3:42 
GeneralRe: My vote of 5 Pin
Nishant Sivakumar17-Mar-11 3:43
mvpNishant Sivakumar17-Mar-11 3:43 
GeneralMy vote of 5 Pin
Brij17-Mar-11 3:11
mvpBrij17-Mar-11 3:11 
GeneralRe: My vote of 5 Pin
Nishant Sivakumar17-Mar-11 3:43
mvpNishant Sivakumar17-Mar-11 3:43 
GeneralNifty Idea Pin
Greg Russell17-Mar-11 0:53
memberGreg Russell17-Mar-11 0:53 
GeneralRe: Nifty Idea Pin
Nishant Sivakumar17-Mar-11 3:09
mvpNishant Sivakumar17-Mar-11 3:09 
GeneralMy vote of 5 Pin
Kunal_Chowdhury16-Mar-11 19:32
mvpKunal_Chowdhury16-Mar-11 19:32 
GeneralRe: My vote of 5 Pin
Nishant Sivakumar17-Mar-11 3:07
mvpNishant Sivakumar17-Mar-11 3:07 
GeneralMy vote of 5 Pin
thatraja16-Mar-11 16:53
mvpthatraja16-Mar-11 16:53 
GeneralRe: My vote of 5 Pin
Nishant Sivakumar17-Mar-11 3:07
mvpNishant Sivakumar17-Mar-11 3:07 
GeneralMy vote of 5 Pin
IGood16-Mar-11 10:39
memberIGood16-Mar-11 10:39 
GeneralRe: My vote of 5 Pin
Nishant Sivakumar16-Mar-11 10:52
mvpNishant Sivakumar16-Mar-11 10:52 
GeneralMy vote of 5 Pin
Marcus Kramer16-Mar-11 10:10
memberMarcus Kramer16-Mar-11 10:10 
GeneralRe: My vote of 5 Pin
Nishant Sivakumar16-Mar-11 10:51
mvpNishant Sivakumar16-Mar-11 10:51 
Generalreally inspired Pin
MDL=>Moshu16-Mar-11 10:05
memberMDL=>Moshu16-Mar-11 10:05 
GeneralRe: really inspired Pin
Nishant Sivakumar16-Mar-11 10:50
mvpNishant Sivakumar16-Mar-11 10:50 
GeneralNice. Very nice. Pin
Pete O'Hanlon16-Mar-11 9:46
mvpPete O'Hanlon16-Mar-11 9:46 
GeneralRe: Nice. Very nice. Pin
Nishant Sivakumar16-Mar-11 10:51
mvpNishant Sivakumar16-Mar-11 10:51 
GeneralMy vote of 5 Pin
Slacker00716-Mar-11 7:54
memberSlacker00716-Mar-11 7:54 
GeneralRe: My vote of 5 Pin
Nishant Sivakumar16-Mar-11 7:59
mvpNishant Sivakumar16-Mar-11 7:59 
GeneralCrap Pin
John Simmons / outlaw programmer16-Mar-11 4:25
mvpJohn Simmons / outlaw programmer16-Mar-11 4:25 
GeneralRe: Crap Pin
Nishant Sivakumar16-Mar-11 4:27
mvpNishant Sivakumar16-Mar-11 4:27 
GeneralRe: Crap Pin
thatraja16-Mar-11 16:52
mvpthatraja16-Mar-11 16:52 
GeneralRe: Crap Pin
Nishant Sivakumar17-Mar-11 3:47
mvpNishant Sivakumar17-Mar-11 3:47 
GeneralCongratulations Pin
Henrique C16-Mar-11 2:53
memberHenrique C16-Mar-11 2:53 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Terms of Use | Mobile
Web03 | 2.8.150731.1 | Last Updated 16 Mar 2011
Article Copyright 2011 by Nish Nishant
Everything else Copyright © CodeProject, 1999-2015
Layout: fixed | fluid