ASP.NET MVC - Creating a DropDownList helper for enums
Do the types you work with in your ASP.NET MVC models ever have enums? Mine do from time to time and I’ve found myself needing to render a dropdown list to allow the user to select the enum value.
For the purposes of this post, I will be working with a Person class and a Color enum (I’ll direct you to some posts by my colleagues to help you decide whether you think Color makes a good enum: here, here and here), but we’ll go with it here :-)
public enum Color { Red, Green, Blue, BrightRed, BrightGreen, BrightBlue, }
public class Person { public int Id { get; set; } public string Name { get; set; } public Color FavoriteColor { get; set; } }
Implementing EnumDropDownList
This post will look at how we can construct a Html Helper to allow us to easily achieve this in a view. Html Helpers are simply extension methods for HtmlHelper, so we will will start with a static class and extension method:
public static class HtmlDropDownExtensions { public static MvcHtmlString EnumDropDownList<TEnum>(this HtmlHelper htmlHelper, string name, TEnum selectedValue) { } }
In the signature above, name is the name of the property we’re rendering and selectedValue is the current model value. The flow of our method will be
- Get the list of possible enum values
- Convert the enum values to SelectListItems
- Use the in-built html helpers to render the SelectListItems as a drop down list
The following code does just that:
public static MvcHtmlString EnumDropDownList<TEnum>(this HtmlHelper htmlHelper, string name, TEnum selectedValue) { IEnumerable<TEnum> values = Enum.GetValues(typeof(TEnum)) .Cast<TEnum>(); IEnumerable<SelectListItem> items = from value in values select new SelectListItem { Text = value.ToString(), Value = value.ToString(), Selected = (value.Equals(selectedValue)) }; return htmlHelper.DropDownList( name, items ); }
Note that we’re using the string value of the enum as the Value for the SelectListItem – fortunately the default model binding in ASP.NET MVC will handle that when the value gets POSTed back.
With this method in place, we can render a drop down using the following
<%= Html.EnumDropDownList("Person.FavoriteColor", Model.Person.FavoriteColor) %>
Implementing EnumDropDownListFor
The helper above is a definite improvement compared to dealing with the enum values directly in the view. This helps to keep your views nice and clean, and the helper fits nicely in alongside some of the existing helpers e.g.
<%= Html.TextBox("Person.Id", Model.Person.Id)%>
But what if you’re used to using the strongly-typed Html helpers, e.g.
<%= Html.TextBoxFor(model => model.Person.Id)%>
If that’s the case then the helper that we just created might not feel quite right. So, let’s create another helper that will fit in better alongside the strongly-typed helpers. The first thing that we need to sort out is the function signature:
public static MvcHtmlString EnumDropDownListFor<TModel, TEnum>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TEnum>> expression)
Notice that we’re now taking an Expression<> that describes how to get from the model to the property that we’re interested in. With that in place we’re pretty much set. We’ll take advantage of the ModelMetadata.FromLambdaExpression function which allows us to retrieve the ModelMetadata for the property that is described by the expression, and we’ll use its Model property to get the current value:
public static MvcHtmlString EnumDropDownListFor<TModel, TEnum>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TEnum>> expression) { ModelMetadata metadata = ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData); IEnumerable<TEnum> values = Enum.GetValues(typeof(TEnum)).Cast<TEnum>(); IEnumerable<SelectListItem> items = values.Select(value => new SelectListItem { Text = value.ToString(), Value = value.ToString(), Selected = value.Equals(metadata.Model) }); return htmlHelper.DropDownListFor( expression, items ); }
Now we can use the following syntax in our view:
<%= Html.EnumDropDownListFor(model => model.Person.FavoriteColor)%>
Nullable Enum Support
The strongly typed version is pretty nice, but it turns out that there are a couple of limitations (ignoring the basics like argument validation ;-) ). The first issue is that it fails if we have a nullable enum, i.e. if our FavoriteColor property was defined as Color? rather than Color. This is the C# shorthand for Nullable<Color>, so we need to check for nullable types and pass the base type to Enum.GetValues. For this we’ll create a little helper function
private static Type GetNonNullableModelType(ModelMetadata modelMetadata) { Type realModelType = modelMetadata.ModelType; Type underlyingType = Nullable.GetUnderlyingType(realModelType); if (underlyingType != null) { realModelType = underlyingType; } return realModelType; }
And update our html helper
public static MvcHtmlString EnumDropDownListFor<TModel, TEnum>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TEnum>> expression) { ModelMetadata metadata = ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData); Type enumType = GetNonNullableModelType(metadata); IEnumerable<TEnum> values = Enum.GetValues(enumType).Cast<TEnum>(); IEnumerable<SelectListItem> items = values.Select(value => new SelectListItem { Text = value.ToString(), Value = value.ToString(), Selected = value.Equals(metadata.Model) }); if (metadata.IsNullableValueType) { items = SingleEmptyItem.Concat(items); } return htmlHelper.DropDownListFor( expression, items ); }
We’re calling the GetNonNullableModelType in the second line to ensure that the type passed to Enum.GetValues is a non-nullable enum type. Also, notice that we’re checking whether the type is nullable and adding an empty item to the list if it is. For ease of use this is declared as a field on the class as
private static readonly SelectListItem[] SingleEmptyItem = new[] { new SelectListItem { Text = "", Value = "" } };
The reason that this is important is that for a nullable enum you would want to support setting the value to null, i.e. “no value”!
We can use this version of the helper in the same way as the previous version, it’s just that it now supports nullable enums – notice the blank entry at the top of the list:
Adding flexibility
Our helper is really starting to take shape. We’ve got an old-school version that takes the name and value and a strongly-typed version that takes an expression and deals with both nullable and non-nullable enums. However, the screenshot above highlights another problem: enum names aren’t necessarily user friendly – “BrightRed” is probably now how you would want to present the value to the user. With that in mind we’ll make one final tweak to allow more flexibility over how the enum values are displayed to the user. To achieve this, we will simply make use of TypeConverters:
public static MvcHtmlString EnumDropDownListFor<TModel, TEnum>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TEnum>> expression) { ModelMetadata metadata = ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData); Type enumType = GetNonNullableModelType(metadata); IEnumerable<TEnum> values = Enum.GetValues(enumType).Cast<TEnum>(); TypeConverter converter = TypeDescriptor.GetConverter(enumType); IEnumerable<SelectListItem> items = from value in values select new SelectListItem { Text = converter.ConvertToString(value), Value = value.ToString(), Selected = value.Equals(metadata.Model) }; if (metadata.IsNullableValueType) { items = SingleEmptyItem.Concat(items); } return htmlHelper.DropDownListFor( expression, items ); }
Note that we are using the converter only for the SelectListItem.Text. The value property needs to be the enum name otherwise the model binding will fail. If we apply these changes and re-run our site we’ll see that nothing has changed! That’s because the default type converter is simply calling ToString on the enum values. All we’ve done so far is to add the extensibility point to EnumDropDownListFor. To make of of it we need to apply a type converter to our enum
[TypeConverter(typeof(PascalCaseWordSplittingEnumConverter))] public enum Color { Red, Green, Blue, BrightRed, BrightGreen, BrightBlue, }
I’ve omitted the code for PascalCaseWordSplittingEnumConverter, but it simply splits words based on Pascal-casing rules as show below (note the space in “Bright Red” etc)
Summary
This started out as a pretty short post in my mind, but grew as I added support for a strongly-typed version, added support for nullable enums and then support for type converters.
The Pascal-casing type converter is just a simple example. You’d probably want to use a type converter that looked for attributes on your enum values to allow you to specify the display value. Better still would be a converter that looks for attributes that let you specify the resource to use for the display value so that you get the benefits of localisation etc.
Finally, a reminder that this is sample code and the usual disclaimers apply etc. If you do improve on it or find a bug then let me know:-)
Comments
Anonymous
June 02, 2010
Cool article. I made some changes to the extension method: public static class HtmlExtensions { public static MvcHtmlString EnumDropDownListFor<TModel,TEnum>(this HtmlHelper<TModel> htmlHelper,Expression<Func<TModel,TEnum>> expression) { IEnumerable<TEnum> values = Enum.GetValues(typeof(TEnum)) .Cast<TEnum>(); TEnum prop = expression.Compile().Invoke(htmlHelper.ViewData.Model); IEnumerable<SelectListItem> items = from value in values select new SelectListItem { Text = value.ToString(), Value = value.ToString(), Selected = (value.Equals(prop)) }; return SelectExtensions.DropDownListFor<TModel,TEnum>(htmlHelper,expression,items); } }Anonymous
June 03, 2010
Hi Bryan, That's an interesting alternative approach. One thing you might want to check is the cost of the expression compilation. I think that the ASP.NET MVC team did some did some work to cache expression compilations (you can always have a look in the source to see!) The other thing that comes to mind is that the change won't cater for properties which are nullable enums. To do this you'd need to take a similar approach to the one from the post: public static MvcHtmlString EnumDropDownListForX2<TModel, TEnum>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TEnum>> expression) { Type enumType = typeof(TEnum); Type actualEnumType = Nullable.GetUnderlyingType(enumType); // handle nulls IEnumerable<TEnum> values = Enum.GetValues(actualEnumType).Cast<TEnum>(); TEnum prop = expression.Compile().Invoke(htmlHelper.ViewData.Model); IEnumerable<SelectListItem> items = from value in values select new SelectListItem { Text = value.ToString(), Value = value.ToString(), Selected = (value.Equals(prop)) }; if (enumType != actualEnumType) { items = SingleEmptyItem.Concat(items); } return SelectExtensions.DropDownListFor(htmlHelper, expression, items); } Thanks for your comments - StuartAnonymous
June 04, 2010
Yea...I totally need to download the source and start getting into it. I'm relatively a newbie in the MVC world. Thanks for the heads up on the nullable enums. I have to admit I make the mistake of considering the "coolness" of the code before I consider the performance often. I'll have to check and see how they are caching expressions.Anonymous
July 12, 2010
Stuart, This was a great post, very helpful. The only change I had to make was to change the value section in the select to "Value = ((int) Enum.Parse(enumType, Enum.GetName(enumType,value))).ToString()," as I needed the integer value. I was wondering if you might have a better way. Thanks again!!!Anonymous
July 25, 2010
Hi Matt, I think that you can probably simplify that slightly with something along the lines of Value = Convert.ToInt32(value).ToString()
- Stuart
Anonymous
December 06, 2010
when value is "int", the dropdown doesn't select the SelectListItem with Selected = true.Anonymous
December 15, 2010
Hi rajesh, I'm not entirely clear what you mean - can you give a bit more of an example? Thanks, StuartAnonymous
February 07, 2011
I get a compilation error. The error is about htmlHelper.DropDownListFor and htmlHelper.DropDownList. 'System.Web.Mvc.HtmlHelper' does not contain a definition for 'DropDownList' and no extension method 'DropDownList' accepting a first argument of type 'System.Web.Mvc.HtmlHelper' could be found (are you missing a using directive or an assembly reference?)Anonymous
February 07, 2011
The comment has been removedAnonymous
February 07, 2011
Done compiling. I'll read the next time better before commenting. Sorry.Anonymous
February 09, 2011
In addition to what stuartle said for converting to the enum's actual value, not name, you can use: Type baseEnumType = Enum.GetUnderlyingType(enumType); ... then set the value of the SelectListItem to: Value = Convert.ChangeType(value, baseEnumType).ToString(), ... For those instances where you don't actually know if the underlying type of you Enum is Int :)Anonymous
June 15, 2011
Great post Stuart. Thanks very much. How about posting the code for PascalCaseWordSplittingEnumConverter? I've had a look at creating a custom type converter and it seems non-trivial. cheers. DSAnonymous
June 16, 2011
@ Damo123 - the code I used for the blog post is below. I've not tested it beyond the needs of this blog post though, so use with extreme caution :-) public class PascalCaseWordSplittingEnumConverter : EnumConverter { public PascalCaseWordSplittingEnumConverter(Type type) : base(type) { } public override object ConvertTo(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value, Type destinationType) { if (destinationType == typeof(string)) { string stringValue = (string) base.ConvertTo(context, culture, value, destinationType); stringValue = SplitString(stringValue); return stringValue; } return base.ConvertTo(context, culture, value, destinationType); } public string SplitString(string stringValue) { StringBuilder buf = new StringBuilder(stringValue); // assume first letter is upper! bool lastWasUpper = true; int lastSpaceIndex = -1; for (int i = 1; i < buf.Length; i++) { bool isUpper = char.IsUpper(buf[i]); if (isUpper & !lastWasUpper) { buf.Insert(i, ' '); lastSpaceIndex = i; } if (!isUpper && lastWasUpper) { if (lastSpaceIndex != i - 2) { buf.Insert(i - 1, ' '); lastSpaceIndex = i - 1; } } lastWasUpper = isUpper; } return buf.ToString(); } }Anonymous
March 16, 2012
The comment has been removedAnonymous
April 13, 2012
I liked this post. Thanks Stuart. I made some changes in order to use DisplayAttribute annotations instead of a TypeConverter. I like to define my enums this way: public enum TransferFrequency { [Display(Name = "One time, immediately")] OneTime, [Display(Name = "One time, on...")] OneTimeOn, [Display(Name = "Weekly")] Weekly } Here's the code in case anyone is interested (semi tested so use at your own risk): public static MvcHtmlString EnumDropDownListFor<TModel, TEnum>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TEnum>> expression, object htmlAttributes) { ModelMetadata metadata = ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData); Type enumType = GetNonNullableModelType(metadata); Type baseEnumType = Enum.GetUnderlyingType(enumType); List<SelectListItem> items = new List<SelectListItem>(); foreach (FieldInfo field in enumType.GetFields(BindingFlags.Static | BindingFlags.GetField | BindingFlags.Public)) { string text = field.Name; string value = Convert.ChangeType(field.GetValue(null), baseEnumType).ToString(); bool selected = field.GetValue(null).Equals(metadata.Model); foreach (var displayAttribute in field.GetCustomAttributes(true).OfType<DisplayAttribute>()) { text = displayAttribute.GetName(); } items.Add(new SelectListItem() { Text = text, Value = value, Selected = selected }); } if (metadata.IsNullableValueType) { items.Insert(0, new SelectListItem { Text = "", Value = "" }); } return SelectExtensions.DropDownListFor(htmlHelper, expression, items, htmlAttributes); }Anonymous
June 20, 2012
Did my own implementation, created a new gist for people to fork: gist.github.com/2965119 Summary: Generic expression support Nullable<Enum> support Optional custom display text Full documentationAnonymous
February 19, 2013
Hi Stuart, thank you for the great tutorial! I tried Shea Strickland suggestion but it didn't work for me (I wanted to save the integer in my DB). So I kept the Helper as explained in the tutorial and did as suggested here in my model class: stackoverflow.com/.../best-method-to-store-enum-in-database hope it helps, I lost a few hours to figure this out...Anonymous
March 19, 2013
The comment has been removedAnonymous
April 24, 2013
What about flags Enum??Anonymous
October 11, 2013
when i use it in edit templet the drop down not appear with selected value.Anonymous
February 11, 2014
iam having same problem as nimesh it is not getting the selected value in edit templateAnonymous
May 20, 2014
I'm having the same issue in edit template. Did anyone find a solution?Anonymous
May 20, 2014
Not sure why that would fail. Can you paste in your enum definition and model definition? Also, what version of MVC are you using?Anonymous
July 22, 2014
Hi, good new. I read this article paulthecyclist.com/2014/04/03/enum-dropdown-mvc5-1. dropdownlist include in asp.net mvc 5.1Anonymous
December 30, 2014
I must use mvc 4 with code first approach and I use EnumDropDownListFor as above. The model is built as the following approach. stackoverflow.com/.../how-is-interpreted-an-enum-type-with-ef-code-first. I want to create a EnumDropDownListFor to input FavoriteColor and stored as FavoriteColorValue in database. I am getting an error of "Unable to cast object of type 'System.Int32' to type 'System.String'." I am trying to solve by changing Value = value.ToString(), into Value = Convert.ToInt32(value).ToString(). However, I still get an error of "Unable to cast object of type 'System.Int32' to type 'System.String'." Can you help me?? Thank you very much.Anonymous
December 30, 2014
Sorry. Please delete the above question. The helper extension, EnumDropDownListFor, work well with MVC 4 Code first. It work well with the model, FavoriteColor, inside the following link. stackoverflow.com/.../how-is-interpreted-an-enum-type-with-ef-code-first No any additional modification, say Value = value.ToString(), is needed to implement this solution. Thank you very much. Unable to cast object of type 'System.Int32' to type 'System.String'." is due to error in data annotation in model class.Anonymous
April 23, 2015
Nice article with very good examples, for more detail www.advancesharp.com/.../mvc-dropdown-binding-best-ways www.advancesharp.com/.../mvc-dropdownlistfor-fill-on-selection-change-of-another-dropdownAnonymous
September 27, 2015
The comment has been removedAnonymous
October 22, 2015
Red Green Blue Bright Red Bright Green Bright Blue
i want this list like : Red Bright Red Blue Bright Blue Green Bright Green is there any helper extension?