Search code examples
c#asp.net-mvcmodel-bindingmvc-editor-templates

Model Binding on Save isn't returning full List of Nested Objects, maximum of one


Model Binding on Save isn't returning full List of Nested Objects, maximum of one

Trying to understand EditorTemplates, here is a simple example that isn't working at the moment. The Model isn't returning the full IList of items, it seems to be returning 1 object each time when there should be 2 in my example.

When I Edit ModelA, the example is:

- Name: Model A
    List of Model Bs:
        - Name: Model B1
            List of Model Cs:
                - Name: Model C1
                - Name: Model C2
        - Name: Model B2

This list shows up properly during Edit, but upon Save, some of the information is "lost", added (not found) below.

- Name: Model A (in Model)
    List of Model Bs:
        - Name: Model B1 (in Model)
            List of Model Cs:
                - Name: Model C1 (in Model)
                - Name: Model C2 (not found)
        - Name: Model B2 (not found)

Basically, on Save, the Model is not returning any more than one object from a list.

Classes:

BaseObject:

public class BaseObject
{
    [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    [Key]
    public Guid Oid { get; set; }

}

ModelA, ModelB and ModelC:

public class ModelA : BaseObject
{
    public string Name { get; set; }
    public virtual IList<ModelB> ModelBs { get; set; }
}

public class ModelB : BaseObject
{
    public string Name { get; set; }

    public Guid? ModelAID { get; set; }

    [ForeignKey("ModelAID")]
    public virtual ModelA ModelA { get; set; }

    public virtual IList<ModelC> ModelCs { get; set; }
}

public class ModelC : BaseObject
{
    public string Name { get; set; }

    public Guid? ModelBID { get; set; }

