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:
- You want to control the pager completely - display, style and all.
- 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
- 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; |
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:
using System; |
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; |
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
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);
}Change / create a view which takes the PagedList
as it’s model. <%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master"
Inherits="System.Web.Mvc.ViewPage<BL.Models.PagedList<Product>>" %>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.