Tuesday, 22 May 2012

MVC Model Binding to List of Complex Objects

Recently, I had a requirement to create a form-based partial view in an MVC3 application that permitted a user to select multiple options using standard HTML checkboxes. What this essentially meant was that I needed MVC to automatically bind a complex list of objects to the argument of an action method.

After doing some reading on this, I found that the DefaultModelBinder supports this as long as the form fields are named in such a way that the model can distinguish one complex object from another - this can be achieved by using a unique index when creating the form. The example below shows exactly how this can be done - I'm using MVC3 with the Razor view engine.

Imagine we need to display a list of options to the user, the user can select multiple options from the list and then post the form back to one of your action methods on the server side. In this scenario, we're going to display a list of planes to the user, the user can select their favourite plane(s) and then click a submit button. We'll start by defining our model - a "complex" but self explanatory class called PlaneModel.

public class PlaneModel
{
  public string Manufacturer { get; set; }
  public string Model { get; set; }
  public bool IsFavourite { get; set; }

  public override string ToString()
  {
    return string.Format("{0} - {1}", Manufacturer, Model);
  }
}

For the sake of brevity, I won't use a partial view (but the method should be the same if you want to use a partial view in your case). We'll create a new controller called PlaneController, with one initial action method "Index". In this action method, we'll new-up some instances of PlaneModel, store them in a list-based collection and then pass this collection as a model to a strongly-typed Index view. The Index action method would therefore look like:

[HttpGet]
public ActionResult Index()
{
  var planes = new List<PlaneModel>(); //Model

  planes.Add(new PlaneModel { 
      Manufacturer = "Cessna"
      Model = "C208B Grand Caravan" });
  planes.Add(new PlaneModel { 
      Manufacturer = "Douglas"
      Model = "DC-3" });
  planes.Add(new PlaneModel { 
      Manufacturer = "Piper"
      Model = "J-3 Cub" });
  planes.Add(new PlaneModel { 
      Manufacturer = "Mooney"
      Model = "M20J" });
            
  return View(planes);
}

Notice that the action method maps to our HTTP GET request. So, whilst we're still in our controller, we'll write the POST action. The key thing to remember here is that our post action will accept a list of PlaneModel objects.

[HttpPost]
public ActionResult ProcessFavouritePlanes(List<PlaneModel> model)
{
  foreach (var planeModel in model)
  {
    if (planeModel.IsFavourite)
      Debug.WriteLine("Favourited: {0}", planeModel);
    else
      Debug.WriteLine("Not favourited: {0}", planeModel);
  }
  return View(model);
}

So, all I'm doing in the POST action is iterating through the planes in the model (which will be passed back from the view) - and hopefully the IsFavourite property should have been bound to the correct values that the user selects using checkboxes.

Now onto the important part - the creation of our view. Create a strongly typed Index view (i.e., a generic list of type PlaneModel). If you're using Visual Studio as your IDE, you can right-click within your Index action method and select the option "Add View" - this should bring up a modal dialog. Leave the view name as "Index", check the "Create a strongly-typed view" option and type:

List<PlaneModel>

in the "Model class" text box (note that you will probably need to prefix the PlaneModel class name with its fully qualified namespace as the generic type parameter to List - if you don't do this you'll get a runtime error when navigating to the Index view). You can now click "Add" and the view will get created under the conventional folder structure.

The view logic will be a simple mixture of standard HTML and C# in Razor syntax:

@model List<PlaneModel>
Please select your favourite plane(s):<br />
@using (Html.BeginForm("ProcessFavouritePlanes"
                       "Plane"
                       FormMethod.Post))
{
  for (int i = 0; i < Model.Count; i++)
  {
    @Html.CheckBoxFor(m => m[i].IsFavourite)
    @Model[i].ToString() 
    @Html.HiddenFor(m => m[i].Manufacturer)
    @Html.HiddenFor(m => m[i].Model)
  }
  <input type="submit" value="Go!" />
}

Notice that we're iterating through each PlaneModel object using a C# for-loop. This allows us to use the incrementing index and display each option from the model. Also note the use of the hidden fields for the Manufacturer and Model properties - these are here to ensure that they're passed back to the DefaultModelBinder on the server side - taking these two lines out will mean that we'll get PlaneModel objects with blank values for those two properties when the form is posted to the POST action. You should now be able to test if this is all working by hitting a breakpoint on the POST action, running the application and selecting some options. You'll find that the model binder will automatically bind the selected checkboxes and update the model passed into the action.

To understand why this works, we can take a look at the rendered HTML sent back to the client for our form:

<form action="/Plane/ProcessFavouritePlanes" method="post">
  <input name="[0].IsFavourite" type="checkbox" value="true" />
  <input name="[0].IsFavourite" type="hidden" value="false" />
  Cessna - C208B Grand Caravan 
  <input name="[0].Manufacturer" type="hidden" value="Cessna" />
  <input name="[0].Model" type="hidden" value="C208B Grand Caravan" />
  <br />
  <input name="[1].IsFavourite" type="checkbox" value="true" />
  <input name="[1].IsFavourite" type="hidden" value="false" />
  Douglas - DC-3 
  <input name="[1].Manufacturer" type="hidden" value="Douglas" />
  <input name="[1].Model" type="hidden" value="DC-3" />
  <br />
  <input name="[2].IsFavourite" type="checkbox" value="true" />
  <input name="[2].IsFavourite" type="hidden" value="false" />
  Piper - J-3 Cub 
  <input name="[2].Manufacturer" type="hidden" value="Piper" />
  <input name="[2].Model" type="hidden" value="J-3 Cub" />
  <br />
  <input name="[3].IsFavourite" type="checkbox" value="true" /> 
  <input name="[3].IsFavourite" type="hidden" value="false" />
  Mooney - M20J 
  <input name="[3].Manufacturer" type="hidden" value="Mooney" />
  <input name="[3].Model" type="hidden" value="M20J" />
  <br />
  <input type="submit" value="Go!" />
</form>

Notice how the framework added index prefixes to the name attributes of the input elements. The use of this index-based naming convention for the input elements allows the DefaultModelBinder in MVC to distinguish between each complex object - and therefore seamlessly create a correct representation of our model that is passed to the POST action - very neat!