Imagine you have the following types defined.
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.
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.
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 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.
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.
The following template code will display the proper text value, even localized, to the user.
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.
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.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)
No comments:
Post a Comment