List item inside model always null on post - Asp.n

2019-07-21 05:07发布

问题:

I am trying to post model which contains List<> item looks like below:

public class AddSubscriptionPlanModel
{
    public AddSubscriptionPlanModel()
    {
        AllFeatures = new List<Feature>();
    }
    public int Id { get; set; }
    public int SubscriptionPlanId { get; set; }
    public int SubscriptionId { get; set; }
    public string Name { get; set; }
    public bool IsActive { get; set; }
    [Display(Name = "No Of Users")]
    public int? NoOfUsers { get; set; }
    [Display(Name = "Duration Type")]
    public DurationType DurationType { get; set; }
    public int Duration { get; set; }
    public decimal Amount { get; set; }
    public int SubscriptionPlanTrailId { get; set; }
    public int FeatureId { get; set; }
    public DateTime CreatedDateUtc { get; set; }
    public DateTime LastUpdatedUtc { get; set; }
    public int CreatedBy { get; set; }
    public int LastUpdatedBy { get; set; }    
    public List<Feature> AllFeatures { get; set; }
}

I am populating all required fields on get method and feeding List from database

public ActionResult Mapping(int id)
{        
    var features = FeatureManager.GetAllFeatures();
    var model = new Ejyle.DevAccelerate.Web.App.Models.SubscriptionPlan.AddSubscriptionPlanModel();

    model.AllFeatures = features;
    model.Id = id;
    model.SubscriptionId = id;
    model.NoOfUsers = 20;
    model.SubscriptionPlanId = 1;
    model.SubscriptionPlanTrailId = 1;
    model.Amount = 1000;
    model.CreatedBy = 1;
    model.LastUpdatedBy = 1;
    model.FeatureId = 1;
    model.Duration = 20;

    return View(model);
}

And my view code looks like below :

@using (Html.BeginForm("Mapping", "SubscriptionPlans", FormMethod.Post))
{
    @Html.HiddenFor(model => model.Id)
    @Html.HiddenFor(model => model.Amount)
    @Html.HiddenFor(model => model.Duration)
    @Html.HiddenFor(model => model.FeatureId)
    @Html.HiddenFor(model => model.CreatedBy)
    @Html.HiddenFor(model => model.LastUpdatedBy)
    @Html.HiddenFor(model => model.NoOfUsers)
    @Html.HiddenFor(model => model.SubscriptionId)
    @Html.HiddenFor(model => model.SubscriptionPlanId)
    @Html.HiddenFor(model => model.SubscriptionPlanTrailId)
    @Html.HiddenFor(model => model.AllFeatures)
    <table class="table table-striped">
        <thead>
            <tr>
                <th>
                    @Html.DisplayNameFor(model => model.Name)
                </th>
                <th>
                    @Html.DisplayNameFor(model => model.IsActive)
                </th>
            </tr>
        </thead>
        <tbody>
            @for (int i=0; i < Model.AllFeatures.Count; i++)
            {
                <tr>
                    @Html.HiddenFor(model => model.AllFeatures[i].Id)
                    @Html.HiddenFor(model => model.AllFeatures[i].Name)
                    @Html.HiddenFor(model => model.AllFeatures[i].IsActive)
                    <td>
                        @Html.TextBoxFor(model => model.AllFeatures[i].Id)
                    </td>
                    <td>
                        @Html.TextBoxFor(model => model.AllFeatures[i].Name)
                    </td>
                    <td>
                        @Html.EditorFor(model => model.AllFeatures[i].IsActive)
                    </td>
                </tr>
            }
        </tbody>
    </table>
    <input type="submit" class="btn btn-success" value="Submit" name="Submit"/>
}

On View, the list items rendered properly and even I am able to edit those fields.
But On post, all properties have the value except AllFeatures list item property which always shows count = 0.

Edit 1: This is how my view looks like while rendering features

Edit 2:

Signature of the method posting to:

[HttpPost]
public ActionResult Mapping(AddSubscriptionPlanModel model)
{       
}

回答1:

But On post, all properties have the value except AllFeatures list item property which always shows count = 0.

AllFeatures is a generic list of Feature. When you do this:

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

The Razor view engine will render this:

<input id="AllFeatures" 
       name="AllFeatures" 
       type="hidden" 
       value="System.Collections.Generic.List`1[Namespace.Feature]">
       <!--Where Namespace is the Namespace where Feature is defined. -->

In other words, HiddenFor calls the ToString() on the item and simply puts that into the input tag's value attribute.

