MVC4 Complex Type Model is null after post

2020-06-25 04:41发布

问题:

This is my model

public class AdministrationModel
{
  public string FirstName { get; set; }
  public string LastName { get; set; }
  public string EmailAddress { get; set; }
  public bool IsApproved { get; set; }
}

This is my controller

public ActionResult GetTabContent(string id)
{
  switch (id)
  {
   case "tab3":
   model = GetAllUsersInfo();
   viewName = "Administration";
   break;
   }
   return View(viewName);
 }

  private List<AdministrationModel> GetAllUsersInfo()
  {
    List<AdministrationModel> userList = new List<AdministrationModel>();
    foreach (MembershipUser user in Membership.GetAllUsers())
    {
      UserProfile userProfile = UserProfile.GetUserProfile(user.UserName);
      userList.Add(new AdministrationModel { EmailAddress = user.Email,                       IsApproved = user.IsApproved, FirstName = userProfile.FirstName, LastName = userProfile.LastName });
    }

    return userList;
  }

This is my View

@model List<AdminContainerModel>
@using (Html.BeginForm("Administration", "Account"))
{
  <fieldset>
    <div>
      @foreach (AdministrationModel AM in Model)
      {
        <div>
         <div class="colFull">@Html.DisplayFor(modelItem => AM.FirstName)</div>
         <div class="colFull">@Html.DisplayFor(modelItem => AM.LastName)</div>
         <div class="colFull">@Html.DisplayFor(modelItem => AM.EmailAddress)</div>
         <div class="colPartial"><input type="checkbox" checked="@AM.IsApproved"/>            </div>
      <div class="clear"></div>
    </div>
  }
</div>
 <input type="submit" value="Update Account" />
 </fieldset>
}

When the user clicks the Update Account button it goes to the controller

  [HttpPost]
  public ActionResult Administration(List<AdministrationModel> model)
  {
     return View();
  }

inside this method, model is always null. however the View that renders everything is perfect and shows what I want it to show. What am I doing wrong?

回答1:

When using collections, in order to correctly process them so they are model-bound on post without any additional leg work, you need to make sure they are indexed correctly, you can do this by using a for loop, something like:

@for (int i = 0; i < Model.Count; i++)
{
    @Html.HiddenFor(m => m[i].FirstName)
    @Html.HiddenFor(m => m[i].LastName)
    @Html.HiddenFor(m => m[i].EmailAddress)
    <div>
        <div class="colFull">@Html.DisplayFor(m => m[i].FirstName)</div>
        <div class="colFull">@Html.DisplayFor(m => m[i].LastName)</div>
        <div class="colFull">@Html.DisplayFor(m => m[i].EmailAddress)</div>
        <div class="colPartial">@Html.CheckBoxFor(m => m[i].IsApproved)</div>
        <div class="clear"></div>
    </div>
}

That should model bind without any other code :)

Edit: Sorry I forgot, displayFors by default don't put the correct properties on for model binding, added hiddenFors for the other fields that don't have an editorFor

Edit2: Based on your other question in the comment, if it was public facing and you didn't want them to change any of the hidden for values using the dev tools, try the following:

Ok so you don't want them to change the hiddenFors, that's fine, but you will need some sort of ID so you know which client is which when the data is posted, I suggest that instead of having these in the above code:

@Html.HiddenFor(m => m[i].FirstName)
@Html.HiddenFor(m => m[i].LastName)
@Html.HiddenFor(m => m[i].EmailAddress)

Replace them with:

@Html.HiddenFor(m => m[i].ClientId)

That way you're not posting back the firstname, lastname or email address, just a reference to the actual client that is ticked, unticked.

To answer your other question in the comment about keeping track of the original values, in your controller method you can just go and get the original values from the database, then here's how you can detect which ones are different, something like:

[HttpPost]
public ActionResult Administration(List<AdministrationModel> model)
{
    var originalMatches = GetAllUsersInfo();

    var differences = (from o in originalMatches
                      join c in model on o.ClientId equals c.ClientId
                      where c.IsApproved != o.IsApproved).ToList()

    return View();
}


回答2:

Your @model directive is incorrect, it should be the fully qualified type name.

In this case:

 @model TheNamespace.AdministrationModel

After your updated question.

Try using:

@Html.EditorFor(Model)

This will call a specialized Editor Template for your list.

http://blogs.msdn.com/b/nunos/archive/2010/02/08/quick-tips-about-asp-net-mvc-editor-templates.aspx



回答3:

If you want to return list items with the model on submit you will need to use a for loop, not a foreach



回答4:

Matty's answer will work.

If you want to avoid having to specify indexes (which wouldn't work if you had an ICollection) you can define a child template as Xander has explained.

The benefit is you still get all your strong typed intellisense in the AdminContainerModel View and MVC hooks the items back in to your List on post back for you out of the box.

Here's an example:

Main Editor template (as Xander said):

    @model IEnumerable<AdminContainerModel>
    @using (Html.BeginForm("Administration", "Account"))
    {
      <fieldset>
        <div>
          @Html.EditorForModel()
        </div?
      </fieldset>
    }

AdminContainerModel Editor template(This is called for each item in your List because you've called @Html.EditorForModel:

@model AdminContainerModel
<div>
    <div class="colFull">@Html.DisplayFor(modelItem => AM.FirstName)</div>
    <div class="colFull">@Html.DisplayFor(modelItem => AM.LastName)</div>
    <div class="colFull">@Html.DisplayFor(modelItem => AM.EmailAddress)</div>
    <div class="colPartial"><input type="checkbox" checked="@AM.IsApproved"/>            
</div>
<div class="clear"></div>