Search code examples
c#asp.net-mvc-3asp.net-mvc-partialviewstrong-typing

Strongly-Typed Model Not Posting from Partial View


I have an MVC3 page comprised of the following components:

ProfileController - Contains all of the actions and loads all of the views below

/Profile/Index - This action method creates an instance of UserViewModel, which contains several properties and nested collections, including one called "PrefPrograms" which is of type "PreferencePrograms," which is defined as:

public class PreferencePrograms : List<PreferenceProgram>

The "Index" action method populates the PrefPrograms collection and the rest of the UserViewModel and passes that model into the strongly typed "Index.cshtml" view.

Index.cshtml - This is a strongly-typed view of type "UserViewModel." It is comprised of multiple partial views. One partial view within this page, named "Preferences.cshtml," provides a formatted display of the PrefPrograms collection. The relevant portion of Index.cshtml is below.

@model ProfilePreferenceCenterProto.Models.UserViewModel

@{ Html.RenderPartial("Preferences", Model); }

Preferences.cshtml - This strongly-typed partial view is loaded by Index.cshtml using Html.RenderPartial (above). Within Preferences.cshtml, I have a Begin.AjaxForm() method for posting to a "PreferenceSubmit" action and the input submit button is at the bottom of this partial view. Within the partial view, I call an @Html.EditorFor() helper to load an Editor Template for each "PreferenceProgramModel" item in the "PrefPrograms" collection.

Here's my problem - all items load correctly (including the partial view and editorfor components), but when I post the form on Preferences.cshtml to the "PreferencesSubmit" action, the model's values are not passed (the model is instantiated, but the values are only initialized - the property and collection values are not actually passed back to the controller).

The Preferences.cshtml Partial View is shown below.

@model ProfilePreferenceCenterProto.Models.UserViewModel

<div id="accordian">
@using(Ajax.BeginForm("PreferencesSubmit", "Profile", new AjaxOptions{ UpdateTargetId = "accordian" })){

  <div id="accordion">

  <ul class="tabs">

  </ul>

  <div class="panes">
  <div> 
  @{
      List<string> AffiliateNames = new List<string>();
      foreach(ProfilePreferenceCenterProto.Models.PreferenceProgramModel list in Model.PrefPrograms)
      {
          AffiliateNames.Add(list.SubcategoryName);
      }

      IEnumerable<string> listNames = AffiliateNames.Distinct();
      int counter = 0;
  }

  @foreach (string AccordionTabName in listNames)
  {
     <h2>@AccordionTabName</h2>
      <div class="pane" @if (counter == 0){ <text>style="display:block;"</text> } >
      <table>
        <tr class="row">
            <th class="name">Subscription</th>
            <th class="icon">Email</th>            
            <th class="icon">SMS</th>
            <th class="icon">Facebook</th>
            <th class="icon">Mail</th>
            <th class="icon">Phone</th>
        </tr>

    @{ 
    counter++;
    var TabPrograms = (from l in Model.PrefPrograms
                       where l.SubcategoryName == @AccordionTabName
                       select l);
    }                              
    @Html.EditorFor(m => TabPrograms)                     
      </table>
      </div>
  }
  </div>

      </div>
    </div>
       <div align="center"><input type="submit" value="Save Preferences" /></div>

}

/Shared/EditorTemplates/PreferenceProgramModel.cshtml - The Editor Template for "PreferenceProgramModel" items, defined as follows:

@model ProfilePreferenceCenterProto.Models.PreferenceProgramModel

<tr class="row">
<td class="name">@Model.ListName</td>                
<td class="icon">
@if (Model.EmailEnabled)
{    
    <a id="@Model.EmailFilterID" href="#" onclick="ImageClick(@Html.IdFor(m => m.EmailStatus));"><img height="25" width="28" src="@Model.Email_Icon_NotSelected" /></a>
}
</td>
<td class="icon">
@if (Model.SMSEnabled)
{
    <a id="@Model.SMSFilterID" href="#"><img height="25" width="28" src="@Model.SMS_Icon_NotSelected" /></a>
}
</td>
<td class="icon">
@if (Model.FBEnabled)
{
    <a id="@Model.FBFilterID" href="#"><img height="25" width="28" src="@Model.FB_Icon_NotSelected" /></a>
}
</td>
<td class="icon">
@if (Model.MailEnabled)
{
    <a id="@Model.MailFilterID" href="#"><img height="25" width="28" src="@Model.Mail_Icon_NotSelected" /></a>
}
</td>
<td class="icon">
@if (Model.PhoneEnabled)
{
    <a id="@Model.PhoneFilterID" href="#"><img height="25" width="28" src="@Model.Phone_Icon_NotSelected" /></a>
}
</td>
</tr>  

