Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

ASP.NET MVC PagedList using AJAX in partial view

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?

like image 680
Alex Avatar asked Mar 06 '17 18:03

Alex


People also ask

Can you define partial view in MVC?

A partial view is a Razor markup file ( . cshtml ) without an @page directive that renders HTML output within another markup file's rendered output. The term partial view is used when developing either an MVC app, where markup files are called views, or a Razor Pages app, where markup files are called pages.

How do I pass a model into partial view?

To create a partial view, right click on Shared folder -> select Add -> click on View.. Note: If the partial view will be shared with multiple views, then create it in the Shared folder; otherwise you can create the partial view in the same folder where it is going to be used.

Can we use Ajax in MVC?

The MVC Framework contains built-in support for unobtrusive Ajax. You can use the helper methods to define your Ajax features without adding a code throughout all the views. This feature in MVC is based on the jQuery features. To enable the unobtrusive AJAX support in the MVC application, open the Web.

Can partial view have controller?

It does not require to have a controller action method to call it. Partial view data is dependent of parent model. Caching is not possible as it is tightly bound with parent view (controller action method) and parent's model.


2 Answers

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

like image 100
Alex Avatar answered Oct 08 '22 22:10

Alex


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"
}))
like image 41
Billdr Avatar answered Oct 08 '22 21:10

Billdr