As the number of contacts grows it is overwhelming to see in single page. To address this I am going to add paging to the contact list and only show 10 contacts per page.
To generate a large set of realistic date I am using Mockaroo. They offers data in CSV, Tab-Delimited, SQL, Excel, JSON and DBUnit XML with up to 1,000 rows of data per generation for free. The data Mockaroo is way better than the crazy stuff I was coming up with. If you need more than 1,000 rows at a time they do have a pay option.
For paging I am going to take advantage of the PagedList.Mvc NuGet package. Using the package manager console it can be installed with the Install-Package PagedList.Mvc command as seen in the following screenshot.
To start using PagedList.Mvc I need the following changes to ContactsController. Add new using for PagedList and the index action needs a page parameter. As part of this change I also refactored the contact query to the GetContacts function so the Index action is much less cluttered than before. The function is now shown below but I moved the distinct city query to the GetDistinctCities function.
public ActionResult Index(string filterCity, string filterSearch, int? page) { ViewBag.filterCity = filterCity; ViewBag.filterSearch = filterSearch; ViewBag.city = new SelectList(GetDistinctCities()); var contacts = GetContacts(filterCity, filterSearch) .ToPagedList(page ?? 1, 10); if (Request.IsAjaxRequest()) { return PartialView("_ContactList", contacts); } return View(contacts); } private IQueryable<Contact> GetContacts(string city, string search) { var contacts = from c in db.Contacts select c; if (!string.IsNullOrWhiteSpace(search)) { contacts = contacts.Where(c => c.Name.Contains(search)); } if (!string.IsNullOrWhiteSpace(city)) { contacts = contacts.Where(c => c.City == city); } return contacts.OrderBy(c => c.Name); }
Notice that I am now saving the parameter values of the city and search filters to the ViewBag in the index action which will be used when changing pages when a filter is in play.
In GetContacts I had to add an order by statement to the contacts being returned. This is to make sure the list is always in the same order. It will also be a good spot to add different ordering options in the future.
The last change in the Index action was to take the return value from GetContacts and call ToPagedList on it. This function takes a page number and number of items per page and returns a paged list as the name implies. In my case if page is null I am using page 1 and each page will contain 10 items.
In both Index.cshtml and the partial view _ContactList.cshtml the models need to be changed to PagedList.IPagedList. In addition @Html.DisplayNameFor also needs to be changed from model.Name to model.FirstOrDefault().Name. The last step is to add the paged list control to the bottom of the view. Here is the code from the contact list partial view.
@using PagedList.Mvc @model PagedList.IPagedList<Contacts.Models.Contact> <p> @Html.ActionLink("Create New", "Create") </p> <table class="table"> <tr> <th> @Html.DisplayNameFor(model => model.FirstOrDefault().Name) </th> <th> @Html.DisplayNameFor(model => model.FirstOrDefault().Address) </th> <th> @Html.DisplayNameFor(model => model.FirstOrDefault().City) </th> <th> @Html.DisplayNameFor(model => model.FirstOrDefault().State) </th> <th> @Html.DisplayNameFor(model => model.FirstOrDefault().ZipCode) </th> <th> @Html.DisplayNameFor(model => model.FirstOrDefault().Country) </th> <th></th> </tr> @foreach (var item in Model) { <tr> <td> @Html.DisplayFor(modelItem => item.Name) </td> <td> @Html.DisplayFor(modelItem => item.Address) </td> <td> @Html.DisplayFor(modelItem => item.City) </td> <td> @Html.DisplayFor(modelItem => item.State) </td> <td> @Html.DisplayFor(modelItem => item.ZipCode) </td> <td> @Html.DisplayFor(modelItem => item.Country) </td> <td> @Html.ActionLink("Edit", "Edit", new { id = item.Id }) | @Html.ActionLink("Details", "Details", new { id = item.Id }) | @Html.ActionLink("Delete", "Delete", new { id = item.Id }) </td> </tr> } </table> @Html.PagedListPager(Model, page => Url.Action("Index", "Contacts", new { ViewBag.filterCity, ViewBag.filterSearch, page }), PagedListRenderOptions.EnableUnobtrusiveAjaxReplacing( new AjaxOptions { HttpMethod = "GET", UpdateTargetId = "contactList" }))
The Url.Action on the PagedListPager is the function that will be called when the user want to changes pages. Index is the action that will be called, Contacts is the controller that the action will be called on. Next is an anonymous type that will be processed to determine what parameters the index action will be called with. It is important that the existing filters are passed to the index action or else when the pager requests a different page it will return all contacts instead of next page of the filtered set of contacts.
The other nice option that the PagedListPager offers is the option to use ajax when requesting a different page. Since the contact page is using ajax when filtering it would be silly not to do the same when paging. To enable ajax just use the PagedListRenderOptions.EnableUnobtrusiveAjaxReplacing with the same AjaxOptions as the filter form.
As a side note I have changed the AjaxOptions on the index view that are used for filtering to match what is shown above for paging. I also removed the accepted verbs from the index action so it only responds to get requests. The request for a new set of data based on a filter change is really more of a get operation than a post operation.