Paging links in ASP.NET MVC
Using the MVC framework on a real, if somewhat trivial, project has been a lot of fun so far. It's unlike me to wait for something to go final, but as I Google problems I encounter, it's good to see that I avoided a great many moving targets.
That said, I've always enjoyed building custom controls. Sometimes they're not even necessary, but I find it to be a good exercise because generally there isn't anything hard that I haven't encountered before. I have a templated gallery control, for example, that populates a grid of images with an ad wrapped inside. With the community and photo stuff I often do, the potential for reuse is great.
At first I worried that you lose some of this joy with MVC, but actually you just have to refactor it. One of the things I've had plenty of uses for is a paging control that builds a series of links to page data, based on the total number of pages and the index you're currently on. It's packaged up in POP Forums (see here for an example), and I've used it in a number of other places as well. Porting it to MVC was surprisingly easy by making it an HtmlHelper extension method.
Here's what I ported into my simple blogging app...
using System;
using System.Text;
using System.Web.Mvc;
using System.Web.Mvc.Html;
namespace CliqueSite.Blog.Web.Helpers
{
public static class Html
{
public static string PagerLinks(this HtmlHelper htmlHelper, string controllerName, string actionName, int pageCount, int pageIndex, string cssClass, string moreTextCssClass, string currentPageCssClass)
{
return PagerLinks(htmlHelper, controllerName, actionName, pageCount, pageIndex, "First", "Previous", "Next", "Last", "More", cssClass, moreTextCssClass, currentPageCssClass);
}
public static string PagerLinks(this HtmlHelper htmlHelper, string controllerName, string actionName, int pageCount, int pageIndex, string firstPage, string previousPage, string nextPage, string lastPage, string moreText, string cssClass, string moreTextCssClass, string currentPageCssClass)
{
var builder = new StringBuilder();
if (String.IsNullOrEmpty(controllerName) || String.IsNullOrEmpty(actionName))
throw new Exception("controllerName and actionName must be specified for PageLinks.");
if (pageCount <= 1)
return String.Empty;
if (String.IsNullOrEmpty(cssClass)) builder.Append("<div>");
else builder.Append(String.Format("<div class=\"{0}\">", cssClass));
if (String.IsNullOrEmpty(moreTextCssClass)) builder.Append(moreText);
else builder.Append(String.Format("<span class=\"{0}\">{1}</span>", moreTextCssClass, moreText));
if (pageIndex != 1)
{
// first page link
builder.Append(htmlHelper.RouteLink("|<", new { controller = controllerName, action = actionName, id = 1 }, new {title = firstPage}));
if (pageIndex > 2)
{
// previous page link
int previousLink = pageIndex - 1;
builder.Append(htmlHelper.RouteLink("<<", new { controller = controllerName, action = actionName, id = previousLink }, new { title = previousPage }));
}
}
// calc low and high limits for numeric links
int intLow = pageIndex - 1;
int intHigh = pageIndex + 3;
if (intLow < 1) intLow = 1;
if (intHigh > pageCount) intHigh = pageCount;
if (intHigh - intLow < 5) while ((intHigh < intLow + 4) && intHigh < pageCount) intHigh++;
if (intHigh - intLow < 5) while ((intLow > intHigh - 4) && intLow > 1) intLow--;
for (int x = intLow; x < intHigh + 1; x++)
{
// numeric links
if (x == pageIndex)
{
if (String.IsNullOrEmpty(currentPageCssClass))
builder.Append(String.Format("{0} of {1}", x, pageCount));
else builder.Append(String.Format("<span class=\"{0}\">{1} of {2}</span>", currentPageCssClass, x, pageCount));
}
else
{
builder.Append(htmlHelper.RouteLink(x.ToString(), new { controller = controllerName, action = actionName, id = x }));
}
}
if (pageIndex != pageCount)
{
if (pageIndex < pageCount - 1)
{
// next page link
int nextLink = pageIndex + 1;
builder.Append(htmlHelper.RouteLink(">>", new { controller = controllerName, action = actionName, id = nextLink }, new { title = nextPage }));
}
// last page link
builder.Append(htmlHelper.RouteLink(">|", new { controller = controllerName, action = actionName, id = pageCount }, new { title = lastPage }));
}
builder.Append("</div>");
return builder.ToString();
}
}
}
As I said, this is largely similar to the server control I built for the forum app, with two important differences. Instead of exposing properties for the control to determine the page numbers, text labels, CSS classes and such, they're set as parameters to the extension methods. The second difference is that the actual links aren't set up using the Control's ResolveUrl() method and some String.Format() methods. In fact, these are even easier, because you simply use the existing RouteLink() extension method to do all of the magic for you. They were even clever enough to give you endless flexibility to add attributes by way of anonymous types. Well done.
In the controller, I'm setting up data like this:
public ActionResult Page(int page)
{
int total;
ViewData["Posts"] = _postRepository.GetPagedPosts(10, page, out total);
ViewData["TotalPages"] = total;
ViewData["Page"] = page;
...
Then in the view itself, this is the magic that keeps it clean:
<%= Html.PagerLinks("Admin", "Page", (int)ViewData["TotalPages"], (int)ViewData["Page"],
"theCssClass", "theMoreTextCssClass", "theCurrentPageCssClass") %>
Does this violate any kind of design principles? I don't think so, but I'm rarely one to get extremely academic about this sort of thing. I'm not crazy about loading up views with a ton of display logic, and there would be a spaghetti mess to do something like this if you left it to the view all by itself.
What do you think? Can we expect to see an explosion of HtmlHelper extension libraries?