Search code examples
asp.net-mvcasp.net-ajaxunobtrusive-ajax

Ajax.BeginForm Post not able to bind a list of objects within a PartialView


I am attempting to set up a PartialView that displays a list of objects, which contain a boolean property that the end user will check off. Upon submitting, the PartialView should perform an AJAX POST in order to pass that list of objects to another PartialViewResult and display another PartialView on the same page.

However, my controller is getting a null value for that list. How Do I fix this?

Simple Example:

View:

<div id="divNumOne">
     <!-- Where the first PartialView is Displayed -->
</div>
<div id="divNumTwo">
     <!-- Where the second PartialView should be Displayed -->
</div>

PartialViewOne:

@model MyApplicationName.Models.SearchList

<script src="~/Scripts/jquery-3.3.1.js"></script>
<script src="~/Scripts/jquery.unobtrusive-ajax.js"></script>

@{using (Ajax.BeginForm("PartialView2", "ControllerName", null, new AjaxOptions()
{
     HttpMethod = "POST",
     UpdateTargetId = "divNumTwo",
     InsertionMode = InsertionMode.Replace
}, new
{
     id = "partialViewOneSubmitForm"
}))
{
     @Html.AntiForgeryToken()
     <table>
          <thead>
               <tr>
                    <th>Select</th>
                    <th>ColumnOne</th>
                    <th>ColumnTwo</th>
                    <!-- etc. ... -->
               </tr>
          </thead>
          <tbody>
          @foreach (var item in Model.SearchResultList)
          {
               <tr>
                    <td>
                         @Html.CheckBoxFor(modelItem => item.Select)
                    </td>
                    <td>
                         @Html.DisplayFor(modelItem => item.ColumnOne)
                         @Html.HiddenFor(modelItem => item.ColumnOne)
                    </td>
                    <td>
                         @Html.DisplayFor(modelItem => item.ColumnTwo)
                         @Html.HiddenFor(modelItem => item.ColumnTwo)
                    </td>
                    <!-- etc. ... -->
               </tr>
          }
          </tbody>

<!-- etc. ... -->

<button type="submit" class="btn btn-outline-primary" style="font-weight:500;">Lock in</button>

Model:

public class SearchList
{
     public List<SearchResult> SearchResultList { get; set; }
}

public class SearchResult
{
     public bool Select { get; set; }
     public string ColumnOne { get; set; }
     public string ColumnTwo { get; set; }
     // etc. ...
}

Solution

  • MVC is very particular about its model binding with lists. It uses the variable names passed in the lambda expression to set as the name attribute on each form element, and then tries to match those against the model when passed back to the controller. If you inspect each element, you'll probably see name="item.Select", name="item.ColumnOne", and name="item.ColumnTwo" for every item on the list. This means the controller can't disinguish between them, so no binding is done.

    The fix: use a for loop instead of a foreach loop.

    @for (var i = 0; i < Model.SearchResultList.Count; i++) // might need to be .Length or .Count() depending on the type of SearchResultList
    {
        <tr>
            <td>
                @Html.CheckBoxFor(modelItem => Model.SearchResultList[i].Select)
            </td>
            <td>
                @Html.DisplayFor(modelItem => Model.SearchResultList[i].ColumnOne)
                @Html.HiddenFor(modelItem => Model.SearchResultList[i].ColumnOne)
            </td>
            <td>
                @Html.DisplayFor(modelItem => Model.SearchResultList[i].ColumnTwo)
                @Html.HiddenFor(modelItem => Model.SearchResultList[i].ColumnTwo)
            </td>
            <!-- etc. ... -->
        </tr>
    }
    

    This allows the controller to correctly model bind on POST.