Model MVC Null on publication when using the partial view

advertisements

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


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:

  1. 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.
  2. Place your partial view in that folder. Alternatively, put it in the Shared\EditorTemplates folder.
  3. 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();
}