Add Paging Functionality to your ASP.NET MVC Site
A common concern among many ASP.NET MVC developers (especially those with an extensive Web Forms background) centers around code-reuse for common UI tasks such as pagination. In this article I'll review a simple way to cleanly and efficiently add pagination functionality to ASP.NET MVC applications through simple code reuse and adherence to MVC's convention over configuration approach to development.
March 3, 2010
Related: "Understanding the ASP.NET MVC Framework" and "ASP.NET MVC: Ready for the Enterprise."
A common concern among many ASP.NET MVC developers (especially those with an extensive Web Forms background) centers around code-reuse for common UI tasks such as pagination. In this article I'll review a simple way to cleanly and efficiently add pagination functionality to ASP.NET MVC applications through simple code reuse and adherence to MVC's convention over configuration approach to development.
Cross Cutting Concerns
Within MVC circles there's a near religious debate that wages over the best way to handle what are known as cross-cutting concerns against what a Controller should be handling with any given request. For example, on an e-commerce site, it would be common to show a user a small summary of what kind of data is in their cart. But the model needed to supply that information (along with the controller logic necessary to arrange that model for use) is orthogonal to, or cross-cutting against, the model (and supporting controller action) you would need to display information about a given product, or a line of clothing, or even a collection of previously purchased orders, and so on.
Some developers choose to handle this kind of cross-cutting functionality within SuperClassed Controllers, or through filters. However, I've come to prefer handling this approach via Partial Requests or full blown requests that map out to a new Controller, invoke an action (which assembles needed models) and return a partial view displaying cross-cutting data such as whether or not a user is logged in, what might be in their cart, common navigational requirements, and so on.
And, while ASP.NET MVC 1.0 provides some great support for Partial Requests out of the box, the example I'll be demonstrating against an ASP.NET MVC 1.0 site in this article takes advantage of the Html.RenderAction() feature found in the ASP.NET MVC 1.0 Futures assembly. (NOTE: At this point, it looks like Html.RenderAction will be a core part of MVC 2.0.)
Convention over Configuration
Another common meme among MVC developers is the notion of favoring convention over configuration. Consequently, with my approach to paging, I've taken the approach of favoring a convention over requiring explicit configuration. As such, to implement paging in my solutions, I've assumed that any view that needs to be paged will contain an optional &page=n value in the query-string. If that variable isn't present in the query-string and the view is paged, it's assumed that the current page equals 1. But, in this regard, I'm assuming that page=n is how paging is handled, rather than allowing configuration of another value or option to look for. Thus, I'm strongly favoring (my own) convention over configuration in this regard.
I could have taken the approach (or convention) of putting paging functionality in the actual path itself, such as something like /widgets/page/3/, but I personally don't believe that pagination should USUALLY be a part of the path. More importantly though, my approach to paging assumes that the data being paged represents a full collection of data that is cached on the server. So, if you're paginating data from the database, my approach won't work for you. I developed this approach to achieve greater scalability and performance when working with full collections of data that are already cached on the server, but which are broken up into bite-sized pieces to make working with the data easier on end users.
Creating a Framework for Paging
The heart, or back-end, of my paging solution derives from a PagedList solution posted by Rob Conery on his blog a long while back. I won't rehash his code here because it's very straight-forward and obvious in that it simply extends a typical List to wrap some Skip() and Take() LINQ functionality along with meta-data to cleanly encapsulate lists which can be easily queried for paged data.
With pagination for my collections handled, the next step in creating a re-usable paging UI was to create a class, or model, that could provide cross-cutting UI and navigation for pagination capabilities. To accomplish this, I created a PagingDisplay class (or View Model) which takes in an IPagedList along with the current url (path and query), which it uses to create links to different pages by merely replacing the page=n variable as needed to link to different pages within the same request.
This PagingDisplay view model then just exposes a handful of methods that can be called from within the .ascx (or partial view) tasked with outputting the paging UI, with the primary goal of the logic within this class focusing on handling what I like to call “eliding” links. For example, if you've got a collection of objects that can all be displayed within about 10 pages, then you can typically display 10 different links on a given page in the Paging UI. But if you've got 30, or 50, or more pages, then putting that many links on a page quickly gets ugly. As such, I like to wrap the current page that a user is on with a buffer of say 4 pages, and then make sure that they can always see the first and last page within the collection. So, if there are 39 pages total, and a user is on 18, an elided set of paging links would show a link to page 1, then 4 links before page 18, page 18, four links after page 18, and then page 39. Consequently, the bulk of the logic in my PagingDisplay class is built around either eliding links, or just building simple links to different pages within the collection.
Achieving Code Reuse
With a PagingDisplay mode in play, it then becomes very easy to add pagination to existing views. I prefer to accomplish this by just routing a partial request via Html.RenderAction() like so:
<% Html.RenderAction("PagingGenerator", "Partial", new \{ source = Model \}); %>
And, in this case, what I'm defining is that I would like the View Engine to run out to my PartialController, execute an Action called PagingDisplay, and inject the markup that that partial request generates right into my view at the location where the Partial Request is placed.
For that to work, I have to have a corresponding ActionResult within the specified controller, like so:
public ActionResult PagingGenerator(IPagedList source)
\{
PagingDisplay display = new PagingDisplay(Request.Url.PathAndQuery, source);
return View(display);
\}
As you can see, that Action just wires up a new PagingDisplay, and hands it the source, or collection of data that needs to be paged, along with the current url, which it can then use to tweak and create new urls with different page-numbers defined.
Of course, the only caveat here is that the PagingDisplay is expecting an IPageList as the source, and your current view is probably working with Lists or some other enumerable collections.
For simple collections (where the Model is just a collection), this is super easy to handle, as the sample below indicates.
// non-paged (old)
public ActionResult GetAllWidgets()
\{
List widgets = this.FakeRepository.GetAllWidgets();
return View(widgets);
\}
// paged (new)
public ActionResult GetAllWidgetsPaged(int? page)
\{
List widgets = this.FakeRepository.GetAllWidgets();
// set up paging details
int currentPage = page.HasValue ? page.Value : 0;
return View(widgets.ToPagedList(currentPage, 20));
\}
As you can see from the examples above, the only change is the need to add a new int? page into the method (which is another reason why I prefer using the query-string because adding this variable doesn't require any changes to routing), along with changing the Model to actually become a .ToPagedList() variant using an Extension Method. Of course, you'll also need to change the Inherits directive of your View (from List to PagedList) if you're using a strongly-typed view, but, otherwise, these two changes are all you'll need.
For more complex View Models - where you might have a model that flattens some meta-data and then provides a collection of info, you'll need to slightly modify your view model to handle pagination. So, for example, if I had a view model for a conference where there were properties for location, date, topics, and so on, along with a List I'd add the following property and Method to my Conference Model:
public class Conference
\{
// ... as before
public int CurrentPage \{ get; set; \}
public PagedList GetAttendees(int size)
\{
return this._attendees.ToPagedList(CurrentPage, size);
\}
// .. as before
\}
Then, in my controller action, instead of merely changing my Model on the way out, I'd assign the .CurrentPage value which would set the stage for pagination details, like so.
public ActionResult Conference(int? page)
\{
Conference conference = this.FakeRepository.GetConference();
// set up paging details
int currentPage = page.HasValue ? page.Value : 0;
conference.CurrentPage = currentPage;
return View(conference);
\}
Then, within my view, I'd then just work with that .GetAttendees() method - knowing that it will give me the paged data that was configured within the controller action.
<% PagedList attendees = Model.GetAttendees(); %>
<% Html.RenderAction("PagingGenerator", "Partial", new \{ source = attendees \}); %>
<% foreach(Attendee a in attendees) \{ %>
Display Stuff here...
<% \} %>
In this way, my view just defines that it's now working with a PagedList, which it gets from the ViewModel, and that PagedList, in turn, is also handed-off to the cross-cutting, view model tasked with handling pagination. Best of all, with this approach I can add pagination into any view within just a few seconds.
Do You Want a Copy?
I've used this paging approach now on a couple of my own ASP.NET MVC projects and solutions. And what I've written about in this article is a version with many of the initial kinks worked out. Overall, I'm now much more comfortable adding paging to my MVC applications with this approach than I ever was with adding pagination to Web Forms. (But I'm one of those folks that felt like I was always fighting Web Forms at every turn. So if that's not you, then you probably won't share my sentiment.) But, the key benefit of using this paging approach is that it enables end users to easily work with large amounts of cached data (or collections) on the server while making re-use very easy on the developer.
As with previous MVC examples and ideas that I've written about, I've created a sample project that you can use to get a feel for how this pagination works in the wild. This sample project will also give you an idea of how easy it is to extend my paging ideas and approach to meet any of your needs. So, if you'd like a copy of this sample application, just drop me a line at [email protected].
About the Author
You May Also Like