Wednesday, December 8, 2010

ASP.NET MVC Pager

As promised in my last blog post about creating a reusable NHibernate paging extension method, here is some code that uses the IPagedList interface we created to render a nice looking pager control in an ASP.NET MVC site.

Pager.cs
  1. public class Pager
  2. {
  3.     public enum PagerMode
  4.     {
  5.         NextPrev,
  6.         Numbers,
  7.         All
  8.     }
  9.  
  10.     public string QueryStringParam { get; set; }
  11.     public PagerMode Mode { get; set; }
  12.     public string NextText { get; set; }
  13.     public string PreviousText { get; set; }
  14.     public string FirstText { get; set; }
  15.     public string LastText { get; set; }
  16.     public IPagedList List { get; set; }
  17.     public bool ShowTotalRecords { get; set; }
  18.     public string TotalRecordsText { get; set; }
  19.     public RequestContext Context { get; set; }
  20.     public bool EnableAjax { get; set; }
  21.     public int MaxNumberOfNextPages { get; set; }
  22.     public int MaxNumberOfPreviousPages { get; set; }
  23.  
  24.     private StringBuilder _builder;
  25.     private UrlHelper _urlHelper;
  26.  
  27.     public Pager(IPagedList list, RequestContext context)
  28.     {
  29.         List = list;
  30.         Context = context;
  31.  
  32.         _builder = new StringBuilder();
  33.         _urlHelper = new UrlHelper(Context);
  34.  
  35.         // Defaults
  36.         QueryStringParam = "page";
  37.         Mode = PagerMode.All;
  38.         NextText = "Next »";
  39.         PreviousText = "« Previous";
  40.         FirstText = "« First";
  41.         LastText = "Last »";
  42.         ShowTotalRecords = false;
  43.         TotalRecordsText = "{0} total records";
  44.         EnableAjax = true;
  45.         MaxNumberOfNextPages = 4;
  46.         MaxNumberOfPreviousPages = 5;
  47.     }
  48.  
  49.     public string Render()
  50.     {
  51.         if (List.TotalCount == 0 || List.TotalPages == 1)
  52.             return String.Empty;
  53.  
  54.         _builder.AppendLine("<div class=\"pagination\">");
  55.  
  56.         if (Mode == PagerMode.NextPrev || Mode == PagerMode.All)
  57.         {
  58.             WriteFirstLink();
  59.             WritePreviousLink();
  60.         }
  61.  
  62.         if (Mode == PagerMode.Numbers || Mode == PagerMode.All)
  63.         {
  64.             WritePageNumbers();
  65.         }
  66.  
  67.         if (Mode == PagerMode.NextPrev || Mode == PagerMode.All)
  68.         {
  69.             WriteNextLink();
  70.             WriteLastLink();
  71.         }
  72.  
  73.         if (ShowTotalRecords)
  74.         {
  75.             WriteTotalRecords();
  76.         }
  77.  
  78.         _builder.AppendLine("</div>");
  79.  
  80.         return _builder.ToString();
  81.     }
  82.  
  83.     private void WriteTotalRecords()
  84.     {
  85.         var span = new TagBuilder("span");
  86.         span.AddCssClass("total");
  87.         span.SetInnerText(String.Format(TotalRecordsText, String.Format("{0:n0}", this.List.TotalCount)));
  88.  
  89.         _builder.AppendLine(span.ToString());
  90.     }
  91.  
  92.     private void WriteLastLink()
  93.     {
  94.         if (!List.HasNextPage)
  95.             return;
  96.  
  97.         WriteLink(GenerateUrl(List.TotalPages - 1), LastText);
  98.     }
  99.  
  100.     private void WriteNextLink()
  101.     {
  102.         if (!List.HasNextPage)
  103.             return;
  104.  
  105.         WriteLink(GenerateUrl(List.PageIndex + 1), NextText);
  106.     }
  107.  
  108.     private void WritePageNumbers()
  109.     {
  110.         int start = 0;
  111.         int end = List.TotalPages - 1;
  112.         int maxPages = (MaxNumberOfPreviousPages + MaxNumberOfNextPages) + 1; // the + 1 is for the current page
  113.  
  114.         if (List.TotalPages > maxPages)
  115.         {
  116.             start = List.PageIndex - MaxNumberOfPreviousPages;
  117.             end = List.PageIndex + MaxNumberOfNextPages;
  118.  
  119.             if (start < 0)
  120.                 start = 0;
  121.             if (end > List.TotalPages - 1)
  122.                 end = List.TotalPages - 1;
  123.         }
  124.  
  125.         for (int i = start; i <= end; i++)
  126.         {
  127.             WriteLink(GenerateUrl(i), i + 1);
  128.         }
  129.     }
  130.  
  131.     private void WritePreviousLink()
  132.     {
  133.         if (!List.HasPreviousPage)
  134.             return;
  135.  
  136.         WriteLink(GenerateUrl(List.PageIndex - 1), PreviousText);
  137.     }
  138.  
  139.     private void WriteFirstLink()
  140.     {
  141.         if (!List.HasPreviousPage)
  142.             return;
  143.  
  144.         WriteLink(GenerateUrl(0), FirstText);
  145.     }
  146.  
  147.     private void WriteLink(string url, string text)
  148.     {
  149.         var tag = new TagBuilder("a");
  150.         tag.Attributes["href"] = url;
  151.         tag.Attributes["title"] = text;
  152.         tag.SetInnerText(text);
  153.  
  154.         _builder.Append(tag.ToString());
  155.     }
  156.  
  157.     private void WriteLink(string url, int pageNumber)
  158.     {
  159.         var tag = new TagBuilder("a");
  160.         tag.Attributes["href"] = url;
  161.         tag.Attributes["title"] = pageNumber.ToString();
  162.         tag.AddCssClass("number");
  163.         if ((pageNumber - 1) == List.PageIndex)
  164.             tag.AddCssClass("current");
  165.         tag.SetInnerText(pageNumber.ToString());
  166.  
  167.         _builder.Append(tag.ToString());
  168.     }
  169.  
  170.     private string GenerateUrl(int pageIndex)
  171.     {
  172.         if (this.EnableAjax)
  173.         {
  174.             return "#" + QueryStringParam + "=" + pageIndex;
  175.         }
  176.         else
  177.         {
  178.             Context.RouteData.Values[QueryStringParam] = pageIndex;
  179.  
  180.             return _urlHelper.RouteUrl(Context.RouteData.Values);
  181.         }
  182.     }
  183. }

