MVC model Null on post when using Partial view
asp.net-mvc, asp.net-mvc-4, c#
Solution
As you've rightly noted, this is because you're using a partial. This is happening because `Html.Partial` has no idea that it's operating on a collection, so it doesn't generate the names for your form elements with your intention of binding to a collection.
However, the fix in your case appears to be fairly straightforward. Rather than using `Html.Partial`, you can simply change your partial into an `EditorTemplate` and call `Html.EditorFor` on that template instead. `Html.EditorFor` is smart enough to know when it's handling a collection, so it will invoke your template for each item in the collection, generating the correct names on your form.
So to do what you need, follow these steps:
- Create an `EditorTemplates` folder inside your view's current folder (e.g. if your view is `Home\Index.cshtml`, create the folder `Home\EditorTemplates`). The name is important as it follows a convention for finding templates.
- Place your partial view in that folder. Alternatively, put it in the `Shared\EditorTemplates` folder.
- Rename your partial view to `Datapoint.cshtml` (this is important as template names are based on the convention of the type's name).
Now the relevant view code becomes:
// Note: I removed @ from Model here.
@foreach (var g in Model)
{
<h2>@g.Name</h2>
@Html.EditorFor(m => g.DataPoints)
<hr />
}
This ensures the separation of your views, as you had originally intended.
Update per comments
Alright, so as I mentioned below, the problem now is that the model binder has no way of associating a `DataPoint` with the correct `Group`. The simple fix is to change the view code to this:
for (int i = 0; i < Model.Count; i++)
{
<h2>@Model[i].Name</h2>
@Html.EditorFor(m => m[i].DataPoints)
<hr />
}
That will correctly generate the names, and should solve the model binding problem.
OP's addendum
Following John's answer I also included the missing properties on the Group table as HiddenFor's which game me the model back on the post.
@for (int i = 0; i < Model.Count(); i++)
{
@Html.HiddenFor(t => Model[i].ID)
@Html.HiddenFor(t => Model[i].BusinessUnitID)
@Html.HiddenFor(t => Model[i].SortOrder)
@Html.HiddenFor(t => Model[i].Name)
<h2>@Model[i].Name</h2>
@Html.EditorFor(m => Model[i].Datapoints)
<hr />
}
Update 2 - Cleaner solution
My advice for using an `EditorTemplate` for each `DataPoint` also applies to each `Group`. Rather than needing the `for` loop, again sprinkling logic in the view, you can avoid that entirely by setting up an `EditorTemplate` for `Group`. Same steps apply as above in terms of where to put the template.
In this case, the template would be `Group.cshtml`, and would look as follows:
@model Jmp.StaticMeasures.Models.Group
<h2>@Model.Name</h2>
@Html.EditorFor(m => m.DataPoints)
<hr />
As discussed above, this will invoke the template for each item in the collection, which will also generate the correct indices for each `Group`. Your original view can now be simplified to:
@model List<Jmp.StaticMeasures.Models.Group>
@using (Html.BeginForm())
{
// Other markup
@Html.EditorForModel();
}
Problem
I have an MVC controller where the model on the post method always comes back as null. I'm not sure if this is because I am using a partial view within the form. Any idea why the model is not being returned to the controller? Model Loading the model ``` public List<Group> GetStaticMeasures(int businessUnitID) { List<Group> groups = ctx.Groups .Include("Datapoints") .Where(w => w.BusinessUnitID.Equals(businessUnitID)) .OrderBy(o => o.SortOrder).ToList(); groups.ForEach(g => g.Datapoints = g.Datapoints.OrderBy(d => d.SortOrder).ToList()); return groups; } ``` Controller ``` public ActionResult Data() { ViewBag.Notification = string.Empty; if (User.IsInRole(@"xxx\yyyyyy")) { List<Group> dataGroups = ctx.GetStaticMeasures(10); return View(dataGroups); } else { throw new HttpException(403, "You do not have access to the data."); } } [HttpPost] [ValidateAntiForgeryToken] public ActionResult Data(List<Group> model) { ViewBag.Notification = string.Empty; if (User.IsInRole(@"xxx\yyyyyy")) { if (ModelState.IsValid) { ctx.SaveChanges(model); ViewBag.Notification = "Save Successful"; } } else { throw new HttpException(403, "You do not have access to save the data."); } return View(model); } ``` Main view ``` @model List<Jmp.StaticMeasures.Models.Group> <div class="row"> @using (Html.BeginForm()) { @Html.AntiForgeryToken() @Html.ValidationSummary(true) <div class="large-12"> <div class="large-8 large-centered columns panel"> @foreach (var g in @Model) { <h2>@g.Name</h2> foreach (var d in g.Datapoints) { @Html.Partial("Measures", d) } <hr /> } <input type="submit" class="button" value="Save Changes"/> </div> </div> } </div> ``` Partial View ``` @model Jmp.StaticMeasures.Models.Datapoint @Html.HiddenFor(d => d.ID) @Html.HiddenFor(d => d.Name) @Html.HiddenFor(d => d.SortOrder) @Html.DisplayTextFor(d => d.Name) @Html.EditorFor(d => d.StaticValue) @Html.ValidationMessageFor(d => d.StaticValue) ``` Rendered Html showing consecutive IDs