I have a few enumerations in DevEvents, and I found myself frequently writing code similar to the following:
Dictionary<string, int> etlist = new Dictionary<string, int>();
etlist.Add("Code Camp", (int)EventType.CodeCamp);
etlist.Add("Conference", (int)EventType.Conference);
etlist.Add("Day of .NET", (int)EventType.DayOfDotNet);
etlist.Add("Hands-On Labs", (int)EventType.HandsOnLabs);
etlist.Add("User Group Meeting", (int)EventType.UserGroup);
etlist.Add("Workshop", (int)EventType.Workshop);
etlist.Add("Pub Night", (int)EventType.PubNight);
etlist.Add("Online", (int)DevEvents.Common.EventType.Online);
etlist.Add("BarCamp", (int)DevEvents.Common.EventType.BarCamp);
etlist.Add("Geek Dinner", (int)DevEvents.Common.EventType.GeekDinner);
if (evnt != null)
ViewData["EventTypes"] = new SelectList(etlist, "Value", "Key", evnt.EventTypeID);
else
ViewData["EventTypes"] = new SelectList(etlist, "Value", "Key");
Update - 1/12/2009: For an easier way to do the above, check out this post by Rune Jacobson.
The problem with the approach however, is that every time I need to add a new enumeration value, I would have to modify each of these code sections, and that got very old very fast. I needed a way to generate dropdowns from the enumerations themselves. I ended up with a custom HtmlHelper to generate the dropdowns and eliminate the repetitive code.
First, I created the base HtmlHelper:
public static string EnumDropDown(this HtmlHelper helper, Type enumType, string id)
The ID is used to set the HTML ID attribute, since some dropdowns were being used by jQuery. The parameter enumType is used to pass in the type of enumeration that the dropdown should be generated from. Since I need it to be an enumeration, I check as such, returning an empty string if the check fails.
if (enumType == null || !enumType.IsEnum)
return "";
Next, I attempt to retrieve a value to set as default; first from ViewData, and then from the model if it has been set. I catch an empty exception at the end, which is typically a bad practice, because it is not important if the value cannot be found – the View may be an empty form.
object defaultValue = null;
if (helper.ViewData != null)
defaultValue = helper.ViewData.Eval(id);
try
{
if (defaultValue == null)
defaultValue = helper.ViewData.Model.GetType().GetProperty(id).GetValue(helper.ViewData.Model, null);
}
catch (Exception) { }
Finally, I set about to return HTML back to the View for the desired dropdown. I create a StringBuilder, append the select start tag, and iterate through all the fields on the enumeration:
StringBuilder sb = new StringBuilder();
sb.AppendFormat("<select id=\"{0}\">", id);
foreach (var item in enumType.GetFields(BindingFlags.Public | BindingFlags.Static))
{
Next, for each enumeration field, I check whether the value equals the expected default value determined above:
string def = "";
if (defaultValue != null && defaultValue.ToString() == item.GetRawConstantValue().ToString())
def = "selected=\"selected\"";
Finally, I append each option to the StringBuilder:
sb.AppendFormat("<option value=\"{0}\" {1}>{2}</option>",
item.GetRawConstantValue().ToString(), def, item.Name);
}
sb.Append("</select>");
return sb.ToString();
I was then able to call my HtmlHelper from the View as follows:
<%= Html.EnumDropDown(typeof(DevEvents.Common.EventType), "EventTypeID") %>
I ran the code, and had a dropdown rendered with each item in the enumeration. There was one small problem however; each option display value was the enumeration field name, exactly. Instead of seeing a pretty name like “Hands-On Labs”, I saw “HandsOnLabs”. Of course this is to be expected based on the generation method used, but how do I enable a more elegant UX?
The solution I used was to create a custom Attribute for my custom enumerations, storing a single property called DisplayName:
[AttributeUsage(AttributeTargets.Field)]
public class EnumDisplayNameAttribute : Attribute
{
private string _displayName;
public EnumDisplayNameAttribute(string displayName)
{
_displayName = displayName;
}
public string DisplayName
{
get
{
return _displayName;
}
}
}
I then applied that attribute to my custom enumerations as follows:
public enum EventType
{
[EnumDisplayName("User Group")]
UserGroup = 0,
[EnumDisplayName("Code Camp")]
CodeCamp = 1,
[EnumDisplayName("Day of .NET")]
DayOfDotNet = 2,
[EnumDisplayName("Conference")]
Conference = 3,
[EnumDisplayName("Hands-On Lab")]
HandsOnLabs = 4,
[EnumDisplayName("Workshop")]
Workshop = 5,
[EnumDisplayName("Pub Night")]
PubNight = 6,
[EnumDisplayName("Online")]
Online = 7,
[EnumDisplayName("BarCamp")]
BarCamp = 8,
[EnumDisplayName("Geek Dinner")]
GeekDinner = 9
}
Finally, in my HtmlHelper, I checked for the presence of this attribute on each enumeration field, and set the display name accordingly:
EnumDisplayNameAttribute[] attributes =
(EnumDisplayNameAttribute[])item.GetCustomAttributes(typeof(EnumDisplayNameAttribute), false);
if (attributes.Length > 0)
sb.AppendFormat("<option value=\"{0}\" {1}>{2}</option>",
item.GetRawConstantValue().ToString(), def, attributes[0].DisplayName);
else
sb.AppendFormat("<option value=\"{0}\" {1}>{2}</option>",
item.GetRawConstantValue().ToString(), def, item.Name);
Here is the final completed code for the HtmlHelper:
public static string EnumDropDown(this HtmlHelper helper, Type enumType, string id)
{
if (enumType == null || !enumType.IsEnum)
return "";
object defaultValue = null;
if (helper.ViewData != null)
defaultValue = helper.ViewData.Eval(id);
try
{
if (defaultValue == null)
defaultValue = helper.ViewData.Model.GetType().GetProperty(id).GetValue(helper.ViewData.Model, null);
}
catch (Exception) { }
StringBuilder sb = new StringBuilder();
sb.AppendFormat("<select id=\"{0}\">", id);
foreach (var item in enumType.GetFields(BindingFlags.Public | BindingFlags.Static))
{
string def = "";
if (defaultValue != null && defaultValue.ToString() == item.GetRawConstantValue().ToString())
def = "selected=\"selected\"";
EnumDisplayNameAttribute[] attributes =
(EnumDisplayNameAttribute[])item.GetCustomAttributes(typeof(EnumDisplayNameAttribute), false);
if (attributes.Length > 0)
sb.AppendFormat("<option value=\"{0}\" {1}>{2}</option>",
item.GetRawConstantValue().ToString(), def, attributes[0].DisplayName);
else
sb.AppendFormat("<option value=\"{0}\" {1}>{2}</option>",
item.GetRawConstantValue().ToString(), def, item.Name);
}
sb.Append("</select>");
return sb.ToString();
}
What do you think? Would you have done it differently?
Updated:
In thinking about this, the next logical step would be to store the enumeration values in the database so they could be updated at runtime. While I’m not going to set about to do this right away, I would imagine pulling them when the application starts and caching them using a SQL cache dependency. Not too many things should be cached in application memory, but this list should stay relatively small. Thoughts?