Notes to Self

View the Project on GitHub ssarabando

Bind to an enum in ASP.NET MVC 4 and show the correct resource string

28 Aug 2015

To my future self: this has been a very big detour. I’m again jumping over globalization hurdles, but this time in ASP.NET MVC 4.

My problem was simple: given an enum in one of the models, how to show it in a Razor view and in the correct language?

For example, given this enum:

public enum SignalType
{
    Binary,
    Analog
}

How to make Razor render the following (simplified) HTML when the browser’s UI is in portuguese:

<select>
    <option value="Binary">Binário</option>
    <option value="Analog">Analógico</option>
</select>

And this one when in english:

<select>
    <option value="Binary">Binary</option>
    <option value="Analog">Analog</option>
</select>

As usual, the plan was to use the Display attribute and fetch the string from a resource. Thus, the enum ended up like this:

public enum SignalType
{
    [Display(ResourceType = typeof(Resources), Name = "SignalTypeBinaryCaption")]
    Binary,
    [Display(ResourceType = typeof(Resources), Name = "SignalTypeAnalogCaption")]
    Analog
}

Now I needed to iterate through the enum’s members and render them as option tags.

To my disappointment, there was not built-in helper for that in MVC 4 (although it seems there is in ASP.NET MVC 5.1+) so I was forced to adapt a solution I found in Stack Overflow (see the references).

That solution creates a helper method named EnumDropDownListFor<> which we can call from a Razor view as usual but used a custom attribute named Description which wasn’t what I wanted. I needed the name from the resource file or from the Display attribute or the enum’s value (if no Display attribute was present).

So I took the solution’s code and changed the body of the GetEnumDescription<TEnum>(TEnum value) method like this:

public static string GetEnumDescription<TEnum>(TEnum value)
{
    FieldInfo fi = value.GetType().GetField(value.ToString());

    var attributes = (DisplayAttribute[])fi.GetCustomAttributes(
        typeof(DisplayAttribute), false);

    if ((attributes != null) && (attributes.Length > 0))
    {
        return attributes[0].GetName();
    }

    return value.ToString();
}

If you compare the above with the original you’ll notice that I only changed the attribute type being sought (DisplayAttribute instead of DescriptionAttribute) and invoked the 1st attribute’s GetName() method instead of returning the Description property in the if statement.

With that I can now simply write the following in my Razor view:

@Html.EnumDropDownListFor(model => model.SensorSignalType)

Full code listing

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Web;
using System.Web.Mvc;
using System.Web.Mvc.Html;

namespace System.Web.Mvc.Html
{
    // See http://stackoverflow.com/a/5255108/215576
    public static class HtmlHelperExtensions
    {
        private static Type GetNonNullableModelType(ModelMetadata modelMetadata)
        {
            Type realModelType = modelMetadata.ModelType;

            Type underlyingType = Nullable.GetUnderlyingType(realModelType);
            if (underlyingType != null)
            {
                realModelType = underlyingType;
            }
            return realModelType;
        }

        private static readonly SelectListItem[] SingleEmptyItem = new[]
            {
                new SelectListItem { Text = "", Value = "" }
            };

        public static string GetEnumDescription<TEnum>(TEnum value)
        {
            FieldInfo fi = value.GetType().GetField(value.ToString());

            var attributes = (DisplayAttribute[])fi.GetCustomAttributes(
                typeof(DisplayAttribute), false);

            if ((attributes != null) && (attributes.Length > 0))
            {
                return attributes[0].GetName();
            }

            return value.ToString();
        }

        public static MvcHtmlString EnumDropDownListFor<TModel, TEnum>(
            this HtmlHelper<TModel> htmlHelper,
            Expression<Func<TModel, TEnum>> expression)
        {
            return EnumDropDownListFor(htmlHelper, expression, null);
        }

        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);
            IEnumerable<TEnum> values = Enum.GetValues(enumType).Cast<TEnum>();

            IEnumerable<SelectListItem> items = values.Select(value =>
                new SelectListItem
                    {
                        Text = GetEnumDescription(value),
                        Value = value.ToString(),
                        Selected = value.Equals(metadata.Model)
                    });

            // If the enum is nullable, add an 'empty' item to the collection
            if (metadata.IsNullableValueType)
                items = SingleEmptyItem.Concat(items);

            return htmlHelper.DropDownListFor(expression, items, htmlAttributes);
        }
    }
}

References

This solution was heavily influenced by the following post: