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:
<!-- language: lang-html --> <pre><code><input type="text" name=<b>"People[0].FirstName"</b> value="George" /> <input type="text" name=<b>"People[1].FirstName"</b> value="Abraham" /> <input type="text" name=<b>"People[2].FirstName"</b> value="Thomas" /> </code></pre>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:
<!-- language: lang-html --> <pre><code><input type="text" name=<b>"FirstName"</b> value="New" /> </code></pre>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?
Here's an MVCE
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.