MVC Paging and Sorting

If you’re new to MVC this is undoubtably something you’ll face at some point. You’ve already been searching Google and found a few examples, heard about MVCContribWebGrid and Telerik. All of which are great solutions. However you want the manual control, don’t want to use tables or you just like to stay up late and bang keys on the keyboard. If that is the case this should at the very least get you on the right track. As with anything I post feel free to critique. It can only help. There are always alternate ways to skin a cat. This is no exception. Be advised as often with code I post I’ve significantly scaled this down to make it easy to follow striping out the application specific stuff.

So let’s get to it…

The first thing you’ll need to do is create a class that will handle your paging and sorting. This was expanded on from Scott Gu’s PaginatedList.cs class which didn’t support sorting but we’ll get to that.

PaginatedList.cs  (abbreviated)

Declare a few properties:

public int PageIndex { get; private set; } // Index of your current page.
public string SortExpression { get; private set; } // The expression to sort on ex: "FullName DESC".
public string SortColumn { get; private set; } // The column you are sorting on ex: "FullName".
public string SortDirection { get; private set; } // The direction to sort ex: "DESC" or "ASC".
public int PageDisplayIndex { get; private set; } // This is the page number to display. Show 1-5 instead of 0-4.
public int PageSize { get; private set; } // The amount of records per page.
public int TotalCount { get; private set; } // The total count of records.
public int TotalPages { get; private set; } // The total number of pages. TotalCount / PageSize
public int PagerSize { get; private set; } // The size of the pager. How many page numbers to show.
public int PagerStart { get; private set; } // The starting page in the pager numbers.
public int PagerEnd { get; private set; } // The ending page of the pager numbers.
public List<int> Pages { get; private set; } // The list of pager page numbers to display.

Let’s create the paginated list by passing a few variables from our controller (we’ll get to that shortly) .

public PaginatedList(IQueryable<T> source, string sortExpression, int pageIndex, int pageSize, int pagerSize)
{
    //Populate variables.
    PageIndex = pageIndex;
    SortExpression = sortExpression;
    PageDisplayIndex = pageIndex;
    PageSize = pageSize;
    PagerSize = pagerSize; 

    //Store the current Sort Column and Direction.
    //There should always be a value but just in case. This is for providing the current sort expression in our Model
    if (sortExpression != null && !string.IsNullOrEmpty(sortExpression))
    {
        string[] curSort = sortExpression.Split(' '); // Split so we can get the col and direction sep.
        SortColumn = curSort[0];

        //This could be null so let's check first.
        if (curSort.Length > 1)
        {
            SortDirection = curSort[1];
        }

    }       

    //Get the record count and the total pages.
    TotalCount = source.Count();
    TotalPages = (int)Math.Ceiling(TotalCount / (double)PageSize);

    //Build out the list of pages available.
    BuildPagerPages(); // See below for this method. Creates the page numbers for the pager.

    //Finally Sort the data and select the page size.
    this.AddRange(source.OrderBy(sortExpression)
        .Skip((PageIndex - 1) * PageSize)
        .Take(PageSize));
}

Now we need a simple method to build out a list of the page numbers to display. Down load source, there are a couple other properties like “HasNextPage” or “HasPreviousPage” that you’ll need to check in your view or Html Helper to verify the link should be displayed.

/// <summary>
/// Creates list of page numbers for pager.
/// </summary>
private void BuildPagerPages()
{
    PagerStart = Math.Max(1, (PageIndex) - PagerSize / 2); // Start page of pager.
    PagerEnd = Math.Min(TotalPages, PageIndex + (PagerSize / 2)); // End page of pager.

    //Iterate through the total pages to make page selectors.
    Pages = new List<int>();
    for (var i = PagerStart; i <= PagerEnd; i++)
    {
        Pages.Add(i); // Add page number to list.
    }
}

DynamicOrderBy.cs (abbreviated)

Grabbed this in part from StackOverflow and Adam Anderson’s blog. It’s a great piece of code. I won’t bore you It is included in the source below but checkout the links for further details. Essentially though this class allows us to do our Linq OrderBy with ease.

Let’s Wire Up Our Controller

Probably the easiest part of the whole process. All we are doing here is grabbing some values from our route grabbing our data from our repository or where ever and then create our new instance of our PaginatedList class.

