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