The Pager class contains the rendering logic for the pager control and makes use of the TagBuilder class supplied in the System.Web.Mvc namespace.  Makes generating well-formatted HTML code a bit easier, in my opinion.  The class itself is pretty boring, just pass in the IPagedList instance and BAM!  There are some customization options as well like changing the text of the Next/Previous/First/Last links, showing or hiding the total record count, etc.  One of the more interesting options is the EnableAjax property which, when set to true, renders the URL of each pager link as a hash link (/list#page=2) instead of a hard link (/list?page=2).  This allows you to intercept the hash changed event using jQuery and dynamically update the page content using Ajax.

In typical MVC fashion, I created a couple extension methods that make calling the Pager class easy from a View.  You can customize the settings however you like.  Call any of these methods from your view and just pass in the IPagedList instance.

MVC Pager Extension Methods
  1. public static string Pager(this HtmlHelper html, IPagedList list)
  2. {
  3.     return Pager(html, list, "page", false);
  4. }
  5.  
  6. public static string AjaxPager(this HtmlHelper html, IPagedList list)
  7. {
  8.     return Pager(html, list, "page", true);
  9. }
  10.  
  11. public static string Pager(this HtmlHelper html, IPagedList list, string queryStringParam, bool enableAjax)
  12. {
  13.     var pager = new Pager(list, html.ViewContext.RequestContext)
  14.     {
  15.         QueryStringParam = queryStringParam,
  16.         EnableAjax = enableAjax,
  17.         ShowTotalRecords = true
  18.     };
  19.     return pager.Render();
  20. }

Here’s what the rendered pager markup looks like, pretty clean if I do say so myself.

Rendered pager markup
  1. <div class="pagination">
  2.     <a title="First" href="/list?page=0">&lt;&lt; First</a>
  3.     <a title="Previous" href="/list?page=0">&lt;&lt; Previous</a>
  4.     <a class="number" title="1" href="/list?page=0">1</a>
  5.     <a class="current number" title="2" href="/list?page=1">2</a>
  6.     <a class="number" title="3" href="/list?page=2">3</a>
  7.     <a title="Next" href="/list?page=2">Next &gt;&gt;</a>
  8.     <a title="Last" href="/list?page=2">Last &gt;&gt;</a>
  9.     <span class="total">37 total records</span>
  10. </div>

It doesn’t take much to make this look nice with some CSS classes.  Here’s a simple example.

Code Snippet
  1. a
  2. {
  3.     color: #003366;
  4. }
  5. .pagination
  6. {
  7.     margin: 0 0 5px;
  8.     text-align: center;
  9. }
  10. .pagination a, .pagination a:visited
  11. {
  12.     margin: 0 5px 0 0;
  13.     padding: 3px 6px;
  14.     text-decoration: none;
  15. }
  16. .pagination a.number
  17. {
  18.     border: 1px solid #DDDDDD;
  19. }
  20. .pagination a.current
  21. {
  22.     color: #000000;
  23.     border-color: #DDDDDD;
  24.     background-color: #DDDDDD;
  25. }
  26. .pagination .total
  27. {
  28.     display: block;
  29.     margin-top: 4px;
  30.     color: #666666;
  31. }

And, finally, here’s what the pager looks like on the, err, page.

pager

No comments:

Post a Comment