5

I'm building an ASP.NET MVC 5 app using Visual Studio 2015. The search works fine on the first try, but then if I click any of the page numbers in the MVC PagedList component, it throws an Internal Server Error. Here's the AJAX form; note that it passes the data received from the search to a partial view:

@using (Ajax.BeginForm("SearchCustomers", "Permits",
new AjaxOptions
{
    UpdateTargetId = "targetElement",
    OnSuccess = "onAjaxSuccess",
    OnFailure = "onAjaxFailure"
},
new { @class = "form-horizontal form-small", role = "form", id="customerSearchForm" }))
{
    @Html.AntiForgeryToken()
    <div class="modal-header">
        <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
        <h4>Customer Search</h4>
    </div>
    <div class="modal-body">
        @Html.ValidationSummary(true, "", new { @class = "text-danger" })
        <div class="form-group-sm clearfix">
            @Html.LabelFor(m => m.SearchVm.SearchCustomerNameNumber, new { @class = "control-label col-xs-5 col-md-5" })
            <div class="col-xs-5 col-md-5">
                <div class="input-group">
                    @Html.EditorFor(m => m.SearchVm.SearchCustomerNameNumber, new {htmlAttributes = new {@class = "form-control"}})
                    <span class="input-group-btn">
                        <button type="submit" class="btn btn-custom-success btn-sm btn-custom-sm small-box-shadow btn-block">
                            Search
                            <i class="fa fa-search fa-lg" aria-hidden="true"></i>
                        </button>
                    </span>
                </div>
                @Html.ValidationMessageFor(m => m.SearchVm.SearchCustomerNameNumber, "", new { @class = "text-danger" })
            </div>
        </div>
        <div class="modal-search" id="targetElement">
            @Html.Partial("_PermitsCustomerList", Model.SearchVm.Customers)
        </div>
    </div>
}

In the _PermitsCustomerList partial view, I have the following:

@using PagedList
@using PagedList.Mvc
@model IPagedList<MyProject.ViewModels.CustomerViewModel>
@if (Model != null && Model.Count > 0)
{
    <div class="panel panel-default data-grid data-grid-wide">
        <table class="table table-hover table-striped table-bordered table-responsive">
            <tr>
                <th>
                    Customer #
                </th>
                <th>
                    Customer Name
                </th>
            </tr>
            @foreach (var item in Model)
            {
                <tr>
                    <td>
                        @Html.DisplayFor(modelItem => item.Customer_NO)
                    </td>
                    <td>
                        @Html.DisplayFor(modelItem => item.Customer_Name)
                    </td>
                </tr>
            }
        </table>
        <div id="contentPager">
            @Html.PagedListPager(Model, page => Url.Action("SearchCustomers", "Permits", 
               new { page }), 
               PagedListRenderOptions.EnableUnobtrusiveAjaxReplacing( new AjaxOptions()
       {
           HttpMethod = "POST",
           UpdateTargetId = "targetElement",
           OnSuccess = "onAjaxSuccess",
           OnFailure = "onAjaxFailure"
       }))
        </div>
    </div>
}

And here's the action on the controller:

[HttpPost]
[ValidateAntiForgeryToken]
public PartialViewResult SearchCustomers(PermitsViewModel permitsVm, int? page)
{
    if (string.IsNullOrEmpty(permitsVm.SearchVm.SearchCustomerNameNumber)) return null;
    permitsVm.Page = page;
    int number;
    var list = int.TryParse(permitsVm.SearchVm.SearchCustomerNameNumber, out number) 
       ? CustomerDataService.SearchCustomerByNumber(number) 
       : CustomerDataService.SearchCustomerByName(permitsVm.SearchVm.SearchCustomerNameNumber);

    return PartialView("_PermitsCreateCustomerList", list.ToPagedList(permitsVm.Page ?? 1, 10));
}

Here are the success and failure callback functions:

function onAjaxFailure(xhr, status, error) {
    $("#targetElement").html("<strong>An error occurred retrieving data:" + error + "<br/>.</strong>");
}
function onAjaxSuccess(data, status, xhr) {
    if (!$.trim(data)) {
        $("#targetElement").html("<div class='text-center'><strong>No results found for search.</strong></div>");
    }
}

I looked at this example: MVC 4 Using Paged List in a partial View and added the PagedListRenderOptions.EnableUnobtrusiveAjaxReplacing but something is still missing.

When I view the console panel in Chrome, it has this error when I click on a page number:

http://localhost:9999/MyProject/Permits/SearchCustomers?page=2 500 (Internal Server Error)

What am I doing wrong when trying to do an AJAX call with the PagedList component?

2 Answers 2

4

