Reputation: 35801
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">×</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?
Upvotes: 5
Views: 8136
Reputation: 1579
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"
}))
Upvotes: 1
Reputation: 35801
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
Upvotes: 4