When reading Steve Smith’s articles on enums on ASP.NET Alliance and his blog, I couldn’t help but think of a different approach to the issue. I’ve seen, used, and coded the enum class pattern before (and it works well), but we have new language features today to encourage reuse and empower true enums. I am going to show you techniques that won’t require significant changes to your code if you have an existing base of enums.
First, let’s start off with the Role enum Steven introduced:
public enum Role
{
Author = 1,
Editor = 2,
SalesRepresentative = 3,
Secret = 4,
AnotherSecret = 5,
Manager = 6
}
It’s a straightforward enum with its ordinal values defined. The original code was retrieving a list of names. I contend that this shouldn’t be limited to one particular enum. I created an EnumList class to turn any enum into an IEnumerable<T>.
public class EnumList<T> : IEnumerable<T> where T : struct
{
public EnumList()
{
if (!typeof(T).IsEnum)
{
throw new ArgumentException("Generic parameter must be enum");
}
}
public virtual IEnumerator<T> GetEnumerator()
{
return Enum.GetValues(typeof(T)).Cast<T>().GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return ((IEnumerable<T>)this).GetEnumerator();
}
}
**UPDATED** Paulo Morgado reminded me that I could just call GetEnumerator().
This is merely hiding the necessity of calling Enum.GetValue(typeof(T)).Cast<T>(), but it makes for friendlier tests.
[TestMethod]
public void ShouldCreateValidListAndReturnFirstValue()
{
var roles= new EnumList<Role>();
Assert.AreEqual(Role.Author, roles.First());
}
When displaying enum values, it may be requisite to flag a value to not display, or to show a better description than the value itself. We can use attributes to achieve this. A description attribute is provided in System.ComponentModel. I created the Visible attribute.
{
public class VisibleAttribute : Attribute
{
private readonly bool visible;
public bool Visible
{
get { return this.visible; }
}
public VisibleAttribute()
{
this.visible = true;
}
public VisibleAttribute(bool visible)
{
this.visible = visible;
}
}
I then set the appropriate attributes on the enum values.
public enum Role
{
Author = 1,
Editor = 2,
[Description("Sales Representative")]
SalesRepresentative = 3,
[Visible(false)]
Secret = 4,
[Visible(false)]
AnotherSecret = 5,
Manager = 6
}
Here’s where we can begin empowering enums. All enums can be given extension method to retrieve the value of the attribute.
public static class EnumExtensions
{
public static bool IsVisible(this Enum value)
{
var visible = value.GetType()
.GetField(value.ToString())
.GetCustomAttributes(typeof(VisibleAttribute), false)
.Cast<VisibleAttribute>()
.FirstOrDefault() ?? new VisibleAttribute(true);
return visible.Visible;
}
public static string Description(this Enum value)
{
var description = value.GetType()
.GetField(value.ToString())
.GetCustomAttributes(typeof(DescriptionAttribute), false)
.Cast<DescriptionAttribute>()
.FirstOrDefault() ?? new DescriptionAttribute(value.ToString());
return description.Description;
}
}
The great thing is that these methods work for every attribute. EnumExtensions is a helper class, but we treat these methods as belonging to the enum value.
The original post was interested in displaying friendly names which consists of only those values that are visible. In addition, we want to use the description instead of the value if it is available. This can be achieved by writing an extension method for IEnumerable<Role>.
public static class RoleListExtensions
{
public static IEnumerable<string> DisplayFriendlyNames(this EnumList<Role> list)
{
return list.Where(role => role.IsVisible())
.Select(role => role.Description());
}
}
This allows the test to pass, which proves that only visible descriptions pulled from the list of roles.
[TestMethod]
public void DisplayFriendlyNamesShouldBeVisibleAndFullyDescriptive()
{
var roles = new EnumList<Role>().DisplayFriendlyNames().ToArray();
Assert.AreEqual(4, roles.Length);
Assert.AreEqual("Author", roles[0]);
Assert.AreEqual("Editor", roles[1]);
Assert.AreEqual("Sales Representative", roles[2]);
Assert.AreEqual("Manager", roles[3]);
}
There are strengths to creating classes based on an enum. The approach I demonstrated should only be used when it is desirable to add a little functionality to enums across the board. Create classes for anything more complex. It is possible to go overboard with the extension methods and completely mimic a class from an enum… if you find yourself doing this, perhaps a class would be more appropriate.