Tags: , , , , , , , | Categories: Web Posted by nurih on 8/5/2009 4:16 AM | Comments (0)

The need

As certain as the sun rising tomorrow, there will come the point where you will want to display a list or grid with paging. While many solutions exist, and many component developers are coming in with robust solutions, a simple and satisfactory solution can be created fairly easily.

Implementation

Why create a pager from scratch? Several reasons:

1) You want to control the pager completely – display, style and all.

2) You don't like the idea of JavaScript paging, which will load your hundreds of pages to the browser and do client side paging / grid

3) You want to understand and control exactly how a page subset of record is fetched and take control of database or IO thrashing

Of the quickly surveyed solutions out there, I found this one simple and straightforward. Being small, straightforward and simple means also easy to maintain, extend or modify. Based on that solution, I've created my own pager which breaks into 2 class implementations and one usage guidance.

The first class, is the PagedList class. The whole class is rather small and the only crux is doing correct math and ensuring the logic handles zero items returned. This class is responsible for taking a source list of all items (more on that below in the performance considerations) and presenting simple properties for HasNextPage, HasPreviousPage, TotalPages and CurrentPage. The implementation inherits from the generic List<T>, and so exposes and enumerator and the Count property. The constructor copies only the current page's worth of items into the instance though, so Count will return the number of items on the current page (0 to page size) therefore an additional property TotalItems is populated upon construction which exposes the total number of items in the underlying source.

using System;
using System.Collections.Generic;
using System.Linq;
namespace BL.Models
{
/// <summary>
/// Adapted from http://blog.wekeroad.com/2007/12/10/aspnet-mvc-pagedlistt/
/// </summary>
/// <typeparam name="T">The type of item this list holds</typeparam>
public class PagedList<T> : List<T>, IPagedList
{
/// <summary>
/// Initializes a new instance of the <see cref="PagedList&lt;T&gt;"/> class.
/// </summary>
/// <param name="source">The source list of elements containing all elements to be paged over.</param>
/// <param name="currentPage">The current page number (1 based).</param>
/// <param name="pageSize">Size of a page (number of items per page).</param>
public PagedList(IEnumerable<T> source, int currentPage, int itemsPerPage)
{
this.TotalItems = source.Count();
this.ItemsPerPage = itemsPerPage;
this.CurrentPage = Math.Min(Math.Max(1, currentPage), TotalPages);
this.AddRange(source.Skip((this.CurrentPage - 1) * itemsPerPage).Take(itemsPerPage).ToList());
}
public int CurrentPage {get ;private set;}
public int ItemsPerPage { get; private set; }
public bool HasPreviousPage { get { return (CurrentPage > 1); } }
public bool HasNextPage { get { return (CurrentPage * ItemsPerPage) < TotalItems; } }
public int TotalPages { get { return (int)Math.Ceiling((double)TotalItems / ItemsPerPage); } }
public int TotalItems { get; private set; }
}
}

PagedList implements the interface IPagedList, since a static class can not be generic, and the control renderer will need access to the PagedList's properties:

   1: using System;
   2:  
   3: namespace BL.Models
   4: {
   5:     public interface IPagedList
   6:     {
   7:         int CurrentPage { get; }
   8:         bool HasNextPage { get; }
   9:         bool HasPreviousPage { get; }
  10:         int ItemsPerPage { get; }
  11:         int TotalItems { get; }
  12:         int TotalPages { get; }
  13:     }
  14: }

The second class is more like a custom web control. Since this is MVC, we are driven to use a helper like implementation. My approach to developing HTML helpers for MVC is to create an extension method on the System.Web.MVC.ViewPage type. This allows the use of the well known and tested HtmlTextWriter to render the actual HTML rather than creating angled brackets in strings on the fly. I find this approach both more true to the form – rendering output to the output stream and not composing a string to be copied later – and safe: using compliant well tested constants and constructs rather than typing in HTML and hoping your syntax and understanding of the tag is correct.

