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:

http://blogs.taiga.nl/martijn/Default.aspx

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 – ControllersHomeController.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 – ModelsMovieRepository.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 – ModelsPageOfList.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 – ViewsHomeIndex.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

image

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′>&lt;</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′>&gt;</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 – ViewsHomeIndex.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> &nbsp;
    <% } %>


 </div>
</body>
</html>

Figure 2 – Custom pager user interface

image

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.

Download the Code

Discussion

  1. http:// says:

    Great Post!!! Keep it coming

  2. ricky says:

    There must be Pager fever!! (I just blogged about my simple implementation a day or two ago rickyrosario.com/…/simple-htmlhelper-extensio…).

    I went with a very simplistic approach, being the lazy programmer that I am :). But I do see some value in making it testable in all. And I also see value in keeping the lines of code for paging to a minimum. So its a debatable tradeoff.

    Thanks for all your Tips!!!

  3. http:// says:

    Very timely and very awesome! Just thought about having to do this last night. Thanks!

  4. Troy Goode says:

    I like it Stephen, thanks for another great MVC tip! This is such a common use case I’d really love to see something similar available “out of the box” at some point.

  5. http:// says:

    very easy and powerful
    i finded several solutions but your solution is very better .

  6. Kyle LeNeau says:

    This code is great! I am attempting to use it under a medium trust environment though (shared hosting on godaddy) and am getting a security exception (like a trust issue, godaddy is medium trust). I have set the trust level in my web.config on my development machine and run/debug the code. The first security exception get thrown at PageLinqExtensions.cs (var totalItemCount = allItems.Count();). I changed that line to be an array and return the length and that works, then the same exception is thrown at PageOfList.cs (this.AddRange(items);). I stopped at this point. Any pointers or help on why this wont work in Medium trust? Or what could be changed so that it would work under medium trust level. Thanks.

  7. http:// says:

    oh!!! beutiful~~!!! wonderful~~!!!!

  8. http:// says:

    good!!!

  9. v.baskar says:

    We are using MVC Preview5 ,then how we will
    implement the Paging ?

  10. Antonio says:

    Hi, Looking at the code it seems you’d be querying a DB each time you request a page. Is this so? How would you get around this using MVC?

  11. Oleg Shastitko says:

    Hello Stephen
    I like your blog and use as tutorial about ASP.NET MVC
    I want to ask you about repository – I try to use this way too – create context object in constructor and use same object in all methods.

    public class MovieRepository
    {
    private MovieDataContext _dataContext;

    public MovieRepository()
    {
    _dataContext = new MovieDataContext();
    }

    public IPageOfList SelectRange(int pageIndex, int pageSize)
    {
    return _dataContext.Movies.ToPageOfList(pageIndex, pageSize);
    }
    }

    But I think about update/add/delete actions – if something method do update/add/delete we should invoke SubmitChanges(), but SubmitChanges calls for all changes and we can have situation when it’s not necessary to call in that moment. How to solve it?

    Thanks

  12. Michael says:

    Can the Page Helper run under VB?

  13. Rony says:

    The page numbers starts with 0 is this configurable?

  14. faffy fuck says:

    Great post! I keep cumming.

  15. Ramon Hagelen says:

    I tried to translate your code into VB.NET, the code compiles but I can’t get it working in .aspx pages.
    I think it’s because of this statement in C#:
    public static string Pager(this HtmlHelper helper, PageOfList pageOfList)

    Which I translated in:
    _
    Public Function Pager(Of T)(ByVal helper As HtmlHelper, ByVal pageOfList As PageOfList(Of T)) As String

    Unfortunately when I try to incorporate this into a View with:
    < %=MvcPaging.Pager(Me, ViewData.Model)%>

    (I can’t get it with HTML.Pager…)

    I get the message:
    Data type(s) of the type parameter(s) in method ‘Public Function Pager(Of T)(helper As System.Web.Mvc.HtmlHelper, pageOfList As MVC_BackOffice.MvcPaging.PageOfList(Of T)) As String’ cannot be inferred from these arguments

    Do you or anyone reading this have a solution for this?

  16. Gill Bates says:

    Stephen Waiter has made a great post once again! He looks like a douche Microsoft Program Manager though. I bet he chases ducks in the hallway!

  17. faffy fuck says:

    Gill, shut up or I’ll pop a cap in yo ass.

  18. Ramon Hagelen says:

    Problem solved:

    In VB.NET you have to call the Pager like this:
    < %=MvcPaging.Pager(Of PageOfList)(ViewData.Model)%>

  19. faffy fuck says:

    @ramon: so basically you’re saying don’t call it at all?

  20. Ramon Hagelen says:

    @faffy fuck:

    No I’m saying:
    call the pager from the view page like:
    < %=MvcPaging.Pager(Of PageOfList)(ViewData.Model)%>

    and not like:
    < %=MvcPaging.Pager(ViewData.Model)%>

    In VB.NET you explicitly have to state the type as well, unlike in C#

  21. faffy fuck says:

    @ramon: as strange as it might seem to you, I somehow can’t see what you’re saying. Really, when you look at your comment above this one… you just say “like: (nothing) and not like: (nothing else)”…?!

  22. kwon says:

    I faced a build error In your sample code.

    Error was occured in PagerBuilder.cs

    Following is detailed exception :

    Error 2 Argument ‘1’: cannot convert from ‘System.Web.Mvc.ViewContext’ to ‘System.Web.Routing.RequestContext’ D:TEMPCSTip44MvcPagingPagerBuilder.cs 165 43 MvcPaging

    Is there any soulution about this?
    Thanks

  23. Chris says:

    How would this fit into a page that also contain fields to search on, where you need to submit form data?

  24. midavis says:

    kwon,

    modifiy that line of code to.

    var urlHelper = new UrlHelper(_helper.ViewContext.RequestContext);

  25. Really such a useful information. I will try to do that. Thanks.

  26. Oh Stephen! You always giving the best one.

  27. Microsoft Enterprise Validation Application BlockI think this is the way to go

  28. such a nice information is given. I am looking for that one. Thanks once again Stephen.

  29. f3 I tried to mock a call to a Linq to SQL query, but I am struggling.

  30. PHP help says:

    Very useful tips and instructions for HTML users.

  31. Kenny Nguyen says:

    Hello, I’m having a problem using it with a ViewModel class. This is what I have:

    Repository code:
    public IPageOfList GetBlogArchivesPaging(int month, int year, int pageIndex, int pageSize)
    {
    return (from b in this.Blogs
    orderby b.BlogPostedDate descending
    where b.BlogPostedDate.Month == month && b.BlogPostedDate.Year == year
    select b).ToPageOfList(pageIndex, pageSize);
    }

    My ViewModel:
    public class BlogViewModel
    {
    public BlogArchivesViewModel BlogArchivesModel { get; set; }
    }

    public class BlogArchivesViewModel
    {
    public IPageOfList BlogArchivesModelPaging { get; set; }
    }

    My Controller:
    [AcceptVerbs(HttpVerbs.Get)]
    public ActionResult Archives(int month, int year, int? page)
    {
    var pageIndex = page ?? 0;
    using (var dc = new KennyDataContext())
    {
    BlogViewModel model = new BlogViewModel
    {
    BlogArchivesModel = new BlogArchivesViewModel
    {
    BlogArchivesModelPaging = dc.GetBlogArchivesPaging(month, year, pageIndex, 5)
    }
    };

    return View(model);
    }
    }

    I have problem with this line (and all others code with regard to Html.Pager(…)):

    < %= Html.Pager(ViewData.Model.BlogArchivesModel.BlogArchivesModelPaging) %>

    The error is “The type arguments for method ‘KennyLib.Paging.PagerHelper.Pager(System.Web.Mvc.HtmlHelper, KennyLib.Paging.PageOfList)’ cannot be inferred from the usage. Try specifying the type arguments explicitly.”

    Any idea why? Thank you very much.

  32. Kenny says:

    How can I replace the page index so it starts with 1 instead of 0? It funny how the text of the links display “1 2 3 ….” and such and the link displays page=0, 1, 2, ….”

    Thanks.

  33. 4vg
    I believe it is a promising (currently version 4.0). So I would stick with it.. thanks