ASP.NET MVC Rendering Enum DropDownLists, Radio Buttons and ListBoxes

Hi Folks,

Sometimes you will want to display Enums for your lists. This can easily be done with some HTML extensions and then evaluating the member expression.

It is a two step process.

  1. Get the expression and and get the type
  2. If the Model has data, then compile the expression to get the value
  3. If the Model does not have data, then parse the expression and get the enum values via reflection

Here is some HTML Helper extensions that you can find useful. It also has support for place holders, where you can provide a hint or tip as the first element in the list for non HTML 5 support.

Remember this important rule.

  1. Your model for single select items from a DropDownList should NEVER be an IEnumerable, it should just be a string or int or enum etc, since it is only ONE value going in the form post.
  2. Use the ViewBag to store the entire list of items to CHOOSE from
  3. Use the model to STORE the single value item SELECTED
  4. For multiple selects, like ListBox, then you can have an IEnumerable in the model, since the user will select multiple values.

Allot of people get confused and assume that because they need a drop down list for a typed view, that it must be IEnumerable or IList or whatever sort of collection in the model, this is not true for single select items from lists.

By understanding the fundamentals of HTTP GET and HTTP POST, you will then realise that if only single item form a list needs to be selected that the form values is only going to be one item, and hence your model can represent how the HTTP FORM DATA coming over the wire looks like. Perhaps a blog post soon on HTML and MVC Model Bindings….

Note, that ordering of enum values is supported with Display attributes. Also, since you can have nullable enums, the EnumRadioButtonFor support none option, turn it off with explicit false.

@Html.EnumRadioButtonFor(m => m.Gender, false)
public enum Gender {
 [Display(Name="Male", Order=0)]
 Male,
 [Display(Name="Female", Order=1)]
 Female,
 [Display(Name="WhoTheHellKnows", Order=2)]
 Both
}

public class MyModel
{
 public string User {get; set;}
 public Gender Gender {get; set;}
 public IEnumerable {get; set;} //This is ok for multi select lists, like ListBox
}

Remember HTTP GET –>Controller–> ViewBag.Users(Populate from database in controller for non enums) –> @Html.DropDownListFor(m => m.User, ViewBag.Users)

For Enums
@Html.EnumDropDownListFor(m => m.Gender)

See above, the SINGULAR property in the model for m.User.

The ViewBag.Users has an IEnumerble of users.

Right, now here is your HTML extensions for ENUM lists

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

namespace Web
{
    public static class HtmlHelperExtensions
    {
        public static MvcHtmlString EnumDropDownList<TEnumType>(this HtmlHelper htmlHelper, string name, TEnumType value)
        {
            var selectItems = GetSelectItemsForEnum(typeof(TEnumType), value);
            return htmlHelper.DropDownList(name, selectItems);
        }

        public static MvcHtmlString EnumDropDownListPlaceholder<TEnumType>(this HtmlHelper htmlHelper, string name, TEnumType value, string placeholderName = null)
        {
            var selectItems = GetSelectItemsForEnum(typeof(TEnumType), value);

            AddPlaceHolderToSelectItems(placeholderName, selectItems);
            return htmlHelper.DropDownList(name, selectItems, new { @class = "placeholder" });
        }

        public static MvcHtmlString GenerateHiddenFieldsForIncomingModel<TModel, TProperty>(
        this HtmlHelper<TModel> htmlHelper,
        Expression<Func<TModel, TProperty>> expression,
        object model)
        {
            var sb = new StringBuilder();

            foreach (PropertyInfo info in model.GetType().GetProperties())
            {
                if (info.CanRead)
                {
                    var o = info.GetValue(model, null);

                    if (o is DateTimeOffset || o is DateTime)
                        sb.Append("<input type='hidden' name='" + info.Name + "' value='" + string.Format("{0:dd MMM yyyy}", o) + "'/>");
                    else if (!(o is IList))
                        sb.Append("<input type='hidden' name='" + info.Name + "' value='" + o + "'/>");
                }
            }
            return MvcHtmlString.Create(sb.ToString());
        }

