Introduction
I am new to MVC as I just started working on a project using it. I was stuck on this model binding thingy for a couple of days, so just want to share this experience. Hope it will be useful to other newbies like me.
Background
I have this master-detail (one to many) relationship in one view and the detail is represented by HTML table in which user can add/remove the rows as they like. And I want the Add
method in the controller to just accept the model as a parameter. Something like this:

I found this great article here which helped me a lot.
Using the Code
OK, just to make it simple, I will create 2 classes with master-detail relationship in the model class.
I name it AuthorModel
:
public class Author
{
public Author()
{
Books = new List<Book>();
}
public string Name { get; set; }
public List<Book> Books { get; set; }
}
public class Book
{
public string Title { get; set; }
public DateTime PublishedDate { get; set; }
}
Next, we add controller and view into the project:
Controller :
public class AuthorController : Controller
{
//
// GET: /Author/
public ActionResult Index()
{
var model = new Author();
return View(model);
}
}
View :
@using MvcModelBinding.Models
@model MvcModelBinding.Models.Author
@{
ViewBag.Title = "Index";
Layout = "~/Views/Shared/_Layout.cshtml";
}
<h2>
Index</h2>
@using (Html.BeginForm())
{
@Html.ValidationSummary(true)
<fieldset>
<legend>Author</legend>
<div class="editor-label">
@Html.LabelFor(model => model.Name)
</div>
<div class="editor-field">
@Html.EditorFor(model => model.Name)
@Html.ValidationMessageFor(model => model.Name)
</div>
<div>
<input id="btnAddBook" type="button"
onclick="addRow();" value="Add Book" />
</div>
<table>
<thead>
<tr>
<td>
Title
</td>
<td>
Published Date
</td>
</tr>
</thead>
<tbody id="tbBooks">
</tbody>
</table>
<p>
<input type="submit" value="Create" />
</p>
</fieldset>
}
<div>
@Html.ActionLink("Back to List", "Index")
</div>
Since the idea is we can add/remove the books in the client side, then only submit it to the server after we are satisfied. The client script looks like this:
<script language="javascript" type="text/javascript">
function addRow() {
@{
Model.Books.Add(new Book());
}
var index = $("#tbBooks").children("tr").length;
var indexCell = "<td style='display:none'><input name='Books.Index'
type='hidden' value='" + index + "' /></td>";
var titleCell = "<td><input id='Books_" + index + "__Title'
name='Books[" + index + "].Title' type='text' value='' /></td>";
var publishedCell = "<td><input id='Books_" + index + "__Title'
name='Books[" + index + "].PublishedDate' type='text' value='' /></td>";
var removeCell = "<td><input id='btnAddBook' type='button'
value='Remove' onclick='removeRow(" + index + ");' /></td>";
var newRow = "<tr id='trBook" + index + "'>" +
indexCell + titleCell + publishedCell + removeCell + "</tr>";
$("#tbBooks").append(newRow);
}
function removeRow(id) {
var controlToBeRemoved = "#trBook" + id;
$(controlToBeRemoved).remove();
}
</script>
Note that the hidden value part below is necessary to allow arbitrary indices.
var indexCell = "<td style='display:none'>
<input name='Books.Index' type='hidden' value='" + index + "' /></td>";
If we didn't add this extra hidden input, removing n index in the collection will also remove the subsequent items; for example: if you have 3 items in the collection, removing item number 2 will also remove item number 3.
Now let us add code in the controller to display the result:
[HttpPost]
public string Index(Author author)
{
var sb = new StringBuilder();
try
{
sb.AppendFormat("Author : {0}", author.Name);
sb.AppendLine("<br />");
sb.AppendLine("--------------------------------");
sb.AppendLine("<br />");
foreach (var book in author.Books)
{
sb.AppendFormat("Title : {0} | Published Date : {1}", book.Title, book.PublishedDate);
sb.AppendLine("<br />");
}
}
catch(Exception ex)
{
throw ex;
}
return sb.ToString();
}
Observe that the controller only accepts Author model as the parameter.
Here are some of the results:

Delete the second item:


That's it! Hope it helps.