Enhancing the WebGrid: Paging Enhancements

February 12th, 2016

In the last post, I showed you how to create a paged list of records in the WebGrid. Today, we continue by adding paging enhancements to the bottom of the grid.

After writing the last post about WebGrid paging, it felt half-finished and I felt bad that we didn't add any cool features.

Well, let's change that. In this post we'll look at a "fully loaded" pagination bar on our WebGrid.

If you look at the WebGrid we made on Wednesday, you'll notice we didn't even create a bar at the bottom to include any pagination data.

However, we need to refactor some methods and models in our example before we can continue with the UI portion so let's get started.

Time to Refactor!

Since we are looking to include more details about the records returns from Entity Framework, we need a third-party library called MvcPaging. In your Package Manager Console, type:

Install-Package MvcPaging

This NuGet package has been around for a long time (since version 3 I think?) and is a pretty powerful paging package (say that three times fast). This adds everything into your project for a solid paging mechanism.

If this looks familiar, it's because we used this same library in a past post called Using Display and Editor Templates. We will be reusing that same code here.

Unfortunately, we require a number of refactorings to get this new functionality to work properly.

Since we added this library, we have a new interface (and class) called PagedList<T> at our disposal. We need to implement this starting with the repository.

Refactor the Repository (Method)

I like to start at the bottom level and work my way to the UI.

First, let's fix the GetPagedUsers() method in the UserRepository.

public IPagedList<User> GetPagedUsers(PagingModel model)
{
    var records = GetAll()
        .OrderBy(e=> e.UserName);
    var total = records.Count();
    return new PagedList<User>(records, model.PageIndex, model.PageSize, total);
}

As you can see, we are retrieving the records just like a normal data call, we order them by UserName, and get the count while we're there.

This is one of the classes that was brought over from the MvcPaging library. We wrap the records into a PagedList of User and add the appropriate parameters.

Don't forget to modify the Interface while you're at it.

Interface\IUserRepository.cs

public interface IUserRepositoryIRepository<User>
{
    User GetById(int id);
    IPagedList<User> GetPagedUsers(PagingModel model);
}

The UserRepository and interface is done. Let's move up the chain.

Change Your View...Model!

The UserViewModel needs adjusted. We need to change the type from an IEnumerable<User> to an IPagedList<User> (Change in Bold)

ViewModel\UserViewModel.cs

public class UserViewModel
{
    public IPagedList<User> Users { getset; }
    public User User { getset; }
    public IEnumerable<User> SelectedUsers { getset; }
    public bool Delete { getset; }
    public bool SendEmail { getset; }
}

Once we have that type changed to an IPagedList, we can turn our focus to the View.

Ahhh...the View.

View the View!

I always like to look at sample pagination styles after I get the functionality working. There are a diverse number of examples out there.

One of the best examples is the Smashing Magazine's Pagination Gallery, but the one pagination style I like the most is Google Analytics.

As you can see, they have the Page Size ("Show Rows"), Go to Page Number, the amount of records on the screen out of the total and the left and right arrows.

Let's focus on the Page Range on the screen, the common page numbers, and allow the user to change the PageSize with a Dropdown.

"Bootstrapping" our Pagination

To reuse our existing code, we'll steal the partial view from the post Using Display and Editor Templates and put it to good use here. The PartialView looks like this.

Views\Shared\DisplayTemplates\BootstrapPagination.cshtml

@using WebGridExample.Helpers.Html
@model PaginationModel
<ul class="pagination pagination-sm">
    @foreach (var link in Model.PaginationLinks)
    {
        @BuildLink(link)
    }
</ul>
 
@helper BuildLink(PaginationLink link)
{
var liBuilder = new TagBuilder("li");
if (link.IsCurrent)
{
    liBuilder.MergeAttribute("class""active");
}
if (!link.Active)
{
    liBuilder.MergeAttribute("class""disabled");
}
var aBuilder = new TagBuilder("a");
aBuilder.MergeAttribute("href", link.Url ?? "#");
// Ajax support
if (Model.AjaxOptions != null)
{
    foreach (var ajaxOption in Model.AjaxOptions.ToUnobtrusiveHtmlAttributes())
    {
        aBuilder.MergeAttribute(ajaxOption.Key, ajaxOption.Value.ToString(), true);
    }
}
aBuilder.SetInnerText(link.DisplayText);
liBuilder.InnerHtml = aBuilder.ToString();
    @Html.Raw(liBuilder.ToString())
}

For reference purposes, Bootstrap has great documentation about pagination which can be found here. So if you are interested in modifying the class of the pagination control, this is the file to manipulate.

Let's move on to the actual PartialView called UserGrid.cshtml.