    [ForeignKey("ModelBID")]
    public virtual ModelB ModelB { get; set; }
}

Basic Controller for ModelA, here is the Edit:

public ActionResult Edit(Guid? id)
{
    if (id == null)
    {
        return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
    }
    ModelA modelA = db.ModelAs.Find(id);
    if (modelA == null)
    {
        return HttpNotFound();
    }
    return View(modelA);
}

[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Edit(ModelA modelA)
//Removed: [Bind(Include = "Oid,Name,DateCreated,DateUpdated,DateDeleted,IsDeleted")] 
{
    if (ModelState.IsValid) //Break point to review "modelA"
    {
        db.Entry(modelA).State = EntityState.Modified;
        db.SaveChanges();
        return RedirectToAction("Index");
    }
    return View(modelA);
}

Views:

Edit.cshtml (for ModelA):

@model x.Models.Nesting.ModelA

@using (Html.BeginForm())
{
    @Html.AntiForgeryToken()
    @Html.EditorForModel(Model)
}

Views in Views/Shared/EditorTemplates:

ModelA.cshtml:

@model x.Models.Nesting.ModelA

<div class="form-horizontal">
    <h4>ModelA</h4>
    <hr />
    @Html.ValidationSummary(true, "", new { @class = "text-danger" })
    @Html.HiddenFor(model => model.Oid)

    <div class="form-group">
        @Html.LabelFor(model => model.Name, htmlAttributes: new { @class = "control-label col-md-2" })
        <div class="col-md-10">
            @Html.EditorFor(model => model.Name, new { htmlAttributes = new { @class = "form-control" } })
            @Html.ValidationMessageFor(model => model.Name, "", new { @class = "text-danger" })
        </div>
    </div>

    <div class="form-group">
        @Html.LabelFor(model => model.ModelBs, htmlAttributes: new { @class = "control-label col-md-2" })
        <div class="col-md-10">

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

            @* Tried the code below as well *@
            @*
            @for (var i = 0; i < Model.ModelBs.Count(); i++)
            {
                @Html.EditorFor(m => m.ModelBs[i])
            }
            *@

        </div>
    </div>

    <div class="form-group">
        <div class="col-md-offset-2 col-md-10">
            <input type="submit" value="Save" class="btn btn-default" />
        </div>
    </div>
</div>

ModelB.cshtml:

@model x.Models.Nesting.ModelB
@using (Html.BeginForm())
{
    <div class="form-horizontal">
        @Html.ValidationSummary(true, "", new { @class = "text-danger" })
        @Html.HiddenFor(model => model.Oid)
        @Html.HiddenFor(model => model.ModelAID)

        <div class="form-group">
            @Html.LabelFor(model => model.Name, htmlAttributes: new { @class = "control-label col-md-2" })
            <div class="col-md-10">
                @Html.EditorFor(model => model.Name, new { htmlAttributes = new { @class = "form-control" } })
                @Html.ValidationMessageFor(model => model.Name, "", new { @class = "text-danger" })
            </div>
        </div>

        <div class="form-group">
            @Html.LabelFor(model => model.ModelCs, htmlAttributes: new { @class = "control-label col-md-2" })
            <div class="col-md-10">

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

                @* Tried the code below as well *@
                @*
                @foreach(var item in Model.ModelCs)
                {
                    @Html.EditorFor(x => item)
                }
                *@

            </div>
        </div>

    </div>
}

ModelC.cshtml:

@model x.Models.Nesting.ModelC

@using (Html.BeginForm())
{
    <div class="form-horizontal">
        @Html.ValidationSummary(true, "", new { @class = "text-danger" })
        @Html.HiddenFor(model => model.Oid)
        @Html.HiddenFor(model => model.ModelBID)

        <div class="form-group">
            @Html.LabelFor(model => model.Name, htmlAttributes: new { @class = "control-label col-md-2" })
            <div class="col-md-10">
                @Html.EditorFor(model => model.Name, new { htmlAttributes = new { @class = "form-control" } })
                @Html.ValidationMessageFor(model => model.Name, "", new { @class = "text-danger" })
            </div>
        </div>

    </div>
}

As an example from the HTML (when using @Html.EditorFor(model => model.ModelBs)), the binding for both ModelB objects look look like they would be bound properly (but it only returns the first object):

Model B1:

<input 
    data-val="true" data-val-required="The Oid field is required." 
    id="ModelBs_f1aa613f-96f4-427f-a568-c70556ad2117__Oid" 
    name="ModelBs[f1aa613f-96f4-427f-a568-c70556ad2117].Oid" 
    type="hidden" 
    value="7e99950b-62c9-e711-afd4-7cb0c2b5b934">

<input 
    id="ModelBs_f1aa613f-96f4-427f-a568-c70556ad2117__ModelAID" 
    name="ModelBs[f1aa613f-96f4-427f-a568-c70556ad2117].ModelAID" 
    type="hidden" 
    value="7e99950b-62c9-e711-afd4-7cb0c2b5b935">

<input 
    class="form-control text-box single-line valid" 
    id="ModelBs_f1aa613f-96f4-427f-a568-c70556ad2117__Name" 
    name="ModelBs[f1aa613f-96f4-427f-a568-c70556ad2117].Name" 
    type="text" 
    value="Model B1" aria-invalid="false">

Model B2:

<input 
    data-val="true" data-val-required="The Oid field is required." 
    id="ModelBs_128318da-af85-46a5-bc0d-71361610d989__Oid" 
    name="ModelBs[128318da-af85-46a5-bc0d-71361610d989].Oid" 
    type="hidden" 
    value="7e99950b-62c9-e711-afd4-7cb0c2b5b936">

<input 
    id="ModelBs_128318da-af85-46a5-bc0d-71361610d989__ModelAID" 
    name="ModelBs[128318da-af85-46a5-bc0d-71361610d989].ModelAID" 
    type="hidden" 
    value="7e99950b-62c9-e711-afd4-7cb0c2b5b935">

<input 
    class="form-control text-box single-line valid" 
    id="ModelBs_128318da-af85-46a5-bc0d-71361610d989__Name" 
    name="ModelBs[128318da-af85-46a5-bc0d-71361610d989].Name" 
    type="text" 
    value="Model B2" aria-invalid="false">

When using the for loop in ModelA, looping ModelB (@for (var i = 0; i < Model.ModelBs.Count(); i++) { @Html.EditorFor(m => m.ModelBs[i]) }), the Indexes are Numbers, but still don't return full Models.

Model B1:

<input 
    data-val="true" data-val-required="The Oid field is required." 
    id="ModelBs_0__Oid" name="ModelBs[0].Oid" 
    type="hidden" 
    value="7e99950b-62c9-e711-afd4-7cb0c2b5b934">

<input 
    id="ModelBs_0__ModelAID" 
    name="ModelBs[0].ModelAID" 
    type="hidden" 
    value="7e99950b-62c9-e711-afd4-7cb0c2b5b935">

<input 
    class="form-control text-box single-line valid" 
    id="ModelBs_0__Name" name="ModelBs[0].Name" 
    type="text" 
    value="Model B1" aria-invalid="false">

Model B2:

<input 
    data-val="true" data-val-required="The Oid field is required." 
    id="ModelBs_1__Oid" name="ModelBs[1].Oid" 
    type="hidden" 
    value="7e99950b-62c9-e711-afd4-7cb0c2b5b936">

<input 
    id="ModelBs_1__ModelAID" name="ModelBs[1].ModelAID" 
    type="hidden" 
    value="7e99950b-62c9-e711-afd4-7cb0c2b5b935">

<input 
    class="form-control text-box single-line valid" 
    id="ModelBs_1__Name" name="ModelBs[1].Name" 
    type="text" 
    value="Model B 2" aria-invalid="false">

POST Form Data:

@Html.EditorFor(...) POST (didn't update ModelBs list of ModelCs):

__RequestVerificationToken:1PfXHdYtb5eE-j6g4DWBEZiRa0trOL8UvYGKVjL0pxR1qOjQE52be7UB14VaIJRpp5UA1Iz9WXt4g_7LKixKhK7ah7Hjp6hOLmLa1m7XavI1
Oid:7e99950b-62c9-e711-afd4-7cb0c2b5b935
Name:Model A1
ModelBs.index:4a481093-9bdd-43ae-b84b-144c576ff346
ModelBs[4a481093-9bdd-43ae-b84b-144c576ff346].Oid:7e99950b-62c9-e711-afd4-7cb0c2b5b934
ModelBs[4a481093-9bdd-43ae-b84b-144c576ff346].ModelAID:7e99950b-62c9-e711-afd4-7cb0c2b5b935
ModelBs[4a481093-9bdd-43ae-b84b-144c576ff346].Name:Model B1
ModelBs[4a481093-9bdd-43ae-b84b-144c576ff346].ModelCs[0].Oid:7e99950b-62c9-e711-afd4-7cb0c2b5b936
ModelBs[4a481093-9bdd-43ae-b84b-144c576ff346].ModelCs[0].ModelBID:7e99950b-62c9-e711-afd4-7cb0c2b5b934
ModelBs[4a481093-9bdd-43ae-b84b-144c576ff346].ModelCs[0].Name:Model C 1
ModelBs.index:a1dd130d-a1f7-47ed-90a2-28055a960c9b

For loop POST:

__RequestVerificationToken:x9hpnm-c1g0Cm9gTnSRjCFIVflziqXqiO3iFkzVpMc33gnNlBoDsvwBHMmRT38sWTCGrFSqCqzcuuBZdXLsXTgX1EbkqUSqPuAtwUrR1XXA1
Oid:7e99950b-62c9-e711-afd4-7cb0c2b5b935
Name:Model A1
ModelBs[0].Oid:7e99950b-62c9-e711-afd4-7cb0c2b5b934
ModelBs[0].ModelAID:7e99950b-62c9-e711-afd4-7cb0c2b5b935
ModelBs[0].Name:Model B1
ModelBs[0].ModelCs[0].Oid:7e99950b-62c9-e711-afd4-7cb0c2b5b936
ModelBs[0].ModelCs[0].ModelBID:7e99950b-62c9-e711-afd4-7cb0c2b5b934
ModelBs[0].ModelCs[0].Name:Model C 1

EditorFor has ModelBs.index twice (hopeful this leads to a solution), but not in the For. Something getting cut off> (I double and triple checked that I wasn't missing something further down)

FireFox POST data, looks like the Indexes of 0 and 1 are working to some extent:

__RequestVerificationToken  qlrlO7Z0_byGZYLGJ6Tbx5Fzmpd0dd6b-JPac4V-f1U-17v06OQr27dYZPh_VmRI3X4nGj7ZAOHtBdERnuZscJlNlgoHAqdeXaNQN04e2qE1
Oid 7e99950b-62c9-e711-afd4-7cb0c2b5b935
Name    Model+A1
ModelBs.index   […]
0   3d003e30-d350-4fb0-becd-f65207b033c4
1   be4e35c3-3fee-4c7f-9ee7-580b6e0c8169
ModelBs[3d003e30-d350-4fb0-becd-f65207b033c4].Oid   7e99950b-62c9-e711-afd4-7cb0c2b5b934
ModelBs[3d003e30-d350-4fb0-becd-f65207b033c4].ModelAID  7e99950b-62c9-e711-afd4-7cb0c2b5b935
ModelBs[3d003e30-d350-4fb0-becd-f65207b033c4].Name  Model+B1
ModelBs[3d003e30-d350-4fb0-becd-f65207b033c4].ModelCs[0].Oid    7e99950b-62c9-e711-afd4-7cb0c2b5b936
ModelBs[3d003e30-d350-4fb0-becd-f65207b033c4].ModelCs[0].ModelBID   7e99950b-62c9-e711-afd4-7cb0c2b5b934
ModelBs[3d003e30-d350-4fb0-becd-f65207b033c4].ModelCs[0].Name   Model+C+1

... the mystery continues


Solution

  • The EditorTemplate files (cshtml) for ModelB and ModelC had @using (Html.BeginForm()) { ... }, removing this resolved the issue, allowing the POST to contain all the information. Kept the BeginForm on ModelA, that is required.

    I figured it out when reviewing the POST information, it showed a(n) .index for the second ModelB, but no associated data, it also has a form tag directly below, everyone knows that many form tags isn't good.

    Updated cshtml

    ModelB.cshtml:

    @model x.Models.Nesting.ModelB
    
    <div class="form-horizontal">
    
        @Html.ValidationSummary(true, "", new { @class = "text-danger" })
        @Html.HiddenFor(model => model.Oid)
        @Html.HiddenFor(model => model.ModelAID)
    
        <div class="form-group">
            @Html.LabelFor(model => model.Name, htmlAttributes: new { @class = "control-label col-md-2" })
            <div class="col-md-10">
                @Html.EditorFor(model => model.Name, new { htmlAttributes = new { @class = "form-control" } })
                @Html.ValidationMessageFor(model => model.Name, "", new { @class = "text-danger" })
            </div>
        </div>
    
        <div class="form-group">
            @Html.LabelFor(model => model.ModelCs, htmlAttributes: new { @class = "control-label col-md-2" })
            <div class="col-md-10">
    
                @Html.EditorFor(model => model.ModelCs)
    
                @* Tried the code below as well *@
                @*
                @foreach(var item in Model.ModelCs)
                {
                    @Html.EditorFor(x => item)
                }
                *@
    
            </div>
        </div>
    
    </div>
    

    ModelC.cshtml:

    @model x.Models.Nesting.ModelC
    
    <div class="form-horizontal">
    
        @Html.ValidationSummary(true, "", new { @class = "text-danger" })
        @Html.HiddenFor(model => model.Oid)
        @Html.HiddenFor(model => model.ModelBID)
    
        <div class="form-group">
            @Html.LabelFor(model => model.Name, htmlAttributes: new { @class = "control-label col-md-2" })
            <div class="col-md-10">
                @Html.EditorFor(model => model.Name, new { htmlAttributes = new { @class = "form-control" } })
                @Html.ValidationMessageFor(model => model.Name, "", new { @class = "text-danger" })
            </div>
        </div>
    
    </div>