Working with Enums and Templates In ASP.NET MVC

Tuesday, September 4, 2012

Imagine you have the following types defined.

public class Movie
{
public int Id { get; set; }
public string Title { get; set; }
public MovieGenre Genre { get; set; }
}

public enum MovieGenre
{
Action,
Drama,
Adventure,
Fantasy,
Boring
}

If you want to display MovieGenre, the DisplayFor helpers generally give you what you expect (i.e. @Html.DisplayFor(m=> m.Genre) displays "Adventure"), but EditorFor will only give you a text input because the framework treats an enum the same as a string.

Default EditorFor with enum

If the user knows your enum values they can type in a value and model binding works correctly, but we certainly don't want users to guess the valid values. A better approach would use a drop down list, or a set of radio buttons, to constrain and guide the user's input. There are a few articles on the web showing how to build a drop down list from enums (Stuart Leeks has a good one - Creating a DropDownList helper for enums). I wanted to try a slightly different approach using templates and supporting localization. If you are familiar with templates in ASP.NET MVC, see Brad Wilson's series on templates and metadata).

Template Selection

The first challenge is getting the MVC framework to select a custom template when it sees @Html.EditorFor(m => m.Genre). If you create a view named MovieGenre.cshtml and place it in the ~/Views/Shared/EditorTemplates folder, the MVC runtime will use the template and you can place inside the template any custom markup you want for editing a Genre. However, trying to have a single generic template (Enum.cshtml) for all enums won't work. The MVC framework sees the enum value as a simple primitive and uses the default string template. One solution to change the default framework behavior is to write a custom model metadata provider and implement GetMetadataForProperty to use a template with the name of "Enum" for such models.

public override ModelMetadata GetMetadataForProperty(
Func<object> modelAccessor, Type containerType, string propertyName)
{
var result = _inner.GetMetadataForProperty(modelAccessor, containerType, propertyName);
if (result.TemplateHint == null &&
typeof(Enum).IsAssignableFrom(result.ModelType))
{
result.TemplateHint = "Enum";
}
return result;
}

The "_inner" field represents the default metadata provider. Depending on how you want to setup the runtime, _inner could also be a call to the base class if you derive from CachedDataAnnotationsModelMetadataProvider. Regardless of the approach, the above code will rely on another provider to do the hard work. The code only throws in "Enum" as the template name to use if there is no template name specified and the model type derives from Enum.

The Template

With a custom metadata provider in place the runtime will locate and use an Enum template when a view uses EditorFor against a MovieGenre property.

Editor templates in place and ready!

The implementation of the template is straightforward. Regardless of the type of UI you want, you'll need to use Enum.GetValues to get all the possible values of the enumeration. You can then use the values to build a drop down list, radio buttons, anything you can image. The following code would render a simple drop down list.

@model Enum

@{

var values = Enum.GetValues(ViewData.ModelMetadata.ModelType).Cast<object>()
.Select(v => new SelectListItem
{
Selected = v.Equals(Model),
Text = v.ToString(),
Value = v.ToString()
});
}

@Html.DropDownList("", values)

Localization and Display Name Customization

Things get a little more complicated in the view if you want to customize the text display of an enum value, or if you want to localize the text using resource files. The Display attribute does most of the work, but as far as I can tell the code has to dig out the attribute on its own (there is no help from the MVC metadata provider). For example, take the following definition for MovieGenre.

public enum MovieGenre
{
[Display(ResourceType = typeof(Resources.Translations), Name = "Action")]
Action,

[Display(Name="Drama!")]
Drama,

Adventure,

Fantasy,

Boring
}

The following template code will display the proper text value, even localized, to the user.

@using System.ComponentModel.DataAnnotations
@model Enum

@{

Func<object, string> GetDisplayName = o =>
{
var result = null as string;
var display = o.GetType()
.GetMember(o.ToString()).First()
.GetCustomAttributes(false)
.OfType<DisplayAttribute>()
.LastOrDefault();
if (display != null)
{
result = display.GetName();
}

return result ?? o.ToString();
};

var values = Enum.GetValues(ViewData.ModelMetadata.ModelType).Cast<object>()
.Select(v => new SelectListItem
{
Selected = v.Equals(Model),
Text = GetDisplayName(v),
Value = v.ToString()
});
}

@Html.DropDownList("", values)

I'd want to measure the performance of the template if it is heavily used in a busy application, but the idea here is to use this approach if you want to create some fancy markup in the template. If all you want to do is ultimately call Html.DropDownList, a custom HTML helper approach is simpler.


Comments
gravatar Eric hexter Tuesday, September 4, 2012
Where is the nuget ?
gravatar Rui Jarimba Tuesday, September 4, 2012
Good stuff Scott.

I created some helpers some months ago based on that article you mentioned (Creating a DropDownList helper for enums), with localization support.

For an enum like this one:

[LocalizationEnum(typeof(MyResources))]
public enum WeekDay
{
Sunday,
Monday,
Tuesday,
Wednesday,
Thursday,
Friday,
Saturday
}

HTML Helper is used like this:

@Html.EnumDropDownList<WeekDay>("selectDay", "")


Article can be found here:
ruijarimba.wordpress.com/...


gravatar Michel Tuesday, September 4, 2012
This is much simpler than what I was doing.

But you can use a data annotation to select a Enum.cshtml custom template :

[UIHint("Enum")]
public MovieGenre Genre { get; set; }
gravatar George Mauer Tuesday, September 4, 2012
For use with js templates:

readonly static Regex capitalLetters = new Regex(@"([a-z](?=[A-Z])|[A-Z](?=[A-Z][a-z]))");
public static HtmlString JsonStringKeysEnumOptions<T>(this HtmlHelper html) where T : struct, IConvertible {
Contract.Requires(typeof(T).IsEnum);

var etype = typeof(T);
var dict = etype.GetEnumNames().ToDictionary(k => k, k => capitalLetters.Replace(k, "$1 "));
return new HtmlString(SurgeJson.Serialize(dict));
}
gravatar iresearchpapers.com Saturday, September 15, 2012
To be honest I was searching for this information! Thanks a lot for sharing the script like this
Comments are now closed.
by K. Scott Allen K.Scott Allen
My Pluralsight Courses
The Podcast!