ASP.NET MVC: Dropdown EditorTemplate from ViewModel Instance by Convention
Posted by Ricibald on June 2nd, 2012
Based on separation between ViewModel and View when you want to display a Dropdown you should configure allowedvalues for a certain type inside the ViewModel instead of the Controller or the View.
For this reason, after long searches, I decided to implement my own solution based on convention.
This is the ideally expected result:
public class UserViewModel
{
// the enum type is already handled specializing the string template
public AreaEnumType AreaId { get; set; }
[MultivalueAllowed]
public int? CityId { get; set; }
public IEnumerable<SelectListItem> CityIdAllowedValues()
{
var cities = getCitiesByRegion(this.AreaId);
foreach(var city in cities) {
yield return new SelectListItem { Text = city.Text, Value = city.Id, Selected = this.CityId == city.Id };
}
}
}
In other words:
if you have a property that is to view as a dropdown, just add the attribute MultivalueAllowed and implement an instance method using the convention PropNameAllowedValues: you can also access to every instance property inside it!!!
These are the files to modify to achieve this (I’m using extension methods, reflection, UIHintAttribute, EditorTemplates, Razor):
/********** MultivalueAllowedAttribute.cs **********/
public class MultivalueAllowedAttribute : UIHintAttribute
{
public MultivalueAllowedAttribute(bool allValuesVisibleFromStart = false)
: base(getUiHintFromConfiguration(allValuesVisibleFromStart))
{
}
private static string getUiHintFromConfiguration(bool allValuesVisibleFromStart)
{
return allValuesVisibleFromStart ? "Combobox" : "Dropdown";
}
}
/********** global.asax.cs **********/
protected override void OnApplicationStarted()
{
ModelMetadataProviders.Current = new CustomMetadataProvider();
}
/********** CustomMetadataProvider.cs **********/
namespace MvcExtensions
{
public class CustomMetadataProvider : DataAnnotationsModelMetadataProvider
{
protected override ModelMetadata CreateMetadata(IEnumerable<Attribute> attributes, Type containerType, Func<object> modelAccessor, Type modelType, string propertyName)
{
var modelMetadata = base.CreateMetadata(attributes, containerType, modelAccessor, modelType, propertyName);
// add container instance
if(modelAccessor != null && modelAccessor.Target != null)
{
var containerField = modelAccessor.Target.GetType().GetField("container");
if(containerField != null)
{
var containerValue = containerField.GetValue(modelAccessor.Target);
modelMetadata.SetContainerInstance(containerValue);
}
}
return modelMetadata;
}
}
}
namespace System
{
public static class ModelMetadataExtensions
{
private static void addSafe(ModelMetadata modelMetadata, string key, object value)
{
modelMetadata.AdditionalValues.Add(key, value);
}
private static T getSafe<T>(ModelMetadata modelMetadata, string key)
{
object value;
if(modelMetadata.AdditionalValues.TryGetValue(key, out value))
return (T)value;
return default(T);
}
public static void SetContainerInstance(this ModelMetadata modelMetadata, object value)
{
addSafe(modelMetadata, "containerInstance", value);
}
public static object GetContainerInstance(this ModelMetadata modelMetadata)
{
return getSafe<object>(modelMetadata, "containerInstance");
}
}
}
/********** Views/Shared/EditorTemplates/Dropdown.cshtml **********/
@model System.Object
@if (ViewData.ModelMetadata.DisplayName != null)
{
@Html.LabelForModel()
}
@Html.DropDownListByConvention("")
@Html.Description("", new { @class = "description" })
@Html.ValidationMessage("", null, new { @class = "ym-message" })
/********** HtmlHelperExtensions.cs **********/
public static MvcHtmlString DropDownListByConvention<TModel>(this HtmlHelper<TModel> htmlHelper, string expression)
{
var metadata = ModelMetadata.FromStringExpression(expression, htmlHelper.ViewData);
var containerInstance = metadata.GetContainerInstance();
var containerType = metadata.ContainerType;
var items = containerInstance.GetAllowedValues(containerType, metadata.PropertyName);
if (metadata.IsNullableValueType)
{
items = SingleEmptyItem.Concat(items);
}
return htmlHelper.DropDownList(expression, items);
}
public static IEnumerable<SelectListItem> GetAllowedValues(this object instance, Type instanceType, string propertyName)
{
var allowedValuesMethodName = propertyName + "AllowedValues";
var allowedValuesMethod = instanceType.GetMethod(allowedValuesMethodName, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
if(allowedValuesMethod == null)
{
throw new Exception("Unable to find instance method " + instanceType.Name + "." + allowedValuesMethodName);
}
var result = allowedValuesMethod.Invoke(instance, null);
return (IEnumerable<SelectListItem>)result;
}
July 3rd, 2012 at 8:59
Thank you for the auspicious writeup. It actually used to be a enjoyment account it. Look complicated to far brought agreeable from you! By the way, how could we be in contact?