May 26, 2012

Paging with RavenDB and ASP.NET MVC

The RavenDB LINQ client supports both Skip and Take functions, making paging through a collection super easy:

int pageSize = 10;
int pageIndex = 2;

var candidates = session.Query<Candidate>()
				.Skip((pageIndex - 1) * pageSize)
				.Take(pageSize)
				.ToList();

Note: The page number requested by the user is normally 1-based, hence the need to do pageIndex - 1.

To add paging to your UI you’ll typically need the following information:

  • Page index
  • Page size
  • Total number of items
  • Total Pages
  • Whether there is a previous page
  • Whether there is a next page

Most of the above can be calculated from the total number of query results, which RavenDB conveniently provides via the RavenQueryStatistics object.

RavenQueryStatistics stats;
var candidates = session.Query<Candidate>()
				.Statistics(out stats)
				.Skip((pageIndex - 1) * pageSize)
				.Take(pageSize)
				.ToList();
				
var totalNumberOfItems = stats.TotalResults;

To encapsulate the above we use the following “PagedList” implementation. In addition to automatically calculating the total number of records (issuing a Count() on the input query/collection) you can also specify it manually, perfect for both NHibernate future queries and of course RavenDB.

public interface IPagedList : IEnumerable
{
    int PageIndex { get; }
    int PageSize { get; }
    int TotalCount { get; }
    int TotalPages { get; }
    bool HasPreviousPage { get; }
    bool HasNextPage { get; }
}

public interface IPagedList<T> : IPagedList, IList<T>
{
}

public class PagedList<T> : List<T>, IPagedList<T>
{
    public PagedList(IEnumerable<T> source, int pageIndex, int pageSize) :
        this(source.GetPage(pageIndex, pageSize), pageIndex, pageSize, source.Count()) { }

    public PagedList(IEnumerable<T> source, int pageIndex, int pageSize, int totalCount)
    {
        this.TotalCount = totalCount;
        this.TotalPages = totalCount / pageSize;

        if (totalCount % pageSize > 0)
            TotalPages++;

        this.PageSize = pageSize;
        this.PageIndex = pageIndex;

        this.AddRange(source.ToList());
    }

    public int PageIndex { get; private set; }
    public int PageSize { get; private set; }
    public int TotalCount { get; private set; }
    public int TotalPages { get; private set; }

    public bool HasPreviousPage { get { return (PageIndex > 0); } }
    public bool HasNextPage { get { return (PageIndex + 1 < TotalPages); } }
}

Let’s look at how we use this in our ASP.NET MVC controllers:

[HttpGet]
public ViewResult Index(int page = 1)
{
	int pageSize = 20;

	RavenQueryStatistics stats;
	var candidates = session.Query<Fabrik.Recruit.Domain.Candidate>()
						.Statistics(out stats)
						.OrderBy(c => c.ContactDetails.LastName)
						.Skip((page - 1) * pageSize)
						.Take(pageSize)
						.Select(c => new CandidatesIndexModel.CandidateContactCard
						{
							Id = c.Id,
							Name = c.ContactDetails.FullName,
						})
						.ToList();

	var model = new PagedList<CandidatesIndexModel.CandidateContactCard>(
					candidates, 
					page - 1, 
					pageSize, 
					stats.TotalResults
				);

	return View(model);                      
}

To create the pager in our views I created the following razor helper (twitter bootstrap compatible):

@using System;

@helper If(bool condition, string then) {
    if(condition) { <text>@then</text> }
}

@helper Pager(int currentPage, int totalPages, Func<int, string> pageUrl, string cssClass = null) {
    if (totalPages > 1) { 
    <div class="pagination @cssClass">
        <ul>
            @for (int i = 1; i < totalPages + 1; i++) { 
                <li @If(i == (currentPage + 1), then: "class=active")>
					<a href="@pageUrl(i)">@i</a>
				</li>
            }
        </ul>
    </div>
    }
}

The helper takes in a Func<int, string> for generating the page link urls. Assuming our view model is a IPagedList we add a pager like so:

@RazorHelpers.Pager(
	Model.PageIndex, 
	Model.TotalPages, 
	x => Url.Action("index", new { page = x })
)

Paging has never been so simple.

© 2022 Ben Foster