0
votes

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?

2

2 Answers

1
votes

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"

0
votes

I've now solved this, but ultimately had to just add several hidden fields and rebuild the model by calling back to my data source after posting the data to my receiving action method.

I did run into another issue in my Preferences.cshtml view since I was trying to parse apart my collection in order to display specific collection items in separately-grouped areas in the interface. The @Html.EditorFor helper method caused the auto-generated form IDs to stomp on each other, since I was calling @Html.EditorFor on the collection multiple times from a specific loop. I was able to work around this by switching from Html.EditorFor to Html.RenderPartial and using Steve Sanderson's BeginCollectionItem helper, described here:

http://blog.stevensanderson.com/2010/01/28/editing-a-variable-length-list-aspnet-mvc-2-style/

Feel free to contact me directly if you have any questions or would like to see how I accomplished this.

Thanks so much for your help!