According to the seminal Scott Hanselman article on the complexities of the ASP.NET Wire Format for Model Binding to Arrays, Lists, Collections, Dictionaries:
We read in the properties by looking for
parameterName[index].PropertyName
The index must be zero-based and unbroke
So this HTML:
<input type="text" name="People[0].FirstName" value="George" />
<input type="text" name="People[1].FirstName" value="Abraham" />
<input type="text" name="People[2].FirstName" value="Thomas" />
Which will post like this:
However, if I load a new person into my model over AJAX, I lose the context for building that person into the model and get the following output:
<input type="text" name="FirstName" value="New" />
Which won't get picked up by the model binder.
Q: How can I preserve the expression tree when dynamically adding new elements over AJAX?
Model: /Model/Person.cs
public class PersonViewModel
{
public List<Person> People { get; set; }
}
public class Person
{
public String FirstName { get; set; }
public String LastName { get; set; }
}
Controller: Controllers/PersonController.cs
[HttpGet]
public ActionResult Index()
{
List<Person> people = new List<Person> {
new Person { FirstName = "George" , LastName = "Washington"},
new Person { FirstName = "Abraham" , LastName = "Lincoln"},
new Person { FirstName = "Thomas" , LastName = "Jefferson"},
};
PersonViewModel model = new PersonViewModel() {People = people};
return View(model);
}
[HttpPost]
public ActionResult Index(PersonViewModel model)
{
return View(model);
}
public ActionResult AddPerson(String first, String last)
{
Person newPerson = new Person { FirstName = first, LastName = last };
return PartialView("~/Views/Person/EditorTemplates/Person.cshtml", newPerson);
}
View: Views/Person/Index.cshtml
@model PersonViewModel
@using (Html.BeginForm()) {
<table id="table">
<thead>
<tr>
<th>@Html.DisplayNameFor(model => model.People.First().FirstName)</th>
<th>@Html.DisplayNameFor(model => model.People.First().LastName)</th>
</tr>
</thead>
<tbody>
@for (int i = 0; i < Model.People.Count; i++)
{
@Html.EditorFor(model => model.People[i])
}
</tbody>
</table>
<input type="button" value="Add Person" id="add"/>
<input type="submit" value="Save" />
}
<script type="text/javascript">
$("#add").click(function() {
var url = "@Url.Action("AddPerson")?" + $.param({ first: "", last: "" });
$.ajax({
type: "GET",
url: url,
success: function(data) {
$("#table tbody").append(data);
}
});
});
</script>
View: Views/Person/EditorTemplates/Person.cshtml
@model Person
<tr>
<td>@Html.EditorFor(model => model.FirstName)</td>
<td>@Html.EditorFor(model => model.LastName)</td>
</tr>
NOTE: There are other complexities when deleting an item that I'm not looking to address here per se. I'd just like to add an element and know that it belongs in a nested context alongside other properties.
You can install the Html.BeginCollectionItem
utility like this:
PM> Install-Package BeginCollectionItem
Then wrap your collection item partial view like this:
@model Person
<tr>
@using (Html.BeginCollectionItem("people"))
{
<td>@Html.EditorFor(model => model.FirstName)</td>
<td>@Html.EditorFor(model => model.LastName)</td>
}
</tr>
Which will generate a GUID-driven collection like this:
<tr>
<input type="hidden" name="people.index" autocomplete="off"
value="132bfe2c-75e2-4f17-b54b-07e011971d78">
<td><input class="text-box single-line" type="text" value="Abraham"
id="people_132bfe2c-75e2-4f17-b54b-07e011971d78__FirstName"
name="people[132bfe2c-75e2-4f17-b54b-07e011971d78].FirstName"></td>
<td><input class="text-box single-line" type="text" value="Lincoln"
id="people_132bfe2c-75e2-4f17-b54b-07e011971d78__LastName"
name="people[132bfe2c-75e2-4f17-b54b-07e011971d78].LastName"></td>
</tr>
Now we get posted form data that look like this:
This leverages the DefaultModelBinder which allows for Non-Sequential Indices as explained by Phil Haack:
The good news is that by introducing an extra hidden input, you can allow for arbitrary indices. Just provide a hidden input with the
.Index
suffix for each item we need to bind to the list. The name of each of these hidden inputs is the same, which will give the model binder a nice collection of indices to look for when binding to the list.
Right away, your model should build just fine, but you'll also be able to add and remove items as well.
Further Reading
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With