ASP.NET MVC 3 HtmlHelper for custom pager

For our existing intranet applications written using web forms we have a web control to do our paging like most web controls it uses viewstate to remember what state it is currently in going forward we are now using MVC so I needed to replace the webcontrol with a HtmlHelper method to create a pager.

I had a look round to find an existing one however all the ones I came across work in slightly different way compared to our existing webcontrol below is screenshot of how it looks:

image

The HtmlHelpers I found seem to work by taking the current page number and populating the page numbers either side so that the page number is always in the middle of the other numbers, our existing web control works by displaying a group of page numbers you can then use the ellipse button to move to the previous/next group of pages, for example:

given the above I can move between all the pages above and the selected will be displayed emboldened, if I want to move to the next group of pages I can either click the ellipse button or if I’m on page 5 I can use the > button both will display the next group starting from 6. After racking my brain I came up with an algorithm to support this with no viewstate being available.

Basically we can use the current page number to find the last page number in this current group, i.e. if were displaying groups of 3 and were on page 4 the last number in this group is 6 the first group is 1-3. We can then use this to find the start number for the group by taking this number and take off the group size – 1 (2 in this case). The only thing we need to provision for is that we haven’t gone over the page count which can be done by checking which is minimum between the last page number for the group or the page count. Once we have the boundaries of the group we can work out which buttons to display and which page to display fairly easily.

Here is the code to demonstrate:

  
public static MvcHtmlString Pager(this HtmlHelper helper, int currentPage, int pageSize, int totalItemCount, object routeValues) 
{  	
    // how many pages to display in each page group const  	
    int cGroupSize = 5;  	
    var pageCount = (int)Math.Ceiling(totalItemCount / (double)pageSize);   	
    
    // cleanup any out bounds page number passed  	
    currentPage = Math.Max(currentPage, 1);  	
    currentPage = Math.Min(currentPage, pageCount);  	
    
    var urlHelper = new UrlHelper(helper.ViewContext.RequestContext, helper.RouteCollection);  	
    var container = new TagBuilder("div");
    container.AddCssClass("pager");  	
    var actionName = helper.ViewContext.RouteData.GetRequiredString("Action");   	
    
    // calculate the last page group number starting from the current page  	
    // until we hit the next whole divisible number  	
    var lastGroupNumber = currentPage;  	
    while ((lastGroupNumber % cGroupSize != 0)) lastGroupNumber++;   	
    
    // correct if we went over the number of pages  	
    var groupEnd = Math.Min(lastGroupNumber, pageCount);   	
    
    // work out the first page group number, we use the lastGroupNumber instead of  	
    // groupEnd so that we don't include numbers from the previous group if we went  	
    // over the page count  	
    var groupStart = lastGroupNumber - (cGroupSize - 1);   	
    
    // if we are past the first page  	
    if (currentPage > 1)  	
    {  		
        var previous = new TagBuilder("a");  		
        previous.SetInnerText("<");  		
        previous.AddCssClass("previous");  		
        var routingValues = new RouteValueDictionary(routeValues);  		
        routingValues.Add("page", currentPage - 1);  		
        previous.MergeAttribute("href", urlHelper.Action(actionName, routingValues));  		
        container.InnerHtml += previous.ToString();  	
    }   	
    
    // if we have past the first page group  	
    if (currentPage > cGroupSize)  	
    {  		
        var previousDots = new TagBuilder("a");  		
        previousDots.SetInnerText("...");  		
        previousDots.AddCssClass("previous-dots");  		
        var routingValues = new RouteValueDictionary(routeValues);  		
        routingValues.Add("page", groupStart - cGroupSize);  		
        previousDots.MergeAttribute("href", urlHelper.Action(actionName, routingValues));  		
        container.InnerHtml += previousDots.ToString();  	
    }   	
    
    for (var i = groupStart; i <= groupEnd; i++)  	
    {  		
        var pageNumber = new TagBuilder("a");  		
        pageNumber.AddCssClass(((i == currentPage)) ? "selected-page" : "page"); 				
        pageNumber.SetInnerText((i).ToString());  		
        var routingValues = new RouteValueDictionary(routeValues);  		
        routingValues.Add("page", i);  		
        pageNumber.MergeAttribute("href", urlHelper.Action(actionName, routingValues));  		
        container.InnerHtml += pageNumber.ToString();  	
    }   	
    
    // if there are still pages past the end of this page group  	
    if (pageCount > groupEnd)  	
    {  		
        var nextDots = new TagBuilder("a");  		
        nextDots.SetInnerText("...");  		
        nextDots.AddCssClass("next-dots");  		
        var routingValues = new RouteValueDictionary(routeValues);  		
        routingValues.Add("page", groupEnd + 1); 
        nextDots.MergeAttribute("href", urlHelper.Action(actionName, routingValues));  		
        container.InnerHtml += nextDots.ToString();  	
    }   	
    
    // if we still have pages left to show  	
    if (currentPage < pageCount)  	
    {  		
        var next = new TagBuilder("a");  		
        next.SetInnerText(">");  		
        next.AddCssClass("next");  		
        var routingValues = new RouteValueDictionary(routeValues);  		
        routingValues.Add("page", currentPage + 1);  		
        next.MergeAttribute("href", urlHelper.Action(actionName, routingValues));  		
        container.InnerHtml += next.ToString();  	
    }   	
    
    return MvcHtmlString.Create(container.ToString());  
}