using System.Web.Mvc;
using System.Web.UI;
public static partial class HtmlHelpers
{
/// <summary>
/// Shows a pager control - Creates a list of links that jump to each page
/// </summary>
/// <param name="page">The ViewPage instance this method executes on.</param>
/// <param name="pagedList">A PagedList instance containing the data for the paged control</param>
/// <param name="controllerName">Name of the controller.</param>
/// <param name="actionName">Name of the action on the controller.</param>
public static void ShowPagerControl(this ViewPage page, IPagedList pagedList, string controllerName, string actionName)
{
HtmlTextWriter writer = new HtmlTextWriter(page.Response.Output);
if (writer != null)
{
for (int pageNum = 1; pageNum <= pagedList.TotalPages; pageNum++)
{
if (pageNum != pagedList.CurrentPage)
{
writer.AddAttribute(HtmlTextWriterAttribute.Href, "/" + controllerName + "/" + actionName + "/" + pageNum);
writer.AddAttribute(HtmlTextWriterAttribute.Alt, "Page " + pageNum);
writer.RenderBeginTag(HtmlTextWriterTag.A);
}
writer.AddAttribute(HtmlTextWriterAttribute.Class,
pageNum == pagedList.CurrentPage ?
"pageLinkCurrent" :
"pageLink");
writer.RenderBeginTag(HtmlTextWriterTag.Span);
writer.Write(pageNum);
writer.RenderEndTag();
if (pageNum != pagedList.CurrentPage)
{
writer.RenderEndTag();
}
writer.Write("&nbsp;");
}
writer.Write("(");
writer.Write(pagedList.TotalItems);
writer.Write(" items in all)");
}
}
}

 

The implementation creates a list of page numbers, with a link on each except for the current page. The link will be of the format "/{controller name}/{action name}/{page number}".

I have added conditional style attribute to the link so that you can style the current page differently from the other pages easily. Since you have the code, you can extend the resultant HTML as you wish. You might want to have text indication of "no more pages" or some indication if the list is empty etc.

Finally, you would want to make use of this shiny new widget. The steps are as follows

1) In your controller, create an action which takes the page number as it's sole parameter. The action would then create a new instance of the PagedList, passing it the "full list" and the current page number from the parameter.

public ActionResult Page(int id)
        {
            List<Product> products = CatalogService.ListOpenProducts();
            PagedList<Product> data = new PagedList<Product>(products, id, PAGE_SIZE);
 
            return View(data);
        }

2) Change / create a view which takes the PagedList<your row type> as it's model.

<%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master" 
Inherits="System.Web.Mvc.ViewPage<BL.Models.PagedList<Product>>" %>

3) Place a call to the extension method to display the pager anywhere in your view (multiple placement allowed – you can put one on top and one on the bottom etc). Recall that the method ShowPagerControl() extends  ViewPage, so the keyword this should show your intellisence for the method. If you chose a more complex model (MVVM ViewModel containing more data than just the paged list) then you would use Model.{paged list property name}. The use of the view as a bag of random data conjured up by string names should IMHO be universally abandoned and eliminated.

<% this.ShowPagerControl(Model, "Bids", "Page"); %>

 

Considerations

Take != load all + scroll

The PagedList implementation takes an IEnumerable<T> as it's source data. Internally, it uses Linq syntax which would seem to require all items be loaded, and then skip the first N pages and take the next {page size} worth of items. If your underlying list of items is a huge DB call, you will find that troubling. What you might consider then will be to use deferred loading. Extend the Linq IQueryable<T> or ObjectQuery<T> and let the ORM of your choice do the paging in the database. If your ORM is eager loader, you will need to implement custom partial record loading and paging at the data source level. If it can defer loading you will be in better shape.

Conversely, you might want to actually eager-load all records at the first shot. This will provide you with 2 benefits: cachability and coherency. Loading all items into memory incurs one DB call overhead and the IO required for all records. If you load each page at a time, you would incur {page count} * page clicks DB call overheads which might exceed the former if users scroll often back and forth. Once you load the whole list, you can cache it in memory and expire it based on data change events. If you have the base list in memory, access to it incurs no more IO regardless of pager clicks. Another phenomena caching gets around is coherency problems. If you page ad the DB level and an item is inserted or deleted, the end user experiences skips or stutter items. A skip is when a user clicks from page 1 to 2 an item which was to be on page 2 now is in the range of page 1 because an item on page 1 was deleted. Going to page 2 skips this item, and paging back should reveal it but would be surprising to the user (because she just came from page 1 and it wasn't there before) creating the appearnace of a skipped / missed item. A stutter is the reverse situation: user clicks from page 1 to 2, and an item from page 1 appears again on page 2. This happens when an item was added and "pushed" the repeat item into page 2 because of it's sorting order. This appears to the user as a malfunction and may infuriate some enough to call customer service (alas, advising customers to adjust their medication does not actually calm them down). A solution to coherence is to cache the result list for each user, expiring the cache actively when navigating away or running a different query.

Conclusion

The code above and variations of it are fairly easy to create. If your favorite web control vendor has not solved this for you, if you want to take full control of your paging of if you are just naturally curious – it's a great way to add paging to your MVC application.