My goal was to be able to render my view models using a standard Object.cshtml
editor and display template. In order to do so I needed to be able to always call Html.Editor(propertyName)
or Html.Display(propertyName)
in order to render the HTML element.
There were plenty of resources for creating custom attributes and custom editor templates. However I ran into a wall when creating the DropDownListAttribute
and template since I needed to obtain the values for the select list as well as set the selected item based on the current instance of the view model.
Here is the reference I used to create the custom ModelMetadataProvider
and custom attributes.
Here is the problem I ran into with getting the current instance of the view model.
It turns out using reflection here to access the view model is not safe. Depending on how you iterate over the property metadata the view model can sometimes be null.
Here is my implementation using the resource referenced above. This is a simple version that only uses a List<string>
for the DropDownList
. Using this example it would be trivial to use dictionaries and set the text/value properties:
View Model:
public class PhoneNumberViewModel
{
// ... normal properties and attributes
[DropDownList("ContactTypes")] // <-- This is the important one!
public string ContactType { get; set; }
public static IEnumerable<string> ContactTypes
{
get
{
return new List<string>()
{
"Home", "Work", "Mobile"
}
}
}
}
DropDownListAttribute Implementation
public class DropDownListAttribute : MetadataAttribute
{
private string _items;
public DropDownListAttribute(string items)
{
_items = items;
}
public override void Process(System.Web.Mvc.ModelMetadata modelMetaData)
{
modelMetaData.AdditionalValues.Add("Items", _items);
modelMetaData.TemplateHint = TemplateHints.DropDownList; // Just a const string
}
}
Custom HTML Helper used to render elements that need access to the view model instance
@Html.ContextualEditor(propertyName)
ContextualEditor HTML Helper Implementation
public static MvcHtmlString ContextualEditor(this HtmlHelper html, string expression)
{
var model = html.ViewData.Model;
var propertyMetadata = html.ViewData.ModelMetadata.Properties.Where(x => x.PropertyName == expression).Single();
switch (propertyMetadata.TemplateHint)
{
case TemplateHints.DropDownList:
var items = (IEnumerable<string>)model.GetType().GetProperty((string)propertyMetadata.AdditionalValues["Items"]).GetValue(model, null);
var selectedValue = (string)model.GetType().GetProperty(expression).GetValue(model, null);
var selectList = new TagBuilder("select");
selectList.MergeAttribute("id", expression);
selectList.MergeAttribute("name", expression);
var options = new StringBuilder();
foreach (var item in items)
{
var option = new TagBuilder("option");
option.InnerHtml = item;
if (item == selectedValue)
option.MergeAttribute("selected", "selected");
options.Append(option.ToString());
}
selectList.InnerHtml = options.ToString();
return MvcHtmlString.Create(selectList.ToString());
default:
return System.Web.Mvc.Html.EditorExtensions.Editor(html, expression);
}
}
Please let me know what you think of the implementation. I'd be interested to know if anyone has found a more elegant way of achieving this result.