public ActionResult Index(string sort, int? page )
{
    // Define the page and pager defaults.
    const int pageSize = 10;
    const int pagerSize = 10;

    // You could also set this as an optional value
    // and set the default that way too.
    if (sort == null || string.IsNullOrEmpty(sort))
    {
        sort = "FullName ASC";
    }

    var accounts = repository.GetList(); // This is simply getting your data. That’s up to you, in this case a repository.
    // Note the page number default there. It is 1 instead of 0. This gets changed in our PaginatedList
    // There are a couple ways you could do this but the point is to have page numbers 1-5 instead of 0-5.
    var paginatedAccounts = new PaginatedList<Account>(accounts, sort, page ?? 1, pageSize, pagerSize);          

    return View("Index", paginatedAccounts);
}

Setup Routing

The simplest way to do this is of course in your Global.asax. Or you may have some other custom mechanisim to handle your routes as I do. But for this will show you the Global.asax method for brevity. This is very straight forward. Simply create a new route like below. Essentially you’d have something like “Clients/{sort}/{page}”.

routes.MapRoute(
    "ClientList", // Route name
    "Clients/{sort}/{page}", // URL with parameters
    new { controller = "Account", action = "Index" } // Parameter defaults
);

Last Piece of Puzzle the View and Helper

Updating the view is very easy. The below is abbreviated showing only the Previous button but you could easily add Next, First and Last if you wanted. So there are a couple ways you could do this. You could use JQuery and build out the url or you can use a Helper. There may be reasons why you might use one over the other but I’m going to show the Helper method. Remember in our PaginatedList class we had some properties called SortColumn, SortExpression and SortDirection right? Well now we need them. You could just pass the SortExpression only if you wanted and then split it up in your class however I chose to expose the expression and also the col and exp separately also. So in our link we simply need to pass in the current or last sort details plus the newly selected details. This is simply for one reason and that is if the column name passed is the same as the previous we need simply to sort the opposite way. If the column is a different one or no column exists we just sort ascending. Simple right. May see complicated but isn’t just read the comments it all makes sense.

public static MvcHtmlString SortActionLink(this HtmlHelper helper, string text, string actionName,
    string controller, string curColumn, string curDirection, string tag, object routeValues, object htmlAttributes)
{
    string sortResult, sortCol, sortExpression, sortDir, tagType;
    tagType = "a";
    sortExpression = "{0} {1}"; //Just because I don't want to write + ' ' +...ohh wait I just did. LOL.
    RouteValueDictionary routeDict = new RouteValueDictionary(routeValues);

    //Check for null. If this is the first time the page is loaded
    // if so we don't need to update the sort values.
    if (routeDict["sort"] != null)
    {
        sortCol = routeDict["sort"].ToString();
        if (string.Equals(curColumn, sortCol, StringComparison.CurrentCultureIgnoreCase))
        {
            //We're sorting the same column
            //Check the direction and then supply the opposite.
            switch (curDirection)
            {
                case "ASC":
                    sortDir = "DESC";
                    break;
                case "DESC":
                    sortDir = "ASC";
                    break;
                default:
                    sortDir = "ASC";
                    break;
            }
            sortResult = string.Format(sortExpression, sortCol, sortDir);
        }
        else
        {
            //This is a new column pass the default.
            //No need to append Sort Direction.
            //If no sort direction is supplied ASC is assumed.
            sortResult = sortCol;
        }

        //Update the Route data.
        routeDict["sort"] = sortResult;
        //helper.ViewContext.RequestContext.RouteData.Values["sort"] = sortResult;
    }

    var urlHelper = new UrlHelper(helper.ViewContext.RequestContext);
    var url = urlHelper.Action(actionName, controller, routeDict);

    if (tag != null)
        tagType = tag;

    TagBuilder build = new TagBuilder(tagType);
    build.MergeAttribute("href", url);
    build.InnerHtml = text;
    build.MergeAttributes(new RouteValueDictionary(htmlAttributes));
    return MvcHtmlString.Create(build.ToString(TagRenderMode.Normal));
}

…and the razor markup to make it all work. You are passing in the name for the link, then the action, then the controller. Then we pass in our current values of sort column and direction, then our Route Values which we modified based on the current sort values and finally any html attributes if needed.

@Html.SortActionLink("Full Name", "Index", "Client", Model.SortColumn, Model.SortDirection, new { sort = "FullName", page = Model.PageDisplayIndex }, null)

Wrap Up

To wrap up in your source you will notice that the First, Next etc links can be shown or hidden by a simple if statement that checks the HasPreviousPage bool value that we exposed in our PaginatedList class. Again see source for how to do this, if you don’t already.

Hope this helps you out,

C

Download Source

mvc_paging_sorting.rar

Advertisements

Tags: , , , ,

About Origin1 Technologies

Developer: Asp.Net, MVC, JQuery, MS SQL, VB, C#, Java, Eclipse, Android, Apple, FileMaker Pro and others.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: