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);
Allowed allowedState = color.IsAllowed;
Console.WriteLine(allowedState);
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.