Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

MVC model Null on post when using Partial view

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

enter image description here

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

enter image description here

like image 274
Phil Murray Avatar asked Feb 05 '14 10:02

Phil Murray


People also ask

Can we use model in partial view?

To create a partial view, right click on Shared folder -> select Add -> click on View.. Note: If the partial view will be shared with multiple views, then create it in the Shared folder; otherwise you can create the partial view in the same folder where it is going to be used.

How does a partial view support a model?

It helps us to reduce code duplication. In other word a partial view enables us to render a view within the parent view. The partial view is instantiated with its own copy of a ViewDataDictionary object which is available with the parent view so that partial view can access the data of the parent view.

How do you deal with null models?

In order to simplify your code you should utilize the Null Object pattern. Instead of using null to represent a non existing value, you use an object initialized to empty/meaningless values. This way you do not need to check in dozens of places for nulls and get NullReferenceExpections in case you miss it.


Video Answer


2 Answers

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();
}
like image 192
John H Avatar answered Oct 12 '22 11:10

John H


Binder can't bind to list of objects if it is returned like this. Yes, partial is your problem. You need to specify a number within your form for ID's.

Do something like this:

 // pseudocode
@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">
                    for(int i; i<Model.Count; i++)
                    { 
                        <h2>@g.Name</h2>
                        @Html.HiddenFor(d => Model[i].Id)
                        @Html.HiddenFor(d => Model[i].Name) 
                        @Html.HiddenFor(d => Model[i].SortOrder)

                        @Html.DisplayTextFor(d => Model[i].Name)
                        @Html.EditorFor(d => Model[i].StaticValue)   
                        @Html.ValidationMessageFor(d => Model[i].StaticValue)  
                        <hr />
                    }

                <input type="submit" class="button" value="Save Changes"/>

            </div>
        </div>    
    }
</div>

See more details about binding to a list in Haack's blog

like image 44
trailmax Avatar answered Oct 12 '22 11:10

trailmax