How to implement paging & sorting in MVC 3 , Part 1

I have implemented a paging & sorting functions myself in mvc 3. It is extremely easy to integrate and extend, plus is required minimum codes in View & Action method. How cool is that. Let me show you how to use it with a simple example. Example can be downloaded here.

Here we discuss how I implement the PagedList, PagingOption
and Pager and a few extension methods.

PagingOption.cs (using it to pass around the paging & sorting properties)

public class PagingOption : IPagingOption
{
public int Page { get; set; }
public int PageSize { get; set; }
public int? TotalCount { get; set; }
public string SortBy { get; set; }
public bool? SortDescending { get; set; }

public string OrderByExpression
{
get{
return this.SortDescending.HasValue && this.SortDescending.Value ? this.SortBy + " desc" : this.SortBy + " asc";
}
}
}

Pager.cs (it creates the pager html)

	public class Pager
	{
		private ViewContext viewContext;
		private readonly int pageSize;
		private readonly int currentPage;
		private readonly int totalItemCount;
		private readonly RouteValueDictionary linkWithoutPageValuesDictionary;
		private readonly AjaxOptions ajaxOptions;
private readonly string sortBy;
private readonly bool? sortDescending;

		public Pager(ViewContext viewContext, int pageSize, int currentPage, int totalItemCount, RouteValueDictionary valuesDictionary, AjaxOptions ajaxOptions)
		{
			this.viewContext = viewContext;
			this.pageSize = pageSize;
			this.currentPage = currentPage;
			this.totalItemCount = totalItemCount;
			this.linkWithoutPageValuesDictionary = valuesDictionary;
			this.ajaxOptions = ajaxOptions;
		}

public Pager(ViewContext viewContext, int pageSize, int currentPage, int totalItemCount, string sortBy, bool? sortDescending, RouteValueDictionary valuesDictionary, AjaxOptions ajaxOptions)
{
this.viewContext = viewContext;
this.pageSize = pageSize;
this.currentPage = currentPage;
this.totalItemCount = totalItemCount;
this.sortBy = sortBy;
this.sortDescending = sortDescending;
this.linkWithoutPageValuesDictionary = valuesDictionary;
this.ajaxOptions = ajaxOptions;
}

		public HtmlString RenderHtml()
		{
			var pageCount = (int)Math.Ceiling(totalItemCount / (double)pageSize);
			const int nrOfPagesToDisplay = 10;

			var sb = new StringBuilder();

			// Previous
			sb.Append(currentPage > 1 ? GeneratePageLink("&lt;", currentPage - 1) : "<span class=\"disabled\">&lt;</span>");

			var start = 1;
			var end = pageCount;

			if (pageCount > nrOfPagesToDisplay)
			{
				var middle = (int)Math.Ceiling(nrOfPagesToDisplay / 2d) - 1;
				var below = (currentPage - middle);
				var above = (currentPage + middle);

				if (below < 4)
				{
					above = nrOfPagesToDisplay;
					below = 1;
				}
				else if (above > (pageCount - 4))
				{
					above = pageCount;
					below = (pageCount - nrOfPagesToDisplay);
				}

				start = below;
				end = above;
			}

			if (start > 3)
			{
				sb.Append(GeneratePageLink("1", 1));
				sb.Append(GeneratePageLink("2", 2));
				sb.Append("...");
			}

			for (var i = start; i <= end; i++)
			{
				if (i == currentPage || (currentPage <= 0 && i == 0))
				{
					sb.AppendFormat("<span class=\"current\">{0}</span>", i);
				}
				else
				{
					sb.Append(GeneratePageLink(i.ToString(), i));
				}
			}
			if (end < (pageCount - 3))
			{
				sb.Append("...");
				sb.Append(GeneratePageLink((pageCount - 1).ToString(), pageCount - 1));
				sb.Append(GeneratePageLink(pageCount.ToString(), pageCount));
			}

			// Next
			sb.Append(currentPage < pageCount ? GeneratePageLink("&gt;", (currentPage + 1)) : "<span class=\"disabled\">&gt;</span>");

			return new HtmlString(sb.ToString());
		}

		private string GeneratePageLink(string linkText, int pageNumber)
		{
var pageLinkValueDictionary = new RouteValueDictionary(linkWithoutPageValuesDictionary) { {"Page",pageNumber},{"SortBy", sortBy},{"SortDescending",sortDescending} };

			// To be sure we get the right route, ensure the controller and action are specified.
			var routeDataValues = viewContext.RequestContext.RouteData.Values;
			if (!pageLinkValueDictionary.ContainsKey("controller") && routeDataValues.ContainsKey("controller"))
			{
				pageLinkValueDictionary.Add("controller", routeDataValues["controller"]);
			}
			if (!pageLinkValueDictionary.ContainsKey("action") && routeDataValues.ContainsKey("action"))
			{
				pageLinkValueDictionary.Add("action", routeDataValues["action"]);
			}

			// 'Render' virtual path.
			var virtualPathForArea = RouteTable.Routes.GetVirtualPathForArea(viewContext.RequestContext, pageLinkValueDictionary);

			if (virtualPathForArea == null)
				return null;

			var stringBuilder = new StringBuilder("<a");

			if (ajaxOptions != null)
				foreach (var ajaxOption in ajaxOptions.ToUnobtrusiveHtmlAttributes())
					stringBuilder.AppendFormat(" {0}=\"{1}\"", ajaxOption.Key, ajaxOption.Value);

			stringBuilder.AppendFormat(" href=\"{0}\">{1}</a>", virtualPathForArea.VirtualPath, linkText);

			return stringBuilder.ToString();
		}
	}

