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:

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