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? 

10 Comments

  • I've translated your post

  • Man, this is totally maintainable code. No wonder everybody's so excited about ASP.NET's implementation of MVC. Yawn.

  • Nothing annoys me more than someone who does a drive-by comment, anonymously, with no context for their opinion. It's worthless.

    What exactly is not maintainable about it? Or maybe the better question is, why would you have to maintain it at all?

  • Ignore that other guy, Jeff. I think your post is very useful, but two questions.

    1) Have you verified your code works with the release?
    2) Have you considered a simpler version that just does Next/Previous?

  • Yes, it was written to the release version, and no, I didn't. No, I didn't consider a more simple version because this was a port.

  • What a mess :) Refactor it a bit, this code is as readable as reading the book from 10 feet away in a dark. You know, you can have more than one method in a class.

    Sending CSS styles down to the control seems to be another thing that infected people from webforms age. You can always use container and element hierarchies to configure visual appearance, you don't have to class every single DIV.

    And you could even have separate shared view for HTML.

  • I never suggested that it was the most awesome code ever and didn't need refactoring. And yes, you could use hierarchies of CSS, but you still need to specify a name for the CSS class when the same elements (divs) are targeted for a number of different styles.

  • Did you know what this error is caused from?
    I am using your code with MVC2.

    Method not found: 'System.Web.Mvc.MvcHtmlString System.Web.Mvc.Html.LinkExtensions.RouteLink(System.Web.Mvc.HtmlHelper, System.String, System.Object)'.

  • Sure, the method isn't found. You obviously have a referencing problem.

  • Yes sure is referencing problem, but the mysterious is that it work fine on my pc but on the host server have problem. The host server has mvc 2 as well..

Comments have been disabled for this content.