To use it from the screen you simply do:

@Html.Pager(Model.Page, 20, Model.TotalItems, new {Model.Description})

Hopefully this will help someone who needs a pager to work this way as opposed to the more common offset implementations.

Advertisements

Extending the ASP.NET MVC HtmlHelper

The HtmlHelperobject in MVC is great if we want to have a link to an action on a controller we simply do:


htmlhelper extension manual

And this will create us a link with the text ‘my link’ and will automatically route the href to the ViewItem action on the controller in context. However the HtmlHelper object doesn’t have everything and there are going to be times when you want to have your own HTML rendered on screen, so how can we go about doing that.

The answer lies with extension methods, we can use these to extend the HtmlHelper object and provide us with what were after, in my case it was that I wanted a link but an image enclosed instead of just text.

So here’s the code to achieve this:

public static class HtmlHelperExtensions
{
    public static string ImageActionLink(this HtmlHelper helper, string imagePath, string actionName, string controllerName, object routeValues, object imageAttributes, object linkAttributes)
    {
        var urlHelper = new UrlHelper(helper.ViewContext.RequestContext, helper.RouteCollection);
        var anchortagBuilder = new TagBuilder("a");

        anchortagBuilder.MergeAttributes(new RouteValueDictionary(linkAttributes));
        anchortagBuilder.MergeAttribute("href", urlHelper.Action(actionName, controllerName, routeValues));

                var imageTagBuilder = new TagBuilder("img");
        imageTagBuilder.MergeAttributes(new RouteValueDictionary(imageAttributes));
        imageTagBuilder.MergeAttribute("src", imagePath);

        anchortagBuilder.InnerHtml = imageTagBuilder.ToString(TagRenderMode.SelfClosing);
        return anchortagBuilder.ToString(TagRenderMode.Normal);
    }
}

The good thing is that we get a few objects to help us build up the html and to handle the routing side of things rather than having deal with concatenating strings.

Notice that I use the RouteValueDictionary object so that I can use the anonymous type to dictionary trick, the UrlHelper deals with returning the routed URL I just need to provide the controller and what action to invoke, and the TagBuilder makes  light work of creating HTML.

So now from my view I can use it like this:


I could provide some overloads so that I don’t need to pass empty objects and to accept Dictionaries instead of just object, but you get the idea.

One other thing that’s worth doing is changing the web.config do that your namespace is added automatically to your views that saves having to put the Import on each view that wants to use your extensions, this can be found in the pages section in namespaces.

Using Windsor for Controller Creation in ASP.NET MVC

Given it’s reached v1.0 I finally decided to do some work with the ASP.NET MVC framework and what better way than to take the Issue Tracker which currently runs on Rhino Igloo and refactor it to run on ASP.NET MVC, so expect the next few posts to be related to my findings of how to get things done with it.

First thing is that Rhino Igloo out of the box handles controller creation and uses Castle Windsor to do this ASP.NET MVC by default will use it’s own controller builder, because of this it expects your controllers to have a default no-arg constructor this is no good as I have controllers that need their dependencies passed in, so we need to replace the default controller builder with a version that we can hook up to Castle Windsor.

Now I could go and write my own version this is because the framework is very flexible and was built to allow you to plugin your own  objects, in this case an object that implements IControllerFactory will suffice however why re-invent the wheel when the guy’s over at MvcContrib have done most of the work for us.

Getting the MvcContrib Objects

Head over to to the MvcContrib project on codeplex and grab the extras release, now copy the MvcContrib.dll and MvcContrib.Castle.dll into your external references folder and add a reference where you will be doing the app startup.

Setting the Controller Factory

Inside your global.asax or wherever you do app startup you need to add the following code:

ControllerBuilder.Current.SetControllerFactory(
                new WindsorControllerFactory(this.Container));

Notice that I’m passing the current instance of the windsor container which I get for free because I’m using the UnitOfWorkApplication base class from Rhino.Commons, you will probably have some custom way of managing your container, in which case it just needs to be passed in to the constructor of WindsorControllerFactory.

Wiring up the Controllers

Next we need to make Windsor aware of the controllers, I use Binsor for my Windsor configuration this gives me a nice way to autowire new controllers to Windsor without making any changes:

for type in AllTypes("IssueTracker.Controllers"):
    continue unless typeof(IController).IsAssignableFrom(type)
    component type.Name, type:
        @lifestyle="transient"

Gotta love the conditional expression support Boo has to offer it almost reads like a sentence 🙂 You could also use the XML configuration or programmatic configuration to configure your controllers, and that’s all there is to it!

Kudos to the ASP.NET team for having the foresight to allow these kind of extensions to be plugged in so easily and also to the MvcContrib contributer’s for creating this and support for other IoC containers out there.