        public static MvcHtmlString EnumRadioButtonFor<TModel, TProperty>(
        this HtmlHelper<TModel> htmlHelper,
        Expression<Func<TModel, TProperty>> expression,
        bool includeNoneOption = true,
        bool isDisabled = false,
        string cssClass = null
    ) where TModel : class
        {
            var memberExpression = expression.Body as MemberExpression;
            if (memberExpression == null)
                throw new InvalidOperationException("Expression must be a member expression");

            var name = ExpressionHelper.GetExpressionText(expression);
            var fullName = htmlHelper.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldName(name);
            ModelState currentValueInModelState;
            var couldGetValueFromModelState = htmlHelper.ViewData.ModelState.TryGetValue(fullName, out currentValueInModelState);
            object selectedValue = null;
            if (!couldGetValueFromModelState && htmlHelper.ViewData.Model != null)
            {
                selectedValue = expression.Compile()(htmlHelper.ViewData.Model);
            }

            var enumNames = GetSelectItemsForEnum(typeof(TProperty), selectedValue).Where(n => !string.IsNullOrEmpty(n.Value)).ToList();

            if (includeNoneOption)
            {
                if(!enumNames.Any(e => e.ToLowerInvariant() == "none"))
                    enumNames.Add("None");
            }

            var metaData = ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData);
            var sb = new StringBuilder();
            sb.AppendFormat(
                "<ul class=\"radio-button-list{0}\">",
                string.IsNullOrEmpty(cssClass) ? string.Empty : " " + cssClass);
            foreach (var enumName in enumNames)
            {
                var id = string.Format(
                    "{0}_{1}_{2}",
                    htmlHelper.ViewData.TemplateInfo.HtmlFieldPrefix,
                    metaData.PropertyName,
                    enumName.Value
                    );

                if (id.StartsWith("-"))
                    id = id.Remove(0, 1);

                var value = enumName;
                if (includeNoneOption && enumName == "None")
                    value = string.Empty;

                sb.AppendFormat(
                    "<li>{2} <label for=\"{0}\">{1}</label></li>",
                    id,
                    HttpUtility.HtmlEncode(enumName),
                    isDisabled
                        ? htmlHelper.RadioButtonFor(expression, value, new {id, disabled = "disabled"}).ToHtmlString()
                        : htmlHelper.RadioButtonFor(expression, value, new {id}).ToHtmlString()
                    );
            }
            sb.Append("</ul>");
            return MvcHtmlString.Create(sb.ToString());
        }

        public static MvcHtmlString GuidanceNoteFor<TModel, TProperty>(
        this HtmlHelper<TModel> htmlHelper,
        Expression<Func<TModel, TProperty>> expression,
        string guidanceText)
        {
            var memberExpression = expression.Body as MemberExpression;
            if (memberExpression == null)
                throw new InvalidOperationException("Expression must be a member expression");

            //var metaData = ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData);
            var sb = new StringBuilder();
            var name = ExpressionHelper.GetExpressionText(expression);
            var outterType = expression.Parameters[0].Type;
            var attributes = outterType.GetProperty(name).GetCustomAttributesData();
            var displayText = "";

            foreach(var attr in attributes)
            {
                if (attr.NamedArguments != null)
                foreach (var namedArg in attr.NamedArguments.Where(namedArg => namedArg.MemberInfo.Name == "Name"))
                {
                    displayText = (string)namedArg.TypedValue.Value;
                }
            }

            if (String.IsNullOrEmpty(guidanceText))
                guidanceText = displayText;

            sb.Append("<div class=\"guidance-img\"></div>");
            sb.Append("<div class=\"guidance-box\">");
            sb.Append("    <table class=\"infobox\" width=\"250\" cellspacing=\"0\" cellpadding=\"0\">");
            sb.Append("    <tbody>");
            sb.Append("        <tr>");
            sb.Append("            <td class=\"left\" width=\"14\" rowspan=\"3\">");
            sb.Append("                <div></div>");
            sb.Append("            </td>");
            sb.Append("            <td class=\"top\" colspan=\"2\"><div></div>");
            sb.Append("            </td>");
            sb.Append("        </tr>");
            sb.Append("        <tr>");
            sb.Append("            <td class=\"info\" width=\"356\">");
            sb.Append("                <a href=\"javascript: void(0);\">close<div></div></a>");
            sb.AppendFormat("                <p>{0}</p>", guidanceText);
            sb.Append("            </td>");
            sb.Append("            <td class=\"right\" width=\"3\"><div></div>");
            sb.Append("            </td>");
            sb.Append("        </tr>");
            sb.Append("        <tr>");
            sb.Append("            <td class=\"bottom\" colspan=\"2\"><div></div>");
            sb.Append("            </td>");
            sb.Append("        </tr>");
            sb.Append("    </tbody>");
            sb.Append("</table>");
            sb.Append("</div>");

            return MvcHtmlString.Create(sb.ToString());
        }

