Nick Riggs, Web Developer

Making stuff up about web development since last week.

22 October 2014

Subscribe to our RSS feed

Posted in ASP.NET December 31, 2009

Use ASP.NET MVC’s EditorFor to render a drop down list from a view model. Then use a custom model binder to bind the selection back to the view model

I’m a fan of view models. I’m also a big fan of the new display and editor templates in ASP.NET MVC. However I found there isn’t a default EditorFor implementation for rendering drop down lists. I ended up having to create my own editor template and model binder.

First, I decided I wanted my view model property to hold both the list of potential items and the selected item. The SelectListItem object that ships with MVC made this easy. Here is the view model:

public class BestPictureViewModel
{
    public IEnumerable<SelectListItem> Movies { get; private set; }

    public BestPictureViewModel()
    {
        //Fill the list will all the potential selections.
        Movies = new[] {
            new SelectListItem() { Value = "1", Text = "Slumdog Millionaire" },
            new SelectListItem() { Value = "2", Text = "The Curious Case of Benjamin Button" },
            new SelectListItem() { Value = "3", Text = "Frost/Nixon" },
            new SelectListItem() { Value = "4", Text = "Milk" },
            new SelectListItem() { Value = "5", Text = "The Reader" },
        };
    }
}

Let’s quickly create a controller and action that wires this view model up to a view:

public class HomeController : Controller
{
    public ActionResult Index()
    {
        return View(new BestPictureViewModel());
    }
}

And now the very simple view:

<asp:Content ContentPlaceHolderID="MainContent" runat="server">
    <%= Html.EditorFor(m => m) %>
</asp:Content>

The only property in this ViewModel is a list of movies which we intend on rendering using the EditorFor html helper. However EditorFor doesn’t have a default rendering for IEnumerable<SelectListItem> – so the results are rather lack luster:

Lack-Luster

So let’s teach EditorFor how to render our list. First, we create a new partial view for our editor template: List.ascx

List-Editor-Template

The view should be strongly typed to IEnumerable<SelectListItem> and we use the Html DropDownListFor helper to do the actual rendering.

<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<IEnumerable<SelectListItem>>" %>
<%= Html.DropDownListFor(m => m, Model) %>

That still isn’t enough. We need to give the EditorFor helper a hint to render our list property using the new List partial view. We do that using the aptly named UIHint attribute on the view model property:

[UIHint("List")]
public IEnumerable<SelectListItem> Movies { get; private set; }

The results are much better now!

Better-Results

There is still a problem. Usually you want to post a view model back to the server and optimally we use the view model in the action’s signature instead of relying on the form collection:

public ActionResult Save(BestPictureViewModel viewmodel)
{
    return View();
}

Let’s update our view to call this action:

<% using (Html.BeginForm("Save", "Home"))
   { %>
<%= Html.EditorFor(m => m)%>
<input type="submit" value="Save" />
<% } %>

When the site runs we have a form with a submit button. The submit button posts the form and our Save action gets invoked. The viewmodel parameter has our view model with all the items, however the item that was selected (I selected Benjamin Button) isn’t marked as such in the view model:

Not-Selected

When the form posted, it sent only the value of “2” in the request for the variable “Movies”. The MVC default binder doesn’t know how to bind that to our property of type IEnumerable<SelectListItem>. So we have to teach it.

Create a new model binder that inherits from the DefaultModelBinder:

public class MyModelBinder : DefaultModelBinder
{
    protected override void SetProperty(ControllerContext controllerContext, ModelBindingContext bindingContext, System.ComponentModel.PropertyDescriptor propertyDescriptor, object value)
    {
        if (propertyDescriptor.PropertyType.IsAssignableFrom(typeof(IEnumerable<SelectListItem>)))
        {
            var valueKey = string.IsNullOrEmpty(bindingContext.ModelName)
                ? propertyDescriptor.Name
                : string.Format("{0}.{1}", bindingContext.ModelName, propertyDescriptor.Name);

            bindingContext.ModelState[valueKey].Errors.Clear();

            var listItemValue = bindingContext.ValueProvider.GetValue(valueKey).AttemptedValue;
            var items = propertyDescriptor.GetValue(bindingContext.Model) as IEnumerable<SelectListItem>;

            items.Where(i => i.Value == listItemValue).First().Selected = true;

            return;
        }

        base.SetProperty(controllerContext, bindingContext, propertyDescriptor, value);
    }
}

Our new model binder looks for properties that are being bound who’s type is IEnumerable<SelectListItem>. When it finds one, it extracts the posted value, finds it in the list items, and marks it as selected.

Next, in the Global.asax replace the default model binder with your own:

protected void Application_Start()
{
    AreaRegistration.RegisterAllAreas();
    RegisterRoutes(RouteTable.Routes);
    ModelBinders.Binders.DefaultBinder = new MyModelBinder();
}

Now when we run the application, choose Mr. Button and post the form again. Our view model will reflect our selection:

Selected

Download Source Code