PagedList.cs (when you retrieve the IQueryable<T> list from repository, and convert it to IPagedList<T>, and use this for the view’s model, so the model will contains all the paging & sorting information)

	public class PagedList<T> : List<T>, IPagedList<T>
	{
		public PagedList(IEnumerable<T> source, IPagingOption option)
: this(source.AsQueryable(), option)
		{
		}

public PagedList(IQueryable<T> source, IPagingOption option)
		{
if(option==null)
throw new ArgumentOutOfRangeException("PagingOption", "Value can not be null.");
			if (option.Page < 0)
				throw new ArgumentOutOfRangeException("index", "Value must be greater than 0.");
			if (option.PageSize < 1)
				throw new ArgumentOutOfRangeException("pageSize", "Value must be greater than 1.");

			if (source == null)
				source = new List<T>().AsQueryable();

			var realTotalCount = source.Count();

			PageSize = option.PageSize;
			PageIndex = option.Page;
			TotalItemCount = option.TotalCount.HasValue ? option.TotalCount.Value : realTotalCount;
			PageCount = TotalItemCount > 0 ? (int)Math.Ceiling(TotalItemCount / (double)PageSize) : 0;

SortBy = option.SortBy;
SortDescending = option.SortDescending;
if(!string.IsNullOrEmpty(option.SortBy))
source = source.OrderBy(option.OrderByExpression);

			HasPreviousPage = (PageIndex > 0);
			HasNextPage = (PageIndex < (PageCount - 1));
			IsFirstPage = (PageIndex <= 0);
			IsLastPage = (PageIndex >= (PageCount - 1));

			if (TotalItemCount <= 0)
				return;

			var realTotalPages = (int)Math.Ceiling(realTotalCount / (double)PageSize);

			if (realTotalCount < TotalItemCount && realTotalPages <= PageIndex)
				AddRange(source.Skip((realTotalPages - 1) * PageSize).Take(PageSize));
			else
				AddRange(source.Skip(PageIndex * PageSize).Take(PageSize));
		}

		#region IPagedList Members

		public int PageCount { get; private set; }
		public int TotalItemCount { get; private set; }
		public int PageIndex { get; private set; }
		public int PageNumber { get { return PageIndex + 1; } }
		public int PageSize { get; private set; }
		public bool HasPreviousPage { get; private set; }
		public bool HasNextPage { get; private set; }
		public bool IsFirstPage { get; private set; }
		public bool IsLastPage { get; private set; }
public string SortBy{ get; private set; }
public bool? SortDescending { get; private set; }
		#endregion
}

