ASP.NET MVC Tip #44 – Create a Pager HTML Helper
In this tip, I demonstrate how you can create a custom HTML Helper that you can use to generate a user interface for paging through a set of database records. I build on the work of Troy Goode and Martijn Boland. I also demonstrate how you can build unit tests for HTML Helpers by faking the HtmlHelper class.
This week, I discovered that I desperately needed a way of paging through the database records in two of the sample MVC applications that I am building. I needed a clean, flexible, and testable way to generate the user interface for paging through a set of database records.
There are several really good solutions to this problem that already exist. I recommend that you take a look at Troy Goode’s discussion of his PagedList class at the following URL:
http://www.squaredroot.com/post/2008/07/08/PagedList-Strikes-Back.aspx
Also, see Martijn Boland’s Pager HTML Helper at the following URL:
I used both of these solutions as a starting point. However, there were some additional features that I wanted that forced me to extend these existing solutions:
· I wanted complete flexibility in the way in which I formatted my pager.
· I wanted a way to easily build unit tests for my pager.
Walkthrough of the Pager HTML Helper
Let me provide a quick walkthrough of my Pager Helper solution. Let’s start by creating a controller that returns a set of database records. The Home controller in Listing 1 uses a MovieRepository class to return a particular range of Movie database records.
Listing 1 – Controllers\HomeController.cs
using System.Web.Mvc; using Tip44.Models; namespace Tip44.Controllers { [HandleError] public class HomeController : Controller { private MovieRepository _repository; public HomeController() { _repository = new MovieRepository(); } public ActionResult Index(int? id) { var pageIndex = id ?? 0; var range = _repository.SelectRange(pageIndex, 2); return View(range); } } }
The Home controller in Listing 1 has one action named Index(). The Index() action accepts an Id parameter that represents a page index. The Index() action returns a set of database records that correspond to the page index.
The Home controller takes advantage of the MovieRepository in Listing 2 to retrieve the database records.
Listing 2 – Models\MovieRepository.cs
using MvcPaging; namespace Tip44.Models { public class MovieRepository { private MovieDataContext _dataContext; public MovieRepository() { _dataContext = new MovieDataContext(); } public IPageOfList<Movie> SelectRange(int pageIndex, int pageSize) { return _dataContext.Movies.ToPageOfList(pageIndex, pageSize); } } }
A range of movie records is returned by the SelectRange() method. The ToPageOfList() extension method is called to generate an instance of the PageOfList class. The PageOfList class represents one page of database records. The code for this class is contained in Listing 3.
Listing 3 – Models\PageOfList.cs
using System; using System.Collections.Generic; namespace MvcPaging { public class PageOfList<T> : List<T>, IPageOfList<T> { public PageOfList(IEnumerable<T> items, int pageIndex, int pageSize, int totalItemCount) { this.AddRange(items); this.PageIndex = pageIndex; this.PageSize = pageSize; this.TotalItemCount = totalItemCount; this.TotalPageCount = (int)Math.Ceiling(totalItemCount / (double)pageSize); } public int PageIndex { get; set; } public int PageSize { get; set; } public int TotalItemCount { get; set; } public int TotalPageCount { get; private set; } } }
Finally, the movie database records are displayed in the view in Listing 4. The view in Listing 4 is a strongly typed view in which the ViewData.Model property is typed to an instance of the IPageOfList interface.
Listing 4 – Views\Home\Index.aspx
<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Index.aspx.cs" Inherits="Tip44.Views.Home.Index" %> <%@ Import Namespace="MvcPaging" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" > <head runat="server"> <title>Index</title> <style type="text/css"> .pageNumbers { display:inline; margin:0px; } .pageNumbers li { display: inline; padding:3px; } .selectedPageNumber { font-weight: bold; text-decoration: none; } </style> </head> <body> <div> <ul> <% foreach (var m in ViewData.Model) { %> <li><%= m.Title %></li> <% } %> </ul> <%= Html.Pager(ViewData.Model)%> </div> </body> </html>
When the view in Listing 4 is displayed in a web browser, you see the paging user interface in Figure 1.
Figure 1 – Paging through movie records
Notice that the view in Listing 4 includes a Cascading Style Sheet. By default, the Html.Pager() renders the list of page numbers in an unordered bulleted list (an XHTML <ul> tag). The CSS classes are used to format this list so that the list of page numbers appears in a single horizontal line.
By default, the Html.Pager() renders three CSS classes: pageNumbers, pageNumber, and selectedPageNumber. For example, the bulleted list displayed in Figure 1 is rendered with the following XHTML:
<ul class='pageNumbers'>
<li class='pageNumber'><a href='/Home/Index/2'><</a></li>
<li class='pageNumber'><a href='/Home/Index/0'>1</a></li>
<li class='pageNumber'><a href='/Home/Index/1'>2</a></li>
<li class='pageNumber'><a href='/Home/Index/2'>3</a></li>
<li class='selectedPageNumber'>4</li>
<li class='pageNumber'><a href='/Home/Index/4'>5</a></li>
<li class='pageNumber'><a href='/Home/Index/4'>></a></li>
</ul>
Notice that the 4th page number is rendered with the selectedPageNumber CSS class.
Setting Pager Options
There are several options that you can set when using the Pager HTML Helper. All of these options are represented by the PagerOptions class that you can pass as an additional parameter to the Html.Pager() method. The PagerOptions class supports the following properties:
· IndexParameterName – The name of the parameter used to pass the page index. This property defaults to the value Id.
· MaximumPageNumbers – The maximum number of page numbers to display. This property defaults to the value 5.
· PageNumberFormatString – A format string that you can apply to each unselected page number.
· SelectedPageNumberFormatString – A format string that you can apply to the selected page number.
· ShowPrevious – When true, a previous link is displayed.
· PreviousText – The text displayed for the previous link. Defaults to <.
· ShowNext – When true, a next link is displayed.
· NextText – The text displayed for the next link. Defaults to >.
· ShowNumbers – When true, page number links are displayed.
Completely Customizing the Pager User Interface
If you want to completely customize the appearance of the pager user interface then you can use the Html.PagerList() method instead of the Html.Pager() method. The Html.PagerList() method returns a list of PagerItem classes. You can render the list of PagerItems in a loop.
For example, the revised Index view in Listing 5 uses the Html.PagerList() method to display the page numbers.
Listing 5 – Views\Home\Index.aspx
<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Index.aspx.cs" Inherits="Tip44.Views.Home.Index" %> <%@ Import Namespace="MvcPaging" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" > <head runat="server"> <title>Index</title> <style type="text/css"> .pageNumbers { display:inline; margin:0px; } .pageNumbers li { display: inline; padding:3px; } .selectedPageNumber { font-weight: bold; text-decoration: none; } </style> </head> <body> <div> <ul> <% foreach (var m in ViewData.Model) { %> <li><%= m.Title %></li> <% } %> </ul> <%-- Show Page Numbers --%> <% foreach (PagerItem item in Html.PagerList(ViewData.Model)) { %> <a href='<%= item.Url %>' class='<%=item.IsSelected ? "selectedPageNumber" : "pageNumber" %>'><%= item.Text%></a> <% } %> </div> </body> </html>
Figure 2 – Custom pager user interface
The main reason that I added the Html.PagerList() method to the PagerHelper class is to improve the testability of the class. Testing the collection of PageItems returned by Html.PagerList() is easier than testing the single gigantic string returned by Html.Pager(). Behind the scenes, the Html.Pager() method simply calls the Html.PagerList() method. Therefore, building unit tests for the Html.PagerList() method enables me to test the Html.Pager() method as well.
Testing the Pager HTML Helper
I created a separate test project for unit testing the PagerHelper class. The test project has one test class named PagerHelperTests that contains 10 unit tests.
One issue that I ran into almost immediately when testing the PagerHelper class was the problem of faking the MVC HtmlHelper class. In order to unit test an extension method on the HtmlHelper class, you need a way of faking of the HtmlHelper class.
I decided to extend the MvcFakes project with a FakeHtmlHelper class. I used the FakeHtmlHelper class in all of the PagerHelper unit tests. The FakeHtmlHelper is created in the following test class Initialize method:
[TestInitialize] public void Initialize() { // Create fake Html Helper var controller = new TestController(); var routeData = new RouteData(); routeData.Values["controller"] = "home"; routeData.Values["action"] = "index"; _helper = new FakeHtmlHelper(controller, routeData); // Create fake items to page through _items = new List<string>(); for (var i = 0; i < 99; i++) _items.Add(String.Format("item{0}", i)); }
Notice that I need to pass both a controller and an instance of the RouteData class to the FakeHtmlHelper class. The RouteData class represents the controller and action that generated the current view. These values are used to generate the page number links.
The Intialize() method also creates a set of 100 fake data records. These data records are used in the unit tests as a proxy for actual database records.
Here’s a sample of one of the test methods from the PagerHelperTests class:
[TestMethod] public void NoShowNext() { // Arrange var page = _items.AsQueryable().ToPageOfList(0, 5); // Act var options = new PagerOptions { ShowNext = false }; var results = PagerHelper.PagerList(_helper, page, options); // Assert foreach (PagerItem item in results) { Assert.AreNotEqual(item.Text, options.NextText); } }
This test method verifies that setting the ShowNext PagerOption property to the value false causes the Next link to not be displayed.
In the Arrange section, an instance of the PageOfList class is created that represents a range of records from the fake database records.
Next, in the Act section, the PagerOptions class is created and the PagerHelper.PagerList() method is called. The PagerHelper.PagerList() method returns a collection of PagerItem objects.
Finally, in the Assert section, a loop is used to iterate through each PagerItem object to verify that the Next link does not appear in the PagerItems. If the text for the Next link is found then the test fails.
Using the PagerHelper in Your Projects
At the end of this blog entry, there is a link to download all of the code for the PagerHelper. All of the support classes for the PagerHelper are located in the MvcPaging project. If you want to use the PagerHelper in your MVC projects, you need to add a reference to the MvcPaging assembly.
Summary
Creating a good Pager HTML Helper is difficult. There are many ways that you might want to customize the user interface for paging through a set of database records. Attempting to accommodate all of the different ways that you might want to customize a paging user interface is close to impossible.
Working on this project gave me a great deal of respect for the work that Troy Goode and Martijn Boland performed on their implementations of pagers. I hope that this tip can provide you with a useful starting point, and save you some time, when you need to implement a user interface for paging through database records.