MVC Model Binding with Dynamic Collection

强颜欢笑 提交于 2019-12-12 12:02:54

问题


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?

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.


回答1:


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

  • A Partial View passing a collection using the Html.BeginCollectionItem helper
  • Submit same Partial View called multiple times data to controller?
  • Model Binding To A List - Phil Haack


来源:https://stackoverflow.com/questions/40681707/mvc-model-binding-with-dynamic-collection

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!