What happens upon POST

When you post the form, the DefaultModelBinder is looking for a property named AllFeatures but of type string so it can assign this to it:

System.Collections.Generic.List`1[Namespace.Feature]

It does not find one since your model does not have AllFeatures of type string, so it simply ignores it and binds all the other properties.

AllFeatures list item property which always shows count = 0.

Yes, it will and this is not the AllFeatures list which you posted but the one from the constructor which clearly is an empty list with a count 0:

public AddSubscriptionPlanModel()
{
    AllFeatures = new List<Feature>();
}

I am not sure why you are sending all the features to the client (browser) and then you need to post it back to the server.

Solution

To fix the issue, simply remove this line:

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

Now it will not cause any confusion during binding and MVC will bind the items in the loop to the AllFeatures property.

In fact, the only code you really need, as far as I can tell from your question is this(I could be wrong if you need the hidden fields for some other reason. But if you just want the user to edit the AllFeatures, you do not need any of the hidden fields):

@for (int i = 0; i < Model.AllFeatures.Count; i++)
{
    <tr>
        <td>
            @Html.TextBoxFor(model => model.AllFeatures[i].Id)
        </td>
        <td>
            @Html.TextBoxFor(model => model.AllFeatures[i].Name)
        </td>
        <td>
            @Html.EditorFor(model => model.AllFeatures[i].IsActive)
        </td>
    </tr>
}


回答2:

I think your issue might be the nested property. You may want to review the posted form and see what it looks like.

Your markup looks correct assuming that the model binding would work for a nested property - which I am not sure it does. (Just to make sure I searched for and found this old Haack post that indicates you're doing it right: Haack Post)

As a workaround (and POC), you can try the following: Add a new parameter to your action for your features, post it like your doing now but not nested (so you can't use the Model For HTML helpers) and then in the controller, you can set it back to your main object's features property.



回答3:

This how to do it, as in the context of binding to a list from a view is incorrect.

View:

@model Testy20161006.Controllers.AddSubscriptionPlanModel
<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Mapping</title>
</head>
<body>
    @*I am using controller home*@
    @using (Html.BeginForm("Mapping", "Home", FormMethod.Post))
    {
        @Html.HiddenFor(model => model.Id)
        @Html.HiddenFor(model => model.Amount)
        @Html.HiddenFor(model => model.Duration)
        @Html.HiddenFor(model => model.FeatureId)
        @Html.HiddenFor(model => model.CreatedBy)
        @Html.HiddenFor(model => model.LastUpdatedBy)
        @Html.HiddenFor(model => model.NoOfUsers)
        @Html.HiddenFor(model => model.SubscriptionId)
        @Html.HiddenFor(model => model.SubscriptionPlanId)
        @Html.HiddenFor(model => model.SubscriptionPlanTrailId)
        @Html.HiddenFor(model => model.AllFeatures)
        <table class="table table-striped">
            <thead>
                <tr>
                    <th>
                        @Html.DisplayNameFor(model => model.Name)
                    </th>
                    <th>
                        @Html.DisplayNameFor(model => model.IsActive)
                    </th>
                </tr>
            </thead>
            <tbody>
                @for (int i = 0; i < Model.AllFeatures.Count; i++)
                {
                    <tr>
                        @*BE SURE to get rid of these*@
                        @*@Html.HiddenFor(model => model.AllFeatures[i].Id)
                            @Html.HiddenFor(model => model.AllFeatures[i].Name)
                            @Html.HiddenFor(model => model.AllFeatures[i].IsActive)*@
                        <td>
                            @Html.TextBoxFor(model => model.AllFeatures[i].Id)
                        </td>
                        <td>
                            @Html.TextBoxFor(model => model.AllFeatures[i].Name)
                        </td>
                        <td>
                            @Html.EditorFor(model => model.AllFeatures[i].IsActive)
                        </td>
                    </tr>
                }
            </tbody>
        </table>
        <input type="submit" class="btn btn-success" value="Submit" name="Submit" />
    }
</body>
</html>

Controller:

[HttpPost]
public ActionResult Mapping(AddSubscriptionPlanModel addSubscriptionModel, FormCollection formCollection)
    {
        var AllFeatures = String.Empty;
        var index = "AllFeatures[";
        foreach (var item in formCollection)
        {
            if (item.ToString().Contains(index))
            {
                AllFeatures += " " + formCollection.GetValues(item.ToString())[0];
            }
        }

        //put breakpoint here to see userValues