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

Original source