PagingExtensions.cs (here are the extension methods to help you generate the html for the view)

public static class PagingExtensions
{
#region AjaxHelper extensions

public static HtmlString Pager(this AjaxHelper ajaxHelper, int pageSize, int currentPage, int totalItemCount, AjaxOptions ajaxOptions)
{
return Pager(ajaxHelper, pageSize, currentPage, totalItemCount, null, null, ajaxOptions);
}

public static HtmlString Pager(this AjaxHelper ajaxHelper, int pageSize, int currentPage, int totalItemCount,string sortBy, bool? sortDescending,string actionName, AjaxOptions ajaxOptions)
{
return Pager(ajaxHelper, pageSize, currentPage, totalItemCount, sortBy, sortDescending,actionName, null, ajaxOptions);
}

public static HtmlString Pager(this AjaxHelper ajaxHelper, int pageSize, int currentPage, int totalItemCount, string actionName, AjaxOptions ajaxOptions)
{
return Pager(ajaxHelper, pageSize, currentPage, totalItemCount, actionName, null, ajaxOptions);
}

public static HtmlString Pager(this AjaxHelper ajaxHelper, int pageSize, int currentPage, int totalItemCount, object values, AjaxOptions ajaxOptions)
{
return Pager(ajaxHelper, pageSize, currentPage, totalItemCount, null, new RouteValueDictionary(values), ajaxOptions);
}

public static HtmlString Pager(this AjaxHelper ajaxHelper, int pageSize, int currentPage, int totalItemCount, string actionName, object values, AjaxOptions ajaxOptions)
{
return Pager(ajaxHelper, pageSize, currentPage, totalItemCount,null,null, actionName, new RouteValueDictionary(values), ajaxOptions);
}

public static HtmlString Pager(this AjaxHelper ajaxHelper, int pageSize, int currentPage, int totalItemCount, RouteValueDictionary valuesDictionary, AjaxOptions ajaxOptions)
{
return Pager(ajaxHelper, pageSize, currentPage, totalItemCount, null, valuesDictionary, ajaxOptions);
}

public static HtmlString Pager(this AjaxHelper ajaxHelper, int pageSize, int currentPage, int totalItemCount, string sortBy, bool? sortDescending, string actionName, RouteValueDictionary valuesDictionary, AjaxOptions ajaxOptions)
{
if (valuesDictionary == null)
{
valuesDictionary = new RouteValueDictionary();
}
if (actionName != null)
{
if (valuesDictionary.ContainsKey("action"))
{
throw new ArgumentException("The valuesDictionary already contains an action.", "actionName");
}
valuesDictionary.Add("action", actionName);
}
var pager = new Pager(ajaxHelper.ViewContext, pageSize, currentPage, totalItemCount,sortBy, sortDescending, valuesDictionary, ajaxOptions);
return pager.RenderHtml();
}

#endregion

#region HtmlHelper extensions

public static HtmlString Pager(this HtmlHelper htmlHelper, int pageSize, int currentPage, int totalItemCount)
{
return Pager(htmlHelper, pageSize, currentPage, totalItemCount, null, null);
}

public static HtmlString Pager(this HtmlHelper htmlHelper, int pageSize, int currentPage, int totalItemCount, string sortBy, bool? sortDescending)
{
return Pager(htmlHelper, pageSize, currentPage, totalItemCount, sortBy, sortDescending, null, null);
}

public static HtmlString Pager(this HtmlHelper htmlHelper, int pageSize, int currentPage, int totalItemCount, string actionName)
{
return Pager(htmlHelper, pageSize, currentPage, totalItemCount, actionName, null);
}

public static HtmlString Pager(this HtmlHelper htmlHelper, int pageSize, int currentPage, int totalItemCount, object values)
{
return Pager(htmlHelper, pageSize, currentPage, totalItemCount, null, new RouteValueDictionary(values));
}

public static HtmlString Pager(this HtmlHelper htmlHelper, int pageSize, int currentPage, int totalItemCount, string actionName, object values)
{
return Pager(htmlHelper, pageSize, currentPage, totalItemCount, null,null,actionName, new RouteValueDictionary(values));
}

public static HtmlString Pager(this HtmlHelper htmlHelper, int pageSize, int currentPage, int totalItemCount, RouteValueDictionary valuesDictionary)
{
return Pager(htmlHelper, pageSize, currentPage, totalItemCount, null, valuesDictionary);
}

public static HtmlString Pager(this HtmlHelper htmlHelper, int pageSize, int currentPage, int totalItemCount, string sortBy, bool? sortDescending, string actionName)
{
return Pager(htmlHelper, pageSize, currentPage, totalItemCount, sortBy, sortDescending, actionName, null);
}

public static HtmlString Pager(this HtmlHelper htmlHelper, int pageSize, int currentPage, int totalItemCount, string sortBy, bool? sortDescending, string actionName, RouteValueDictionary valuesDictionary)
{
if (valuesDictionary == null)
{
valuesDictionary = new RouteValueDictionary();
}
if (actionName != null)
{
if (valuesDictionary.ContainsKey("action"))
{
throw new ArgumentException("The valuesDictionary already contains an action.", "actionName");
}
valuesDictionary.Add("action", actionName);
}
var pager = new Pager(htmlHelper.ViewContext, pageSize, currentPage, totalItemCount,sortBy, sortDescending, valuesDictionary, null);
return pager.RenderHtml();
}

#endregion

#region ActionLink extensions

public static HtmlString ActionLink<T>(this AjaxHelper ajaxHelper, string name, string actionName, IPagedList<T> model, AjaxOptions ajaxOptions)
{
var sortDescending = model.SortBy != null && model.SortBy.Equals(name) && model.SortDescending.HasValue && model.SortDescending.Value ? false : true;
var routeValues = new { SortBy = name, SortDescending = sortDescending };

var css = "";
if (!string.IsNullOrEmpty(model.SortBy) && model.SortBy.Equals(name))
{
if (model.SortDescending.HasValue && model.SortDescending.Value)
css = "sort-desc";
else
css = "sort-asc";
}
else
css = "sort-off";
var htmlAttributes = new { @class = css };
return ajaxHelper.ActionLink(name, actionName, routeValues, ajaxOptions, htmlAttributes);
}

public static HtmlString ActionLink<T>(this HtmlHelper htmlHelper, string name, string actionName, IPagedList<T> model)
{
var sortDescending = model.SortBy != null && model.SortBy.Equals(name) && model.SortDescending.HasValue && model.SortDescending.Value ? false : true;
var routeValues = new { SortBy = name, SortDescending = sortDescending };

var css = "";
if (!string.IsNullOrEmpty(model.SortBy) && model.SortBy.Equals(name))
{
if (model.SortDescending.HasValue && model.SortDescending.Value)
css = "sort-desc";
else
css = "sort-asc";
}
else
css = "sort-off";
var htmlAttributes = new { @class = css };
return htmlHelper.ActionLink(name, actionName, routeValues,htmlAttributes);
}

#endregion

#region IQueryable<T> extensions

public static IPagedList<T> ToPagedList<T>(this IQueryable<T> source, IPagingOption option)
{
return new PagedList<T>(source, option);
}

#endregion

#region IEnumerable<T> extensions

public static IPagedList<T> ToPagedList<T>(this IEnumerable<T> source, IPagingOption option)
{
return new PagedList<T>(source, option);
}

#endregion
}

You will need to download the System.Linq.Dynamic from Microsoft site and reference the namespace to your project. Downloaded from: http://msdn.microsoft.com/en-us/vcsharp/bb894665.aspx For info on using Dynamic LINQ, see http://weblogs.asp.net/scottgu/archive/2008/01/07/dynamic-linq-part-1-using-the-linq-dynamic-query-library.aspx

You can download the example here: https://itsharp-mvc-pagingsorting.googlecode.com/svn/trunk/

Advertisements

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