Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

One EditorTemplate for all DropDownLists in ASP.Net MVC

0.00/5 (No votes)
16 Jan 2012 1  
Get the DropDownList items at OnResultExecuting

Introduction

Every ASP.Net MVC programmer knows the need of simplifying the DropDownList when send the ViewModel to the corresponding View.

This is how i do it and there are many other examples to automate this boring task.

Background

Nearly every Edit ViewModel needs at least one DropDownList to handle the ForeignKey in the Model or Data Base.

Almost all of the ASP MVC programmer uses the Html.DropDownList(...) or Html.DropDownListFor(...) helper methods. And to fill this DropDownList they pull the items from Data Store in the Controller and put them in ViewData or ViewBag then use these values in the helper method.

But now I'll use one EditorTemplate for all the DropDownLists in my application.

The code Sample

This is what i'm trying to make:

    public class PersonEditor
    {
        public int Id { get; set; }
        public string Name { get; set; }

        [DropDown("GetServices", 1)]
        public int? ServiceId { get; set; }
    } 

Then i need my Controller to handle this navigational property for me just before the View Executing and pass any parameters that i added "Like number one in this example".

Something like this :

    [FillDropDowns]
    public ActionResult Edit()
    {
        var viewModel = new PersonEditor() {ServiceId = 3};
        return View(viewModel);
    } 

As it clear we need Attribute to decorate navigational property with it and we need also the ActionFilter which will execute before the View rendering to fill the items in the ViewData dictionary.

Lets suppose that this's our simple DropDownListServiceLayer:

public class DropDownListService
{
..........
    public IEnumerable<SelectListItem> GetServices (int typeId)
    {
        return context.Services
            .Where(m => m.Type == typeId)
            .Select(m => new SelectListItem {Text = m.Name, Value = m.Id.ToString()});
    }
} 

Then we need to call GetServices method before the View Rendring and store the result in some ViewData key to retrieve it in the View.

Lets go ahead :).
The DropDownAttribute

The code :

[AttributeUsage(AttributeTargets.Property, AllowMultiple = true, Inherited = false)]
public class DropDownAttribute : UIHintAttribute
{
    private readonly Type _serviceType;
    private readonly string _methodName;
    private readonly object[] _arguments;


        public DropDownAttribute(string methodName, params object[] arguments)
            : this(methodName, "DropDown", typeof(DropDownListService), arguments)
        {
        }

        public DropDownAttribute(string methodName, string templateName, Type serviceType, params object[] arguments)
            : base(templateName)
        {
            _serviceType = serviceType;
            _comboBoxServiceMethods = methodName;
            _arguments = arguments;
        }


    public IEnumerable<SelectListItem> GetMethodResult()
    {
        if (_serviceType == null)
            throw new NoNullAllowedException("Service class type is needed.");
        try
        {

            var serviceInstance = Activator.CreateInstance(_serviceType);
            var methodInfo = _serviceType.GetMethod(_methodName);
            return methodInfo.Invoke(serviceInstance, _arguments) as IEnumerable<SelectListItem>;
        }
        catch (Exception)
        {
            throw;
        }
    }
} 

As it clear this attribute inherit from "UIHintAttribute" to get the name of Editor template. And we need the Service type which will provide us with IEnumerable<SelectListItem> and there is an overridden constructor for default Service Type.

Then we need the method name and the parameters which it will take and therefor i added the params keyword to pass as needed parameters to this method.

The GetMethodResult method will be called from the ActionFilter to execute it and get the result.

The ActionFilter :

The Code :

public class FillDropDowns : ActionFilterAttribute
{
    public override void OnResultExecuting(ResultExecutingContext filterContext)
    {
        var viewModel = filterContext.Controller.ViewData.Model;
        if (viewModel != null)
            setLists(viewModel.GetType(), filterContext.Controller.ViewData);

        base.OnResultExecuting(filterContext);
    }

    private static void setLists(Type viewModelType, IDictionary<string, object> viewData)
    {
        foreach (var property in viewModelType.GetProperties())
        {
            if (!(property.PropertyType.IsClass && !(property.PropertyType == typeof(string))))
            {
                var att = (DropDownAttribute)GetCustomAttribute(property, typeof(DropDownAttribute));
                if (att != null)
                {
                    var viewDataKey = "DDKey_" + property.Name;
                    viewData[viewDataKey] = viewData[viewDataKey] ?? att.GetMethodResult();
                }
            }
            else
            {
                setLists(property.PropertyType, viewData);
            }
        }
    }
} 

The ActionFilter will look for any properties decorated with DropDownAttribute in the ViewModel and if the ViewData dictionary doesn't have this lists already the we'll call GetMethodResult and store this list in the ViewData with corresponding Key.

May you notice that i added "DDKey_" as prefix for this key, that's because ASP.Net MVC 3 has an issue when set a value in ViewData with the same name of your Property name.

One another thing that if you've nested ViewModels then the recursive will loop for them all and fill its Navigational properties too.

Editor Template :

Add new view in the EditorTemplates folder to be the editor for all DropDown navigational properties. And as we decorate our property with UIHint Attribute the EditorTemplate file should named "DropDown" and here is it's simple code:

 
@model object
@Html.DropDownListFor(m => m, Html.GetAutomatedList(m => m).SetSelected(Model))

Read the next section to know about "GetAutomatedList" and "SetSelected" helper methods,

Html Extensions Helpers :

Finally we just need two Helpers method to get the work done:

public static class HtmlExtensions
{
    public static IEnumerable<SelectListItem> SetSelected(this IEnumerable<SelectListItem> selectList, object selectedValue)
    {
        selectList = selectList ?? new List<SelectListItem>();
        if (selectedValue == null)
            return selectList;
        var vlaue = selectedValue.ToString();
        return selectList
            .Select(m => new SelectListItem
                                {
                                    Selected = string.Equals(vlaue, m.Value),
                                    Text = m.Text,
                                    Value = m.Value
                                });
    }


    public static IEnumerable<SelectListItem> GetAutomatedList<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper,
                                                                                    Expression<Func<TModel, TProperty>> expression)
    {
        var metadata = ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData);
        return ((IEnumerable<SelectListItem>) htmlHelper.ViewData["DDKey_" + metadata.PropertyName]);
    }
} 
The GetAutomatedList extension method will get the needed list from ViewData by the ViewModel Property name.

And the other method SetSelected just to set the Selected property to the SelectListItem.



Finally:

This's my first article and as you just discovered my language isn't so good but i hope that the idea is clear and helpful.
Thanks for your time.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here