Mostefai Mohammed Amine's Blog

Say That I Was Here !

Creating an adaptive pager using ASP.NET MVC 4 HTML helpers and Twitter Bootstrap 3

Twitter released recently their brand new Bootstrap and they changed radically many things and notably the paging system. In this post, I will show how to create an ASP.NET MVC4 HTML Helper that renders an adaptive pager. The first thing I have to explain is what is an adaptive pager ? Imagine a collection of 1000 records, if the pager uses a page size of 20 records, the pager will renders 50 links which is not a very positive rendering manner according to the modern web page rules. Accordingly, an adaptive pager is constituted by three sections : the first section contains the three first pages, the last section contains the three last pages and the middle section contains either the current page (if the current page is neither in the first par nor the last part) or the middle pages. An adaptive pager style by bootstrap would look like this : To create the helper, we have to create a static class called “HtmlRenderExtensions”. In this class, we will create an extension method callded “RenderPager” like this : public static HtmlString RenderPager(this HtmlHelper html, string controllerName, string actionName, int recordNumber, int pageSize, int currentPage) { } Let’s calculate the number of pages with this simple formula: // calculate the number of pages var numberOfPages = recordNumber / pageSize; if (recordNumber % pageSize != 0) ++numberOfPages; Of course, if there not enough pages (at least 2) we don’t need pagination : if (numberOfPages < 2) return new HtmlString(string.Empty); .csharpcode, .csharpcode pre { font-size: small; color: black; font-family: consolas, "Courier New", courier, monospace; background-color: #ffffff; /*white-space: pre;*/ } .csharpcode pre { margin: 0em; } .csharpcode .rem { color: #008000; } .csharpcode .kwrd { color: #0000ff; } .csharpcode .str { color: #006080; } .csharpcode .op { color: #0000c0; } .csharpcode .preproc { color: #cc6633; } .csharpcode .asp { background-color: #ffff00; } .csharpcode .html { color: #800000; } .csharpcode .attr { color: #ff0000; } .csharpcode .alt { background-color: #f4f4f4; width: 100%; margin: 0em; } .csharpcode .lnum { color: #606060; } The first thing to do is to create an URL helper to generate the links URLs : // create an URL helper to generate urls var urlHelper = new UrlHelper(html.ViewContext.RequestContext, html.RouteCollection); var link = urlHelper.Action(actionName, controllerName); After that, we need a string builder to generate the HTML markup: StringBuilder builder = new StringBuilder(); After that, the bootstrap, the pagination is built using an unordered list (with the class “pagination”), that’s why we’ll append with an opening “ul” tag : builder.Append("<ul class=\"pagination\">"); .csharpcode, .csharpcode pre { font-size: small; color: black; font-family: consolas, "Courier New", courier, monospace; background-color: #ffffff; /*white-space: pre;*/ } .csharpcode pre { margin: 0em; } .csharpcode .rem { color: #008000; } .csharpcode .kwrd { color: #0000ff; } .csharpcode .str { color: #006080; } .csharpcode .op { color: #0000c0; } .csharpcode .preproc { color: #cc6633; } .csharpcode .asp { background-color: #ffff00; } .csharpcode .html { color: #800000; } .csharpcode .attr { color: #ff0000; } .csharpcode .alt { background-color: #f4f4f4; width: 100%; margin: 0em; } .csharpcode .lnum { color: #606060; }We need a method that generates the list item with the associated links, we’ll call this method “AppendPagerTag” and its code is as follow: private static void AppendPagerTag(StringBuilder builder, int targetPage, UrlHelper helper, string controllerName, string actionName, int currentPage, string tagText = null) { // the link markup string linkTag = ""; // the active css string activeCss = ""; // the page text if (tagText == null) tagText = targetPage.ToString(); // a positive value of targetPage points to a real page while a negative value points to a simple text (span) if (targetPage > 0) { // if the target page is the current page, then we'll add the "active" class to the item if (targetPage == currentPage) activeCss = "active"; var link = helper.Action(actionName, controllerName, new { page = targetPage }); // generate the link markup linkTag = string.Format("<a href=\"{1}\">{0}</a>", tagText, link); } else // generates the separator markup linkTag = string.Format("<span>{0}</span>", tagText); // embed the generated markup in a list item builder.AppendFormat("<li class=\"{1}\">{0}</li>", linkTag, activeCss); } .csharpcode, .csharpcode pre { font-size: small; color: black; font-family: consolas, "Courier New", courier, monospace; background-color: #ffffff; /*white-space: pre;*/ } .csharpcode pre { margin: 0em; } .csharpcode .rem { color: #008000; } .csharpcode .kwrd { color: #0000ff; } .csharpcode .str { color: #006080; } .csharpcode .op { color: #0000c0; } .csharpcode .preproc { color: #cc6633; } .csharpcode .asp { background-color: #ffff00; } .csharpcode .html { color: #800000; } .csharpcode .attr { color: #ff0000; } .csharpcode .alt { background-color: #f4f4f4; width: 100%; margin: 0em; } .csharpcode .lnum { color: #606060; } When we pass negative value, this method will generate separators instead of links. Otherwise, it will generate a link with a different page parameter. The next thing to do in the extension method is to generate the “previous” link if the current page is higher than “1”. if (currentPage > 1) AppendPagerTag(builder, currentPage - 1, urlHelper, controllerName, actionName, currentPage, "&laquo;"); The pager is divided generally in three sections : the first that contains the first pages, the last that contains the last pages and the middle section that contains some calculated pages.Let’s set the first section : // the first section contains the first pages IEnumerable<int> section1 = new int[] { 1, 2, 3 }.ToList(); .csharpcode, .csharpcode pre { font-size: small; color: black; font-family: consolas, "Courier New", courier, monospace; background-color: #ffffff; /*white-space: pre;*/ } .csharpcode pre { margin: 0em; } .csharpcode .rem { color: #008000; } .csharpcode .kwrd { color: #0000ff; } .csharpcode .str { color: #006080; } .csharpcode .op { color: #0000c0; } .csharpcode .preproc { color: #cc6633; } .csharpcode .asp { background-color: #ffff00; } .csharpcode .html { color: #800000; } .csharpcode .attr { color: #ff0000; } .csharpcode .alt { background-color: #f4f4f4; width: 100%; margin: 0em; } .csharpcode .lnum { color: #606060; }Let’s now set the last section : // the last section contains the last pages IEnumerable<int> section3 = new int[] { numberOfPages - 2, numberOfPages - 1, numberOfPages }.ToList(); .csharpcode, .csharpcode pre { font-size: small; color: black; font-family: consolas, "Courier New", courier, monospace; background-color: #ffffff; /*white-space: pre;*/ } .csharpcode pre { margin: 0em; } .csharpcode .rem { color: #008000; } .csharpcode .kwrd { color: #0000ff; } .csharpcode .str { color: #006080; } .csharpcode .op { color: #0000c0; } .csharpcode .preproc { color: #cc6633; } .csharpcode .asp { background-color: #ffff00; } .csharpcode .html { color: #800000; } .csharpcode .attr { color: #ff0000; } .csharpcode .alt { background-color: #f4f4f4; width: 100%; margin: 0em; } .csharpcode .lnum { color: #606060; }Let’s now calculate the middle section which is a special section : // calculate the floating middle section. If the current page is in the middle, the floating section is a region that // contains the current page otherwise, it's the region that contains the middle pages int middleStart; if ((currentPage <= 2) || (currentPage >= numberOfPages - 1)) { middleStart = numberOfPages / 2; if (middleStart < 5) middleStart = 5; } else if ((currentPage >= 3) && (currentPage < 6) && (currentPage < numberOfPages - 2)) { middleStart = 5; } else middleStart = currentPage; var middle = new int[] { middleStart - 1, middleStart, middleStart + 1 }; .csharpcode, .csharpcode pre { font-size: small; color: black; font-family: consolas, "Courier New", courier, monospace; background-color: #ffffff; /*white-space: pre;*/ } .csharpcode pre { margin: 0em; } .csharpcode .rem { color: #008000; } .csharpcode .kwrd { color: #0000ff; } .csharpcode .str { color: #006080; } .csharpcode .op { color: #0000c0; } .csharpcode .preproc { color: #cc6633; } .csharpcode .asp { background-color: #ffff00; } .csharpcode .html { color: #800000; } .csharpcode .attr { color: #ff0000; } .csharpcode .alt { background-color: #f4f4f4; width: 100%; margin: 0em; } .csharpcode .lnum { color: #606060; }Using the magic of Linq, we’ll constitute the full pages list that is composed of the three sections and eventually some separators if the current page do not belong to the first or the last section. // create the list of pages that are composed of the three sections and eventual separators that are represented by negative numbers (-99 and -98) IEnumerable<int> pages = section1; if (middle.First() > 4) pages = pages.Union(new int[] { -98 }); pages = pages.Union(middle); if (middle.Last() < numberOfPages - 3) pages = pages.Union(new int[] { -99 }); .csharpcode, .csharpcode pre { font-size: small; color: black; font-family: consolas, "Courier New", courier, monospace; background-color: #ffffff; /*white-space: pre;*/ } .csharpcode pre { margin: 0em; } .csharpcode .rem { color: #008000; } .csharpcode .kwrd { color: #0000ff; } .csharpcode .str { color: #006080; } .csharpcode .op { color: #0000c0; } .csharpcode .preproc { color: #cc6633; } .csharpcode .asp { background-color: #ffff00; } .csharpcode .html { color: #800000; } .csharpcode .attr { color: #ff0000; } .csharpcode .alt { background-color: #f4f4f4; width: 100%; margin: 0em; } .csharpcode .lnum { color: #606060; } Let’s browse the generated collection and eliminate the pages that are not coherent according to the collection : // filter the pages to take into account only the coherent pages by eliminating redundancies and illogical pages foreach (var page in pages.Where(e => (e <= numberOfPages && e > 0) || e == -99 || e == -98).Distinct()) { if (page > 0) AppendPagerTag(builder, page, urlHelper, controllerName, actionName, currentPage); else AppendPagerTag(builder, page, urlHelper, controllerName, actionName, currentPage, "..."); } .csharpcode, .csharpcode pre { font-size: small; color: black; font-family: consolas, "Courier New", courier, monospace; background-color: #ffffff; /*white-space: pre;*/ } .csharpcode pre { margin: 0em; } .csharpcode .rem { color: #008000; } .csharpcode .kwrd { color: #0000ff; } .csharpcode .str { color: #006080; } .csharpcode .op { color: #0000c0; } .csharpcode .preproc { color: #cc6633; } .csharpcode .asp { background-color: #ffff00; } .csharpcode .html { color: #800000; } .csharpcode .attr { color: #ff0000; } .csharpcode .alt { background-color: #f4f4f4; width: 100%; margin: 0em; } .csharpcode .lnum { color: #606060; } .csharpcode, .csharpcode pre { font-size: small; color: black; font-family: consolas, "Courier New", courier, monospace; background-color: #ffffff; /*white-space: pre;*/ } .csharpcode pre { margin: 0em; } .csharpcode .rem { color: #008000; } .csharpcode .kwrd { color: #0000ff; } .csharpcode .str { color: #006080; } .csharpcode .op { color: #0000c0; } .csharpcode .preproc { color: #cc6633; } .csharpcode .asp { background-color: #ffff00; } .csharpcode .html { color: #800000; } .csharpcode .attr { color: #ff0000; } .csharpcode .alt { background-color: #f4f4f4; width: 100%; margin: 0em; } .csharpcode .lnum { color: #606060; }If we are not in the last page, we need to generate the “next” page : // generate the next page if we are not in the last page if (currentPage < numberOfPages) AppendPagerTag(builder, currentPage + 1, urlHelper, controllerName, actionName, currentPage, "&raquo;"); .csharpcode, .csharpcode pre { font-size: small; color: black; font-family: consolas, "Courier New", courier, monospace; background-color: #ffffff; /*white-space: pre;*/ } .csharpcode pre { margin: 0em; } .csharpcode .rem { color: #008000; } .csharpcode .kwrd { color: #0000ff; } .csharpcode .str { color: #006080; } .csharpcode .op { color: #0000c0; } .csharpcode .preproc { color: #cc6633; } .csharpcode .asp { background-color: #ffff00; } .csharpcode .html { color: #800000; } .csharpcode .attr { color: #ff0000; } .csharpcode .alt { background-color: #f4f4f4; width: 100%; margin: 0em; } .csharpcode .lnum { color: #606060; }Finally, let’s close the “ul” and stop the rendering process : // generate the next page if we are not in the last page if (currentPage < numberOfPages) AppendPagerTag(builder, currentPage + 1, urlHelper, controllerName, actionName, currentPage, "&raquo;"); builder.AppendFormat("</ul>"); return new HtmlString(builder.ToString()); .csharpcode, .csharpcode pre { font-size: small; color: black; font-family: consolas, "Courier New", courier, monospace; background-color: #ffffff; /*white-space: pre;*/ } .csharpcode pre { margin: 0em; } .csharpcode .rem { color: #008000; } .csharpcode .kwrd { color: #0000ff; } .csharpcode .str { color: #006080; } .csharpcode .op { color: #0000c0; } .csharpcode .preproc { color: #cc6633; } .csharpcode .asp { background-color: #ffff00; } .csharpcode .html { color: #800000; } .csharpcode .attr { color: #ff0000; } .csharpcode .alt { background-color: #f4f4f4; width: 100%; margin: 0em; } .csharpcode .lnum { color: #606060; } The complete method code is as follow: public static HtmlString RenderPager(this HtmlHelper html, string controllerName, string actionName, int recordNumber, int pageSize, int currentPage) { // calculate the number of pages var numberOfPages = recordNumber / pageSize; if (recordNumber % pageSize != 0) ++numberOfPages; if (numberOfPages < 2) return new HtmlString(string.Empty); // create an URL helper to generate urls var urlHelper = new UrlHelper(html.ViewContext.RequestContext, html.RouteCollection); var link = urlHelper.Action(actionName, controllerName); // create a string builder to generate HTML StringBuilder builder = new StringBuilder(); builder.Append("<ul class=\"pagination\">"); // generate the previous "link" if (currentPage > 1) AppendPagerTag(builder, currentPage - 1, urlHelper, controllerName, actionName, currentPage, "&laquo;"); // the first section contains the first pages IEnumerable<int> section1 = new int[] { 1, 2, 3 }.ToList(); // the last section contains the last pages IEnumerable<int> section3 = new int[] { numberOfPages - 2, numberOfPages - 1, numberOfPages }.ToList(); // calculate the floating middle section. If the current page is in the middle, the floating section is a region that // contains the current page otherwise, it's the region that contains the middle pages int middleStart; if ((currentPage <= 2) || (currentPage >= numberOfPages - 1)) { middleStart = numberOfPages / 2; if (middleStart < 5) middleStart = 5; } else if ((currentPage >= 3) && (currentPage < 6) && (currentPage < numberOfPages - 2)) { middleStart = 5; } else middleStart = currentPage; var middle = new int[] { middleStart - 1, middleStart, middleStart + 1 }; // create the list of pages that are composed of the three sections and eventual separators that are represented by negative numbers (-99 and -98) IEnumerable<int> pages = section1; if (middle.First() > 4) pages = pages.Union(new int[] { -98 }); pages = pages.Union(middle); if (middle.Last() < numberOfPages - 3) pages = pages.Union(new int[] { -99 }); pages = pages.Union(section3); // filter the pages to take into account only the coherent pages by eliminating redundancies and illogical pages foreach (var page in pages.Where(e => (e <= numberOfPages && e > 0) || e == -99 || e == -98).Distinct()) { if (page > 0) AppendPagerTag(builder, page, urlHelper, controllerName, actionName, currentPage); else AppendPagerTag(builder, page, urlHelper, controllerName, actionName, currentPage, "..."); } // generate the next page if we are not in the last page if (currentPage < numberOfPages) AppendPagerTag(builder, currentPage + 1, urlHelper, controllerName, actionName, currentPage, "&raquo;"); builder.AppendFormat("</ul>"); return new HtmlString(builder.ToString()); } The pager would look like this : .csharpcode, .csharpcode pre { font-size: small; color: black; font-family: consolas, "Courier New", courier, monospace; background-color: #ffffff; /*white-space: pre;*/ } .csharpcode pre { margin: 0em; } .csharpcode .rem { color: #008000; } .csharpcode .kwrd { color: #0000ff; } .csharpcode .str { color: #006080; } .csharpcode .op { color: #0000c0; } .csharpcode .preproc { color: #cc6633; } .csharpcode .asp { background-color: #ffff00; } .csharpcode .html { color: #800000; } .csharpcode .attr { color: #ff0000; } .csharpcode .alt { background-color: #f4f4f4; width: 100%; margin: 0em; } .csharpcode .lnum { color: #606060; }   The source code is attached with the post The source code is available here Enjoy !