After lots of trial-and-error (mostly error!), the answer was to not use a view model field for the search.

Here's the new search field in the main view:

@{
    string searchName = ViewBag.SearchName;
}
@Html.EditorFor(x => searchName, new {htmlAttributes = new {@class = "form-control"}})

Then in the action, this change receives the searchName value:

[HttpPost]
[ValidateAntiForgeryToken]
public PartialViewResult CreateSearch(string searchName, int? page)
{
    if (string.IsNullOrEmpty(searchName)) return null;
    int number;
    var list = int.TryParse(searchName, out number) 
        ? CustomerDataService.SearchCustomerByNumber(number) 
        : CustomerDataService.SearchCustomerByName(searchName);
    var permitsVm = new PermitsViewModel 
        {SearchVm = {Customers = list.ToPagedList(page ?? 1, 20)}};
    ViewBag.SearchName = searchName;
    return PartialView("_PermitsCreateCustomerList", permitsVm);
}

Note the ViewBag.SearchName; that will be used to pass the search field value to the partial view.

<div id="contentPager">
    @Html.PagedListPager(Model, page => Url.Action("SearchCustomers", "Permits", 
       new { ViewBag.SearchName, page }), 
       PagedListRenderOptions.EnableUnobtrusiveAjaxReplacing( new AjaxOptions()
{
   HttpMethod = "POST",
   UpdateTargetId = "targetElement",
   OnSuccess = "onAjaxSuccess",
   OnFailure = "onAjaxFailure"
}))
</div>

In the paging mechanism above, we use the ViewBag to pass the search value back to the controller.

Update 1: You also need the following on the main view (the one containing the partial) to ensure the anti-forgery token gets sent when you click the numbers in the paging:

$.ajaxPrefilter(function(options, originalOptions, jqXHR) {
    if (options.type.toUpperCase() === "POST") {
        // We need to add the verificationToken to all POSTs
        var token = $("input[name^=__RequestVerificationToken]").first();
        if (!token.length) return;

        var tokenName = token.attr("name");

        // If the data is JSON, then we need to put the token in the QueryString:
        if (options.contentType.indexOf('application/json') === 0) {
            // Add the token to the URL, because we can't add it to the JSON data:
            options.url += ((options.url.indexOf("?") === -1) ? "?" : "&") + token.serialize();
        } else if (typeof options.data === 'string' && options.data.indexOf(tokenName) === -1) {
            // Append to the data string:
            options.data += (options.data ? "&" : "") + token.serialize();
        }
    }
});

From here: https://gist.github.com/scottrippey/3428114

Update 2: You can use the view model on the controller but have to pass a RouteValueDictionary:

<div id="contentPager">
    @Html.PagedListPager(Model, page => Url.Action("SearchCustomers", "Permits", 
       new RouteValueDictionary()
    {
        { "Page", page},
        { "SearchVm.SearchCustomerNameNumber", Model.SearchVm.SearchCustomerNameNumber }
    }), 
       PagedListRenderOptions.EnableUnobtrusiveAjaxReplacing( new AjaxOptions()
    {
       HttpMethod = "POST",
       UpdateTargetId = "targetElement",
       OnSuccess = "onAjaxSuccess",
       OnFailure = "onAjaxFailure"
    }))
</div>

With this, you'd change the action:

[HttpPost]
[ValidateAntiForgeryToken]
public PartialViewResult CreateSearch(PermitsViewModel permitsVm)
{
    if (string.IsNullOrEmpty(permitsVm.SearchVm.SearchCustomerNameNumber)) return null;
    int number;
    var list = int.TryParse(permitsVm.SearchVm.SearchCustomerNameNumber, out number) 
        ? CustomerDataService.SearchCustomerByNumber(number) 
        : CustomerDataService.SearchCustomerByName(permitsVm.SearchVm.SearchCustomerNameNumber);
    permitsVm.SearchVm.Customers = list.ToPagedList(permitsVm.Page ?? 1, 10);
    return PartialView("_PermitsCreateCustomerList", permitsVm);
}

The complex objects help on the RouteValueDictionary came from here: https://stackoverflow.com/a/23743358/177416

Sign up to request clarification or add additional context in comments.

Comments

1

It looks like you're not passing a required argument to your controller. Change your PagedListPager to this:

@Html.PagedListPager(Model, page => Url.Action("SearchCustomers", "Permits",
new { page = page, permitsVm = Json.Encode(Model)}), 
PagedListRenderOptions.EnableUnobtrusiveAjaxReplacing( new AjaxOptions()
{
   HttpMethod = "POST",
   UpdateTargetId = "targetElement",
   OnSuccess = "onAjaxSuccess",
   OnFailure = "onAjaxFailure"
}))

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.