Programming Languages Hacks

Importanti regole per linguaggi di programmazione rilevanti come Java, C, C++, C#…

  • Subscribe

  • Lettori

    I miei lettori abituali

  • Twitter

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;
        }

Leave a Reply

You must be logged in to post a comment.