Model Binding to List of Objects in ASP.NET MVC
Showing a single record for editing is quite common and the default model
binding of ASP.NET MVC takes care of mapping the form fields to the model
properties. However, sometimes you need to edit multiple records. For example,
you may want to display an editable grid to the end user filled with existing
data. The user can edit the values from multiple rows and hit Save in an attempt
to save the data. In this case multiple model objects are being submitted to the
action method. The single record editing works on the assumption that form field
names from the view match the corresponding model property names. However, when
multiple model objects are submitted this assumption is no longer valid.
Luckily, by tweaking the form field names you can get this to work as expected.
Let's see how.
Begin by creating a new ASP.NET MVC Application. Then right click on the
Models folder and add an ADO.NET entity framework data model to it. Configure
the model to use Customers table of the Northwind database. The following figure
shows this model:
Then add HomeController in the Controllers folder. Modify the default Index()
action method as shown below:
public ActionResult Index()
{
NorthwindEntities db=new NorthwindEntities();
var query = from c in db.Customers
where c.Country=="UK"
orderby c.CustomerID
select c;
return View(query.ToList());
}
The Index() action method simply selects all the customers from UK and passes
them to the Index view as a List of Customer entities.
Then right click on the Index() action method and add Index view. The Index
view is where you need to follow certain naming convention to get the desired
results:
@model List<ModelBindingToListDemo.Models.Customer>
...
<h1>List of Customers</h1>
@using (Html.BeginForm("Index", "Home", FormMethod.Post))
{
<table border="1" cellpadding="6">
@for (int i = 0; i < Model.Count;i++ )
{
<tr>
<td>@Html.TextBox("customers[" + @i + "].CustomerID",
Model[i].CustomerID,
new { @readonly = "readonly" })</td>
<td>@Html.TextBox("customers[" + @i + "].CompanyName",
Model[i].CompanyName)</td>
<td>@Html.TextBox("customers[" + @i + "].ContactName",
Model[i].ContactName)</td>
<td>@Html.TextBox("customers[" + @i + "].Country",
Model[i].Country)</td>
</tr>
}
<tr>
<td colspan="4">
<input type="submit" value="Submit" />
</td>
</tr>
</table>
}
...
Notice the markup shown in the bold letters. The code is basically generating
names for the textboxes matching the following convention:
customers[n].<Model_Property_Name>
Where n is an index starting from 0 and Model_Property_Name is the name of
the properties such as CustomerID, CompanyName, ContactName and Country. For the
sake of simplicity the above code uses only four properties form the Customer
model class. So, all the textboxes having same index are considered as "one
record". This naming convention is required to successfully bind data to the
model as you will see later.
The following figures shows how the view looks like in the browser:
To see how the textbox names are being generated view the HTML source in the
browser.
The above <form> submits the data to Index() method using post method. To
handle this data write the second version of Index() action method as follows:
[HttpPost]
public ActionResult Index(List<Customer> customers)
{
NorthwindEntities db=new NorthwindEntities();
foreach (Customer cust in customers)
{
Customer existing = db.Customers.Find(cust.CustomerID);
existing.CompanyName = cust.CompanyName;
existing.ContactName = cust.ContactName;
existing.Country = cust.Country;
}
db.SaveChanges();
return View();
}
The overloaded Index() method takes a parameter - List of Customer entities.
Recollect that this parameter name - customers - is what you used in the view
markup earlier. Due the naming conventions followed the model binding framework
of ASP.NET MVC transforms the form field values into a generic List of Customer
objects. Once received you simply iterate through the List and modify the
existing Customer with the new one. You can also put some logic to detect
whether a record was really changed or not. Once all the rows are modified
SaveChanges() is called to save the changes.
As mentioned earlier the naming convention requires that the index start at 0
and then sequentially increment for each record. If you try changing the start
index to say 10, the model binding will fail to bind the data. What if you don't
want to start the index from 0? For example, imagine a case where you are
removing some row using client side script. In such cases the there might be
"gaps" in between various index values. To overcome this situation you can
follow an alternate naming convention:
@using (Html.BeginForm("Index", "Home", FormMethod.Post))
{
<table border="1" cellpadding="6">
@for (int i = 0; i < Model.Count;i++ )
{
<tr>
<td>
@Html.Hidden("customers.Index", (@i + 10))
@Html.TextBox("customers[" + (@i + 10) + "].CustomerID",
Model[i].CustomerID, new { @readonly = "readonly" })
</td>
<td>@Html.TextBox("customers[" + (@i + 10) + "].CompanyName",
Model[i].CompanyName)</td>
<td>@Html.TextBox("customers[" + (@i + 10) + "].ContactName",
Model[i].ContactName)</td>
<td>@Html.TextBox("customers[" + (@i + 10) + "].Country",
Model[i].Country)</td>
</tr>
}
<tr>
...
}
Notice the above markup carefully. Each table row now has a hidden form
field. The name of the hidden form field is customers.Index and its value is set
to some arbitrary index (i + 10 in this case). Then all the textboxes are
assigned names of the form:
customers[<arbitrary_index>].<model_property_name>
In this case all the textboxes having same index as specified by the hidden
field are considered as "one record". In this case the index need not be a
number. It can be a string also. Again, recollect that "customers" in the above
markup is the name of the parameter of the Index() method.
That's it! Run the application and test if it works as expected.