MVC binding to model with list property ignores ot

2019-01-17 00:55发布

问题:

I have a basic ViewModel with a property that is a List of complex types. When binding, I seem to be stuck with getting either the list of values, OR the other model properties depending on the posted values (i.e. the view arrangement).

The view model:

public class MyViewModel
{
    public int Id { get; set; }
    public string Property1 { get; set; }
    public string Property2 { get; set; }

    public List<MyDataItem> Data { get; set; }
}

public class MyDataItem
{
    public int Id { get; set; }
    public int ParentId { get; set; }
    public string Name { get; set; }
    public string Value { get; set; }
}

The controller actions:

    public ActionResult MyForm()
    {
        MyViewModel model = new MyViewModel();

        model.Id = 1;
        model.Data = new List<MyDataItem>() 
        { 
            new MyDataItem{ Id = 1, ParentId = 1, Name = "MyListItem1", Value = "SomeValue"}
        }; 

        return View(model);
    }

    [HttpPost]
    public ActionResult MyForm(MyViewModel model)
    {
        //...

        return View(model);
    }

Here is the basic view (without the list mark-up)

@using (Html.BeginForm()) {
    @Html.ValidationSummary(true)
    <fieldset>
        <legend>My View Model</legend>

        @Html.HiddenFor(model => model.Id)

        <div class="editor-label">
            @Html.LabelFor(model => model.Property1)
        </div>
        <div class="editor-field">
            @Html.EditorFor(model => model.Property1)
            @Html.ValidationMessageFor(model => model.Property1)
        </div>

        <div class="editor-label">
            @Html.LabelFor(model => model.Property2)
        </div>
        <div class="editor-field">
            @Html.EditorFor(model => model.Property2)
            @Html.ValidationMessageFor(model => model.Property2)
        </div>

        <p>
            <input type="submit" value="Save" />
        </p>
    </fieldset>
}

When posted back to the controller, I get the 2 property values and a null value for the 'Data' property as expected.

If I add the mark-up for the List as follows (based on the information in this Scott Hanselman post and Phil Haack's post):

<div class="editor-field">
@for (int i = 0; i < Model.Data.Count(); i++)
{
    MyDataItem data = Model.Data[i];
    @Html.Hidden("model.Data[" + i + "].Id", data.Id)
    @Html.Hidden("model.Data[" + i + "].ParentId", data.ParentId)
    @Html.Hidden("model.Data[" + i + "].Name", data.Name)
    @Html.TextBox("model.Data[" + i + "].Value", data.Value)
}
</div>

The 'Data' property of the model is successfully bound but the other properties are null.

The form values posted are as follows:

Id=1&Property1=test1&Property2=test2&model.Data%5B0%5D.Id=1&model.Data%5B0%5D.ParentId=1&model.Data%5B0%5D.Name=MyListItem1&model.Data%5B0%5D.Value=SomeValue

Is there a way to get both sets of properties populated or am I just missing something obvious?

EDIT:

For those of you who are curious. Based on the answer from MartinHN, the original generated mark-up was:

<div class="editor-field">
    <input id="model_Data_0__Id" name="model.Data[0].Id" type="hidden" value="1" />
    <input id="model_Data_0__ParentId" name="model.Data[0].ParentId" type="hidden" value="1" />
    <input id="model_Data_0__Name" name="model.Data[0].Name" type="hidden" value="MyListItem1" />
    <input id="model_Data_0__Value" name="model.Data[0].Value" type="text" value="SomeValue" />        
</div>

The new generated mark-up is:

<div class="editor-field">
    <input id="Data_0__Id" data-val="true" name="Data[0].Id" type="hidden" value="1" data-val-number="The field Id must be a number." data-val-required="The Id field is required." />
    <input id="Data_0__ParentId" name="Data[0].ParentId" type="hidden" value="1"  data-val="true" data-val-number="The field ParentId must be a number." data-val-required="The ParentId field is required." />
    <input id="Data_0__Name" name="Data[0].Name" type="hidden" value="MyListItem1" />
    <input id="Data_0__Value" name="Data[0].Value" type="text" value="SomeValue" />        
</div>

Which results in the following posted values:

Id=1&Property1=test1&Property2=test2&Data%5B0%5D.Id=1&Data%5B0%5D.ParentId=1&Data%5B0%5D.Name=MyListItem1&Data%5B0%5D.Value=SomeValue

Notice there's no 'model.' in the name and posted values...

回答1:

Try to change the code for the Data collection to this, and let MVC take care of the naming:

<div class="editor-field">
@for (int i = 0; i < Model.Data.Count(); i++)
{
    @Html.HiddenFor(m => m.Data[i].Id)
    @Html.HiddenFor(m => m.Data[i].ParentId)
    @Html.HiddenFor(m => m.Data[i].Name)
    @Html.TextBoxFor(m => m.Data[i].Value)
}
</div>


回答2:

Alternatively you could have created an EditorTemplate for your nested ViewModel as follows.

@model MyDataItem
@Html.HiddenFor(model => model.Id)
@Html.HiddenFor(model => model.ParentId)
@Html.HiddenFor(model => model.Name)
@Html.TextBoxFor(model => model.Value)

Create a folder named 'EditorTemplates' in your 'Shared' folder and save the above as 'MyDataItem.cshtml'.

Then in your View, just call the following instead of the foreach loop:

@Html.EditorFor(model => model.Data)

Feels a bit less hackier IMO :)



回答3:

Just my 2 cents but something worth noting with this issue - the data member in the View Model must be defined as a public property for the postback model binding to work. I had a very similar problem to the above but used a public data member in my View Model. The same HTML is generated as shown above and all looks well but the model binder threw back an empty collection. Worth watching for...