MVC 模型与动态集合的绑定

MVC Model Binding with Dynamic Collection

根据 Scott Hanselman 关于 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

所以这个 HTML:

<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" />

哪个 post 会像这样:

但是,如果我在 AJAX 上将一个新人加载到我的模型中,我将失去将该人构建到模型中的上下文并得到以下输出:

<input type="text" name=<b>"FirstName"</b> value="New" />

不会被模型绑定器拾取。

Q: 如何在 AJAX 上动态添加新元素时保留表达式树?

这是一个 MVCE

型号:/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; }
}

控制器: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);
}

查看: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>

查看:Views/Person/EditorTemplates/Person.cshtml

@model Person

<tr>
    <td>@Html.EditorFor(model => model.FirstName)</td>
    <td>@Html.EditorFor(model => model.LastName)</td>
</tr>

注意:删除项目时还有其他复杂性,我不想在这里解决。我只想添加一个元素并知道它与其他属性一起属于嵌套上下文。

您可以 install the Html.BeginCollectionItem 这样的实用程序:

PM> Install-Package BeginCollectionItem

然后像这样包装您的 collection 项目部分视图:

@model Person
<tr>
    @using (Html.BeginCollectionItem("people"))
    {
        <td>@Html.EditorFor(model => model.FirstName)</td>
        <td>@Html.EditorFor(model => model.LastName)</td>
    }
</tr>

这将生成一个 GUID-driven collection 像这样:

<tr>
    <input type="hidden" name="people.index" autocomplete="off"
                     value="<b><i>132bfe2c-75e2-4f17-b54b-07e011971d78</i></b>">
    <td><input class="text-box single-line" type="text" value="Abraham"
                 id="people_<b><i>132bfe2c-75e2-4f17-b54b-07e011971d78</i></b>__FirstName"
               name="people[<b><i>132bfe2c-75e2-4f17-b54b-07e011971d78</i></b>].FirstName"></td>
    <td><input class="text-box single-line" type="text" value="林肯"
                 id="people_<b><i>132bfe2c-75e2-4f17-b54b-07e011971d78</i></b>__LastName"
               name="people[<b><i>132bfe2c-75e2-4f17-b54b-07e011971d78</i></b>].LastName"></td>
</tr>

现在我们得到如下所示的已发布表单数据:

这利用了 DefaultModelBinder 允许 Non-Sequential 索引 作为 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.

马上,您的模型应该构建得很好,但您也可以添加和删除项目。

进一步阅读