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.
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:
To understand why this works, we can take a look at the rendered HTML sent back to the client for our form:
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:{
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);
}
}
[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.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);
}
[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.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);
}
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.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!" />
}
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!<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>
Sign up here with your email
ConversionConversion EmoticonEmoticon