Give it a Bottom Bar

Open Views\Shared\UserGrid.cshtml and perform these changes:

At the bottom of the WebGrid, add the following lines to the UserGrid.cshtml right after @using...form closing curly brace (}).

@using (Html.BeginForm("Pager""User"FormMethod.Post, new {@id = "pager", @role = "search"}))
{
    if (Model.PageCount > 0)
     {
         <div class="well well-sm">
             <div class="row">
                 <div class="col-lg-3 col-md-3 text-left">
                     @Html.PageRangeDisplay(Model)
                 </div>
                 <div class="text-center col-lg-6 col-md-6">
                     @Html.Pager(Model.PageSize, Model.PageNumber, Model.TotalItemCount).Options(o => o.DisplayTemplate("BootstrapPagination"))
                 </div>
                 <div class="col-lg-3 col-md-3 text-right">
                     @Html.PageSizeSelector(Model)
                 </div>
             </div>
         </div>
     }
}

What the heck is all of this?

The Html.Pager is a stock Pager HtmlHelper method that came with MvcPaging.

Notice at the end of the method? The Options method tells the Pager what DisplayTemplate to use. We defined it to load our BootstrapPagination.cshtml view. It has it's own model as we've discussed before.

The other two HtmlHelper methods (PageRangeDisplay and PageSizeSelector) we need to create.

So let's get to it.

Help! I need Helpers!

First, let's tackle the easy one: The PageRangeDisplay.

All we need to do is display the current range of records on the screen and show the total. Doesn't sound so hard.

#region Display Record Range
 
public static MvcHtmlString PageRangeDisplay<T>(this HtmlHelper helper, IPagedList<T> list) where T : class
{
    return MvcHtmlString.Create(GetRecordTotal(list).ToHtmlString());
}
 
private static MvcHtmlString GetRecordTotal<T>(IPagedList<T> list)
{
    var recordTotal = new TagBuilder("label");
 
    var start = list.PageIndex * list.PageSize + 1;
    var end = start + list.PageSize - 1;
    if (end > list.TotalItemCount)
    {
        end = list.TotalItemCount;
    }
    recordTotal.InnerHtml = String.Format("{0}-{1} of {2}", start, end, list.TotalItemCount);
 
    return MvcHtmlString.Create(recordTotal.ToString(TagRenderMode.Normal));
}
 
#endregion

The PagedList object contains all of the information we need. As mentioned before, all we're doing is wrapping the records and adding additional properties to help us with our pagination controls.

The PageSize is a little different since it's a dropdown menu with record sizes. It contains a couple if..then's so we need an HtmlHelper instead of muddying up our view with code.

#region Set PageSize
 
public static MvcHtmlString PageSizeSelector<T>(this HtmlHelper helper, 
    IPagedList<T> list) where T : class
{
    var div = new TagBuilder("div");
    div.AddCssClass("btn-group");
 
    TagBuilder rowSelect = GetRowSelect(list);
    div.InnerHtml = String.Format("<label>Show Rows:</label>{0}", 
        rowSelect.ToString(TagRenderMode.Normal));
 
    return new MvcHtmlString(div.ToString(TagRenderMode.Normal));
}
 
private static TagBuilder GetRowSelect<T>(IPagedList<T> list)
{
    var rowSelect = new TagBuilder("select");
    rowSelect.Attributes.Add("id""size");
    rowSelect.Attributes.Add("name""size");
    rowSelect.Attributes.Add("class""size");
    // Define the amount of rows to return.
    var rows = new Dictionary<stringstring>
        {
            {"10""10"},
            {"25""25"},
            {"50""50"},
            {"100""100"},
            {"500""500"}
        };
 
    var rowBuilder = new StringBuilder();
    foreach (var row in rows)
    {
        int count;
        if (!int.TryParse(row.Value, out count))
        {
            count = 0;
        }
        rowBuilder.AppendFormat(
            count == list.PageSize
                ? "<option selected=\"selected\" value=\"{0}\">{1}</option>"
                : "<option value=\"{0}\">{1}</option>", row.Value, row.Key);
    }
    rowSelect.InnerHtml = rowBuilder.ToString();
 
    return rowSelect;
}
 
#endregion

Of course, you can put any page size you want into the dropdown. It will all work the same.

So now we have a complete HtmlHelper PagingExtensions.cs file.

Helpers\Html\PagingExtensions.cs

public static class PagingExtensions
{
    #region Set PageSize
 
