Pager for ASP.NET MVC

I recently implemented a pager in ASP.NET MVC and thought it would be worth sharing. It uses fluent syntax - a technique I've not seen used much in MVC to solve similar problems so I thought I'd throw it out there! At first glance, MVC has a much more limited number of built in features for building user interfaces compared to ASP.NET Web Forms where you get hundreds of ready made controls. However, I quite like this as it gives a lot of opportunities to review the separation of concerns between rendering the user interface and performing the necessary logic and calculations on the user interface's behalf. So, let's look at how we can create a powerful pager in MVC without baking the calculations into the UI making it more testable and reusable.

Implementing an ASP.NET MVC Pager

Here is a fluent pager in action with ASP.NET MVC (the code for the pager I've written is listed at the bottom of the article).

First let's look at how you might use a fluent pager in a controller ("fluent" just means the class allows method chaining to modify the way the pager behaves):

public class HomeController : Controller
{
   public ActionResult Index(int? page)
   {
      ViewBag.Pager = Pager.Items(100).PerPage(10).Move(page ?? 1).Segment(5).Center();

      return View();
   }
}

The razor view markup to render the pager is:

@{
   Pager pager = ViewBag.Pager;

   if (pager.HasPreviousPage)
   {
      @Html.ActionLink("<<", "Index", new { page = pager.FirstPageIndex });
      @Html.ActionLink("<", "Index", new { page = pager.PreviousPageIndex });
   }

   foreach (int page in ViewBag.Pager)
   {
      @Html.ActionLink(page.ToString(), "Index", new { page = page });
   }

   if (pager.HasNextPage)
   {
      @Html.ActionLink(">", "Index", new { page = pager.NextPageIndex });
      @Html.ActionLink(">>", "Index", new { page = pager.LastPageIndex });
   }
}

The Pager class implements IEnumerable<int> so you can iterate over it like a collection to display the navigation page links. Of course you can put a much more fancy looking look and feel on things, the beauty of this approach is that it's pretty easy to totally change the appearance of the pager with minimal effort.

A refinement of the view would be to test if the page variable in the main loop is equal to pager.CurrentPageIndex in order to render the current page link differently. Fortunately, I've already provided a CurrentPageIndex property to allow this. You can check out the current set of properties I've added to the pager class at the bottom of this article (you can always add more if you need something extra).

Fluent Syntax for the Pager Explained

The fluent syntax to create the desired pager is: Pager.Items(100).PerPage(10).Move(page ?? 1).Segment(5).Center();

Let's say the page parameter is 5 and break down what's going on.

Pager.Items(100) creates a single page of 100 results. So the pager is only going to display a single page link:

1

Pager.Items(100).PerPage(10) ensures that there are only 10 items per page, which gives 10 page links (by default the pager assumes you're on the first page so we get a move to next page arrow (>) and a move to last page arrow (>>) due to how the view was implemented):

1 2 3 4 5 6 7 8 9 10 > >>

Pager.Items(100).PerPage(10).Move(5) moves the current page index to 5 so that a move to previous page arrow (<) and first page arrow (<<) become active:

<< < 1 2 3 4 5 6 7 8 9 10 > >>

Pager.Items(100).PerPage(10).Move(5).Segment(5) makes sure only 5 of the possible 10 pages are visible in the pager (ending on the current page index, or ending as close as possible to the current page index):

<< < 1 2 3 4 5 > >>

Finally, Pager.Items(100).PerPage(10).Move(5).Segment(5).Center() ensures the 5 visible pages are centred around the current page index rather than ending on the current page index:

<< < 3 4 5 6 7 8 > >>

Pager Class

Here is the fluent pager class. As you can see it provides all the logic and calculations without being coupled to a user interface so you can choose to create as many different views of it as you like. You can add your own methods and properties to take the concept further for your own needs. I've made sure the class is immutable so every method returns a new copy of itself. I think this is a nice way to implement fluent syntax when it's possible as the state of the object cannot be changed if it's passed to another section of code.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Collections;

public sealed class Pager : IEnumerable<int>
{
   private int _numberOfPages;
   private int _skipPages;
   private int _takePages;
   private int _currentPageIndex;
   private int _numberOfItems;
   private int _itemsPerPage;

   private Pager()
   {
   }

   private Pager(Pager pager)
   {
      _numberOfItems = pager._numberOfItems;
      _currentPageIndex = pager._currentPageIndex;
      _numberOfPages = pager._numberOfPages;
      _takePages = pager._takePages;
      _skipPages = pager._skipPages;
      _itemsPerPage = pager._itemsPerPage;
   }

   /// <summary>
   /// Creates a pager for the given number of items.
   /// </summary>
   public static Pager Items(int numberOfItems)
   {
      return new Pager
      {
         _numberOfItems = numberOfItems,
         _currentPageIndex = 1,
         _numberOfPages = 1,
         _skipPages = 0,
         _takePages = 1,
         _itemsPerPage = numberOfItems
      };
   }

   /// <summary>
   /// Specifies the number of items per page.
   /// </summary>
   public Pager PerPage(int itemsPerPage)
   {
      int numberOfPages = (_numberOfItems + itemsPerPage - 1) / itemsPerPage;

      return new Pager(this)
      {
         _numberOfPages = numberOfPages,
         _skipPages = 0,
         _takePages = numberOfPages - _currentPageIndex + 1,
         _itemsPerPage = itemsPerPage
      };
   }

   /// <summary>
   /// Moves the pager to the given page index
   /// </summary>
   public Pager Move(int pageIndex)
   {
      return new Pager(this)
      {
         _currentPageIndex = pageIndex
      };
   }

   /// <summary>
   /// Segments the pager so that it will display a maximum number of pages.
   /// </summary>
   public Pager Segment(int maximum)
   {
      int count = Math.Min(_numberOfPages, maximum);

      return new Pager(this)
      {
         _takePages = count,
         _skipPages = Math.Min(_skipPages, _numberOfPages - count),
      };
   }

   /// <summary>
   /// Centers the segment around the current page
   /// </summary>
   public Pager Center()
   {
      int radius = ((_takePages + 1) / 2);

      return new Pager(this)
      {
         _skipPages = Math.Min(Math.Max(_currentPageIndex - radius, 0), _numberOfPages - _takePages)
      };
   }

   public IEnumerator<int> GetEnumerator()
   {
      return Enumerable.Range(_skipPages + 1, _takePages).GetEnumerator();
   }

   IEnumerator IEnumerable.GetEnumerator()
   {
      return GetEnumerator();
   }

   public bool IsPaged { get { return _numberOfItems > _itemsPerPage; } }

   public int NumberOfPages { get { return _numberOfPages; } }

   public bool IsUnpaged { get { return _numberOfPages == 1; } }

   public int CurrentPageIndex { get { return _currentPageIndex; } }

   public int NextPageIndex { get { return _currentPageIndex + 1; } }

   public int LastPageIndex { get { return _numberOfPages; } }

   public int FirstPageIndex { get { return 1; } }

   public bool HasNextPage { get { return _currentPageIndex < _numberOfPages && _numberOfPages > 1; } }

   public bool HasPreviousPage { get { return _currentPageIndex > 1 && _numberOfPages > 1; } }

   public int PreviousPageIndex { get { return _currentPageIndex - 1; } }

   public bool IsSegmented { get { return _skipPages > 0 || _skipPages + 1 + _takePages < _numberOfPages; } }

   public bool IsEmpty { get { return _numberOfPages < 1; } }

   public bool IsFirstSegment { get { return _skipPages == 0; } }

   public bool IsLastSegment { get { return _skipPages + 1 + _takePages >= _numberOfPages; } }
}

I hope this gives a flavour of how to better push logic out of your MVC views and controllers and into easily testable and reusable code and also gives you a nice base for adding paging to your ASP.NET MVC websites.