        public static MvcHtmlString EnumDropDownListFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, IDictionary<string, object> htmlAttributes = null) where TModel : class
        {
            var memberExpression = expression.Body as MemberExpression;
            if (memberExpression == null)
                throw new InvalidOperationException("Expression must be a member expression");

            var name = ExpressionHelper.GetExpressionText(expression);
            var fullName = htmlHelper.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldName(name);
            ModelState currentValueInModelState;
            var couldGetValueFromModelState = htmlHelper.ViewData.ModelState.TryGetValue(fullName, out currentValueInModelState);
            object selectedValue = null;
            if (!couldGetValueFromModelState &&
                htmlHelper.ViewData.Model != null)
            {
                selectedValue = expression.Compile()(htmlHelper.ViewData.Model);
            }

            var placeholderName = PlaceholderName(memberExpression);

            htmlAttributes = ApplyHtmlAttributes(htmlAttributes, placeholderName);

            var selectItems = GetSelectItemsForEnum(typeof(TProperty), selectedValue).ToList();
            AddPlaceHolderToSelectItems(placeholderName, selectItems);

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

        public static MvcHtmlString PlaceholderDropDownListFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, IEnumerable<SelectListItem> selectList, string optionLabel, IDictionary<string, object> htmlAttributes = null)
            where TModel : class
        {
            var memberExpression = expression.Body as MemberExpression;
            if (memberExpression == null)
            throw new InvalidOperationException("Expression must be a member expression");

            IList<SelectListItem> list = selectList.ToList();
            var placeholderName = PlaceholderName(memberExpression);
            AddPlaceHolderToSelectItems(placeholderName, list);

            htmlAttributes = ApplyHtmlAttributes(htmlAttributes, placeholderName);

            return htmlHelper.DropDownListFor(expression, list, string.IsNullOrEmpty(optionLabel) ? null : optionLabel, htmlAttributes);
        }

        public static IList<SelectListItem> GetSelectItemsForEnum(Type enumType, object selectedValue)
        {
            var selectItems = new List<SelectListItem>();

            if (enumType.IsGenericType &&
                enumType.GetGenericTypeDefinition() == typeof(Nullable<>))
            {
                enumType = enumType.GetGenericArguments()[0];
                selectItems.Add(new SelectListItem { Value = string.Empty, Text = string.Empty });
            }

            var selectedValueName = selectedValue != null ? selectedValue.ToString() : null;
            var enumEntryNames = Enum.GetNames(enumType);
            var entries = enumEntryNames
                .Select(n => new
                {
                    Name = n,
                    DisplayAttribute = enumType
                        .GetField(n)
                        .GetCustomAttributes(typeof(DisplayAttribute), false)
                        .OfType<DisplayAttribute>()
                        .SingleOrDefault() ?? new DisplayAttribute()
                })
                .Select(e => new
                {
                    Value = e.Name,
                    DisplayName = e.DisplayAttribute.Name ?? e.Name,
                    Order = e.DisplayAttribute.GetOrder() ?? 50
                })
                .OrderBy(e => e.Order)
                .ThenBy(e => e.DisplayName)
                .Select(e => new SelectListItem
                {
                    Value = e.Value,
                    Text = e.DisplayName,
                    Selected = e.Value == selectedValueName
                });

            selectItems.AddRange(entries);

            return selectItems;
        }

        public static IEnumerable<string> GetNamesForEnum(Type enumType, object selectedValue)
        {
            if (enumType.IsGenericType &&
               enumType.GetGenericTypeDefinition() == typeof(Nullable<>))
            {
                enumType = enumType.GetGenericArguments()[0];
            }

            var enumEntryNames = Enum.GetNames(enumType);
            var entries = enumEntryNames
                .Select(n => new
                {
                    Name = n,
                    DisplayAttribute = enumType
                        .GetField(n)
                        .GetCustomAttributes(typeof(DisplayAttribute), false)
                        .OfType<DisplayAttribute>()
                        .SingleOrDefault() ?? new DisplayAttribute()
                })
                .Select(e => new
                {
                    Value = e.Name,
                    DisplayName = e.DisplayAttribute.Name ?? e.Name,
                    Order = e.DisplayAttribute.GetOrder() ?? 50
                })
                .OrderBy(e => e.Order)
                .ThenBy(e => e.DisplayName)
                .Select(e => e.Value);
            return entries;
        }

        static string PlaceholderName(MemberExpression memberExpression)
        {
            var placeholderName = memberExpression.Member
                .GetCustomAttributes(typeof(DisplayAttribute), true)
                .Cast<DisplayAttribute>()
                .Select(a => a.Prompt)
                .FirstOrDefault();
            return placeholderName;
        }

        static void AddPlaceHolderToSelectItems(string placeholderName, IList<SelectListItem> selectList)
        {
            if (!selectList.Where(i => i.Text == string.Empty).Any())
                selectList.Insert(0, new SelectListItem { Selected = false, Text = placeholderName, Value = string.Empty });

            if (!selectList.Any() || selectList[0].Text != string.Empty) return;

            selectList[0].Value = "";
            selectList[0].Text = placeholderName;
       }

        static IDictionary<string, object> ApplyHtmlAttributes(IDictionary<string, object> htmlAttributes, string placeholderName)
        {
            if (!string.IsNullOrEmpty(placeholderName))
            {
                if (htmlAttributes == null)
                {
                    htmlAttributes = new Dictionary<string, object>();
                }

                if(!htmlAttributes.ContainsKey("class"))
                    htmlAttributes.Add("class", "placeholder");
                else
                {
                    htmlAttributes["class"] += " placeholder";
                }
            }
            return htmlAttributes;
        }
    }
}