    public static MvcHtmlString PageSizeSelector<T>(this HtmlHelper helper, 
        IPagedList<T> list) where T : class
    {
        var div = new TagBuilder("div");
        div.AddCssClass("btn-group");
 
        TagBuilder rowSelect = GetRowSelect(list);
        div.InnerHtml = String.Format("<label>Show Rows:</label>{0}", 
            rowSelect.ToString(TagRenderMode.Normal));
 
        return new MvcHtmlString(div.ToString(TagRenderMode.Normal));
    }
 
    private static TagBuilder GetRowSelect<T>(IPagedList<T> list)
    {
        var rowSelect = new TagBuilder("select");
        rowSelect.Attributes.Add("id""size");
        rowSelect.Attributes.Add("name""size");
        rowSelect.Attributes.Add("class""size");
        // Define the amount of rows to return.
        var rows = new Dictionary<stringstring>
            {
                {"10""10"},
                {"25""25"},
                {"50""50"},
                {"100""100"},
                {"500""500"}
            };
 
        var rowBuilder = new StringBuilder();
        foreach (var row in rows)
        {
            int count;
            if (!int.TryParse(row.Value, out count))
            {
                count = 0;
            }
            rowBuilder.AppendFormat(
                count == list.PageSize
                    ? "<option selected=\"selected\" value=\"{0}\">{1}</option>"
                    : "<option value=\"{0}\">{1}</option>", row.Value, row.Key);
        }
        rowSelect.InnerHtml = rowBuilder.ToString();
 
        return rowSelect;
    }
 
    #endregion
 
    #region Display Record Range
 
    public static MvcHtmlString PageRangeDisplay<T>(this HtmlHelper helper, IPagedList<T> list) where T : class
    {
        return MvcHtmlString.Create(GetRecordTotal(list).ToHtmlString());
    }
 
    private static MvcHtmlString GetRecordTotal<T>(IPagedList<T> list)
    {
        var recordTotal = new TagBuilder("label");
 
        var start = list.PageIndex * list.PageSize + 1;
        var end = start + list.PageSize - 1;
        if (end > list.TotalItemCount)
        {
            end = list.TotalItemCount;
        }
        recordTotal.InnerHtml = String.Format("{0}-{1} of {2}", start, end, list.TotalItemCount);
 
        return MvcHtmlString.Create(recordTotal.ToString(TagRenderMode.Normal));
    }
 
    #endregion
 
}

Once we include this in our View, we should be all set to see the "fruits of our labors!"

What a View!

After looking at the View with the pagination bar across the bottom, I noticed that the pagination class (.pagination) has a margin top and bottom of 20px. We need to add a quick CSS style to make this bar a little sleeker.

Add this quick change of style to the _Layout.cshtml in the header.

<style>
    .pagination {margin0 !important}
</style>

Our View now looks like this:

We can see that we have our current PageRangeDisplay, our paging buttons are working, and our selectable PageSize is...broken.

Hmmm...We forgot something.

We need to trigger a form-submit when we change the PageSize.

Hold your Horses!

However, before we get started on the JavaScript-y goodness, we need to determine what happens when the form submits.

Remember the Form that contains the Pager ActionResult for the controller? We didn't create that yet.

So let's add our new ActionResult to our UserController.cs

[HttpPost]
public ActionResult Pager(FormCollection forms)
{
    int size;
    if (!TryParse(forms["size"], out size))
    {
        size = 0;
    }
 
    return Redirect(Url.MainUrl(size));
}

Simple enough, right?

We also have to add a UrlHelper for our MainUrl(size).

Helpers\Url\UrlHelperExtensions.cs

public static class UrlHelperExtensions
{
    public static string MainUrl(this UrlHelper helper, int size)
    {
        return helper.Content(string.Format("~/?size={0}", size));
    }
}

So when we select a page size for our webgrid, we perform a postback and immediately redirect back to the main page with the pagesize (size) parameter.

Based on our custom ModelBinder, it will pick it up through the Url and pass it into the webGrid and automatically set our PageSizeSelector as well.

All in a nice, neat package.

Going JavaScript-y

The only thing missing is the post to the Pager ActionResult. When we change the rows on the PageSizeSelector, we should perform a post.

This is simple enough since we've isolated the paging to our Pager form.

Add this JavaScript to your _Layout.cshtml:

$("#size").on("change"function() {
    $("#pager").submit();
});

Once you have this in place, you'll have a functioning pagination bar for every WebGrid you want to build.

Conclusion

Today, I tried to give you a better post than Wednesday. I felt like I left everyone hanging, so I thought this would be a great post to finish off paging with WebGrids. 

If you missed the WebGrid series, I would start with ASP.NET MVC: Enhancing the WebGrid Series Introduction to see how far we've come...and yet, I still forgot about paging. 

If you have any questions about this code, don't hesitate to post.

Do you have a cool technique regarding pagination? Post it below in the comments.