@Html.HiddenFor(m => m.EmailStatus)
@Html.HiddenFor(m => m.SMSStatus)
@Html.HiddenFor(m => m.FBStatus)
@Html.HiddenFor(m => m.MailStatus)
@Html.HiddenFor(m => m.PhoneStatus)

<script type="text/javascript">
$(document).ready(function () {

    function ImageClick(Resource) {
        alert(Resource.attr("value"));
        if (Resource.attr("value") != 1) {
            Resource.val("1");
        }
        else {
            Resource.val("2");
        }
        alert(Resource.attr("value"));
    }

    if ("@Model.EmailEnabled" == "True") {
        $("#@Model.EmailFilterID").click(function () {

            ImageClick($("#@Html.IdFor(m => m.EmailStatus)"));
            return false;
        });
    }

    if ("@Model.SMSEnabled" == "True") {
        $("#@Model.SMSFilterID").click(function () {
            ImageClick($("#@Html.IdFor(m => m.SMSStatus)"));
            return false;
        });
    }

    if ("@Model.FBEnabled" == "True") {
        $("#@Model.FBFilterID").click(function () {
            ImageClick($("#@Html.IdFor(m => m.FBStatus)"));
            return false;
        });
    }

    if ("@Model.MailEnabled" == "True") {
        $("#@Model.MailFilterID").click(function () {
            ImageClick($("#@Html.IdFor(m => m.MailStatus)"));
            return false;
        });
    }

    if ("@Model.PhoneEnabled" == "True") {
        $("#@Model.PhoneFilterID").click(function () {
            ImageClick($("#@Html.IdFor(m => m.PhoneStatus)"));
            return false;
        });
    }

});
</script>

The PreferencesSubmit controller action is defined with this signature:

public ActionResult PreferencesSubmit(Models.UserViewModel model)

The rendered form tag from the the page is below:

<form action="/Profile/PreferencesSubmit" data-ajax="true" data-ajax-mode="replace" data-ajax-update="#accordian" id="form0" method="post"> 

Some of the rendered hidden fields are shown below:

<input data-val="true" data-val-number="The field EmailStatus must be a number." data-val-required="The EmailStatus field is required." id="TabPrograms_0__EmailStatus" name="TabPrograms[0].EmailStatus" type="hidden" value="0" />
<input data-val="true" data-val-number="The field SMSStatus must be a number." data-val-required="The SMSStatus field is required." id="TabPrograms_0__SMSStatus" name="TabPrograms[0].SMSStatus" type="hidden" value="0" />
<input data-val="true" data-val-number="The field FBStatus must be a number." data-val-required="The FBStatus field is required." id="TabPrograms_0__FBStatus" name="TabPrograms[0].FBStatus" type="hidden" value="0" />
<input data-val="true" data-val-number="The field MailStatus must be a number." data-val-required="The MailStatus field is required." id="TabPrograms_0__MailStatus" name="TabPrograms[0].MailStatus" type="hidden" value="0" />
<input data-val="true" data-val-number="The field PhoneStatus must be a number." data-val-required="The PhoneStatus field is required." id="TabPrograms_0__PhoneStatus" name="TabPrograms[0].PhoneStatus" type="hidden" value="0" />

I've verified that ModelState.IsValid is true when PreferencesSubmit is called, but the model itself does not contain the actual values from the page. How do I pass my strongly-typed model values from the partial view back to the Action method on submit?


Solution

  • The values submited from form are binded to properties of the model based on the names of the properties of the model and names of the fields in HTML. In your code:

    var TabPrograms = (from l in Model.PrefPrograms
                       where l.SubcategoryName == @AccordionTabName
                       select l);
    }                              
    @Html.EditorFor(m => TabPrograms)    
    

    HtmlHelper does not know what are the names of properties in UserViewModel. It takes the "TabPrograms" local variable name in order to generate the names of fields in HTML like:

    <input name="TabPrograms[0].EmailStatus" data-val="true" data-val-number="The field EmailStatus must be a number." data-val-required="The EmailStatus field is required." id="TabPrograms_0__EmailStatus"  type="hidden" value="0" />
    

    So MVC will try to bind the value from the field above to property named "TabPrograms" in UserViewModel (and it should be a collection of PreferenceProgramModel objects). Do you have "TabPrograms" property in UserViewModel class? If not, you should create it and the data from these fields will be bound to it. Another, better, solution would be to use "PrefPrograms" local variable name instead of "TabPrograms"