Stuart Leeks

Stuart Leeks - Application Development Consultant

ASP.NET MVC - Creating a DropDownList helper for enums

ASP.NET MVC - Creating a DropDownList helper for enums

  • Comments 24

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:

image

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)

image

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:-)

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

           }

       }

  • 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 - Stuart

  • 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.

  • 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!!!

  • Hi Matt,

    I think that you can probably simplify that slightly with something along the lines of

    Value =  Convert.ToInt32(value).ToString()

    - Stuart

  • when value is "int", the dropdown doesn't select the SelectListItem with Selected = true.

  • Hi rajesh,

    I'm not entirely clear what you mean - can you give a bit more of an example?

    Thanks,

    Stuart

  • 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?)

  • Just solved it a bit. The error is gone when referencing th Mvc.Html namespace. Now I only have an error left on the line with SingleEmptyItem. The same error. What namespace should be referenced here?

  • Done compiling. I'll read the next time better before commenting. Sorry.

  • 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 :)

  • 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.

    DS

  • @ 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();

           }

       }

  • I took a stab at a different approach for the SplitString method. Not exhaustively tested though:

    public string SplitString(string stringValue)

    {

    return Regex.Replace(stringValue, @"((?<=[a-z])[A-Z]\w|(?<=\w)[A-Z][a-z])", @" $0");

    }

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

           }

Page 1 of 2 (24 items) 12
Leave a Comment
  • Please add 3 and 4 and type the answer here:
  • Post