Introduction

Search is an important part for many C# .NET web applications, comprising of both the display of search results and a navigational control for scrolling through the pages of results. While traditional WebForms in C# ASP .NET included a variety of paging controls, such as the default paging included with the DataGrid or GridView controls, MVC .NET is limited with regard to built-in solutions. For MVC .NET web applications, we’ll need to create our own paging control.

In this tutorial, we’ll create “Smart Page”, an MVC helper method for displaying smart paging results. Our pager will display a specific number of page indexes adjacent to the active page, along with a set number of page indexes in the middle and end. The MVC search results pager will adjust the number of adjacent page indexes displayed, according to which search results page the user is currently browsing. The Smart Page search results pager is completely customizable and robust for displaying slick and smart, search results paging.

See It In Action

Vanilla Paging

A first try at creating an MVC .NET paging control might appear as follows:

1 1, 2, 3, 4, 5, 6, 7, 8, 9, 10

The above pager displays all available pages of search results (assuming there are 10 pages worth of results to display). While this is a functional solution to search result paging, it can become cumbersome when many search results are required, thus expanding the list of pages across the screen. A better solution, would be an adaptable search results pager, which limits the number of pages displayed to those the user may be interested in.

Smart Paging

An implementation of smart paging, who’s algorithm originally comes from the traditional Digg-style search results paging interface, can appear as follows:

1 1, 2 ... 4, [5], 6 ... 9, 10

Notice in the above, the active search result page would be page #5. The smart pager has adjusted the view to display the first two pages, the last two pages, and one adjacent page next to the active page. This can be a much more powerful solution for many C# ASP .NET MVC web applications. Since Smart Page is fully customizable, we can adjust the settings for the number of adjacent pages in all 3 sections by passing in a variety of parameters.

The Html Helper Method

We’ll implement Smart Page with an MVC Html helper method. This will allow us to simply pass in the required parameters from our search results paging model, to automatically display the pager. The following algorithm is adapted from a variety of “Digg-style” paging solutions in several languages, and custom coded to work with MVC C# .NET and allow passing in the specific parameters to customize the pager. The code is as follows:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 public static class Html { public static string SmartPage(this HtmlHelper helper, int intCurrentPage, int intPerPage, int intNumberofItems, string pageNumberPrefix, string linkUrl, string onClick, string previousText, string nextText, int minPagesForPaging = 3, int adjacentPageCount = 3, int nonAdjacentPageCount = 1, Func<int, int= "" > pageCalculation = null) { string strPreviousText = previousText; string strNextText = nextText; StringBuilder sb = new StringBuilder(); if (intCurrentPage < 1) { intCurrentPage = 1; } int number_of_pages = (int)Math.Ceiling((double)intNumberofItems / (double)intPerPage); int i = 0; if (number_of_pages > 1) { if (!(intCurrentPage == 1)) { int page = intCurrentPage - 1; if (pageCalculation != null) { page = pageCalculation((intCurrentPage - 1)); } sb. Append ( "<span><a href=" " + linkUrl.Replace(" [PAGE]", page. ToString ()) + "" onclick= "" + onClick. Replace ( "[PAGE]" , page. ToString ()) + "" >" + strPreviousText + "</a></span>" ); } else { sb. Append ( "<span style=" \ "color:#c0c0c0;\" ">" + previousText + "</span>" ); } if (number_of_pages < minPagesForPaging) { for (i = 0; i < number_of_pages; i++) { if (!(i == intCurrentPage - 1)) { int page = i + 1; if (pageCalculation != null) { page = pageCalculation((i + 1)); } sb. Append ( "<span><a href=" " + linkUrl. Replace ( "[PAGE]" , page. ToString ()) + "" onclick= "" + onClick. Replace ( "[PAGE]" , page. ToString ()) + "" >" + (i + 1) + pageNumberPrefix + "</a></span>" ); } else { sb. Append ( "<span>" + (i + 1) + pageNumberPrefix + "</span>" ); } } } else { if (intCurrentPage < adjacentPageCount) { for (i = 0; i < adjacentPageCount; i++) { if (!(i == intCurrentPage - 1)) { int page = i + 1; if (pageCalculation != null) { page = pageCalculation((i + 1)); } sb. Append ( "<span><a href=" " + linkUrl. Replace ( "[PAGE]" , page. ToString ()) + "" onclick= "" + onClick. Replace ( "[PAGE]" , page. ToString ()) + "" >" + (i + 1) + pageNumberPrefix + "</a></span>" ); } else { sb. Append ( "<span>" + (i + 1) + pageNumberPrefix + "</span>" ); } } sb. Append ( "<span class=" pg_dots ">...</span>" ); for (i = number_of_pages - nonAdjacentPageCount; i < number_of_pages; i++) { if (!(i == intCurrentPage - 1)) { int page = i + 1; if (pageCalculation != null) { page = pageCalculation((i + 1)); } sb. Append ( "<span><a href=" " + linkUrl. Replace ( "[PAGE]" , page. ToString ()) + "" onclick= "" + onClick. Replace ( "[PAGE]" , page. ToString ()) + "" >" + (i + 1) + pageNumberPrefix + "</a></span>" ); } else { sb. Append ( "<span>" + (i + 1) + pageNumberPrefix + "</span>" ); } } } else if (intCurrentPage > number_of_pages - (adjacentPageCount - 1)) { for (i = 0; i < nonAdjacentPageCount; i++) { if (!(i == intCurrentPage - 1)) { int page = i + 1; if (pageCalculation != null) { page = pageCalculation((i + 1)); } sb. Append ( "<span><a href=" " + linkUrl. Replace ( "[PAGE]" , page. ToString ()) + "" onclick= "" + onClick. Replace ( "[PAGE]" , page. ToString ()) + "" >" + (i + 1) + pageNumberPrefix + "</a></span>" ); } else { sb. Append ( "<span>" + (i + 1) + pageNumberPrefix + "</span>" ); } } sb. Append ( "<span class=" pg_dots ">...</span>" ); for (i = number_of_pages - adjacentPageCount; i < number_of_pages; i++) { if (!(i == intCurrentPage - 1)) { int page = i + 1; if (pageCalculation != null) { page = pageCalculation((i + 1)); } sb. Append ( "<span><a href=" " + linkUrl. Replace ( "[PAGE]" , page. ToString ()) + "" onclick= "" + onClick. Replace ( "[PAGE]" , page. ToString ()) + "" >" + (i + 1) + pageNumberPrefix + "</a></span>" ); } else { sb. Append ( "<span>" + (i + 1) + pageNumberPrefix + "</span>" ); } } } else { for (i = 0; i < nonAdjacentPageCount; i++) { if (!(i == intCurrentPage - 1)) { int page = i + 1; if (pageCalculation != null) { page = pageCalculation((i + 1)); } sb. Append ( "<span><a href=" " + linkUrl. Replace ( "[PAGE]" , page. ToString ()) + "" onclick= "" + onClick. Replace ( "[PAGE]" , page. ToString ()) + "" >" + (i + 1) + pageNumberPrefix + "</a></span>" ); } else { sb. Append ( "<span>" + (i + 1) + pageNumberPrefix + "</span>" ); } } sb. Append ( "<span class=" pg_dots ">...</span>" ); for (i = intCurrentPage - (adjacentPageCount / 2); i <= intCurrentPage + (adjacentPageCount / 2); i++) { if (i != intCurrentPage) { int page = i; if (pageCalculation != null) { page = pageCalculation(i); } sb. Append ( "<span><a href=" " + linkUrl. Replace ( "[PAGE]" , page. ToString ()) + "" onclick= "" + onClick. Replace ( "[PAGE]" , page. ToString ()) + "" >" + i + pageNumberPrefix + "</a></span>" ); } else { sb. Append ( "<span>" + i + pageNumberPrefix + "</span>" ); } } sb. Append ( "<span>...</span>" ); for (i = number_of_pages - nonAdjacentPageCount; i < number_of_pages; i++) { if (!(i == intCurrentPage - 1)) { int page = i + 1; if (pageCalculation != null) { page = pageCalculation((i + 1)); } sb. Append ( "<span><a href=" " + linkUrl. Replace ( "[PAGE]" , page. ToString ()) + "" onclick= "" + onClick. Replace ( "[PAGE]" , page. ToString ()) + "" >" + (i + 1) + pageNumberPrefix + "</a></span>" ); } else { sb. Append ( "<span>" + (i + 1) + pageNumberPrefix + "</span>" ); } } } } if (intCurrentPage != number_of_pages) { int page = intCurrentPage + 1; if (pageCalculation != null) { page = pageCalculation((intCurrentPage + 1)); } sb. Append ( "<span><a href=" " + linkUrl. Replace ( "[PAGE]" , page. ToString ()) + "" onclick= "" + onClick. Replace ( "[PAGE]" , page. ToString ()) + "" >" + strNextText + "</a></span>" ); } else { sb. Append ( "<span style=" \ "color:#c0c0c0;\" ">" + nextText + "</span>" ); } sb. Append ( "{C}<!-- end of 'paging' -->" ); return sb. ToString (); } else { return "" ; } } }

The above code has 3 scenarios: a left-most scenario when the user is active on the first few pages, a right-most scenario when the user is active on the last few pages, and a middle scenario when the user is active somewhere in the middle of the search results. This allows us to determine the placement and draw the MVC search results pager control with the desired settings.

The Search Results View

Since Smart Page is an MVC Html helper method, we can create an MVC partial view to host the search results and pager, as follows:

1 2 3 4 5 6 7 8 9 10 11 12 @model SearchModel @Html.Partial( "/Views/Controls/SearchPaging.cshtml" , new PagingModel(Model .TotalResults , Model .CurrentPage , Model .PageSize , Model .CurrentMax , Model .CurrentMin , Model .AdjacentPageCount , Model.NonAdjacentPageCount)) <!-- Search Results --> @foreach (Treasure treasure in Model.TreasureList) { [@treasure.Id] @treasure .Name - $@treasure .Value }

Notice in the above view, we have two main sections. The top section draws our pager partial view and the bottom section draws our search results. Our model is SearchModel, which contains both the pager settings (so we know what to pass Smart Page, to draw the pager) and our actual search results for rendering in the web page.

The Pager View

Our pager view will call the actual Smart Page Html helper method, as follows:

1 2 3 4 @model PagingModel Displaying @Model .CurrentResultsMin - @Model .CurrentResultsMax of @Model .TotalResults results @Html .Raw(Html.SmartPage(Model.CurrentPage, Model.PageSize, Model.TotalResults, "|" , "#" , "onSmartPage([PAGE]);" , "< Prev" , "Next >" , 3 , Model.AdjacentPageCount, Model.NonAdjacentPageCount))

Notice the above code takes a different model than the search results view. It actually takes a subset of the fields from SearchModel, specifically those required for displaying the smart pager control. We pass the required parameters to our MVC helper method to draw the MVC C# .NET search results paging. Notice we use the parameter [PAGE] to automatically insert the specific page number into our resulting hyperlink for the page link. Smart Page allows using this variable in the href parameter, as well as the onclick parameter (since either event may be used when clicking a paging link. In addition to passing in the current page, total results, page size, and other properties, Smart Page also includes a delegate method for passing an optional function to change the value of [PAGE], if you require. Normally, Smart Page will simply insert the page index into the link, which you would use in your engine to run the search. However, depending on your backing search engine, you may require a different value, such as the actual hit index (rather than the page index). You can use the delegate method to alter the [PAGE] property in this manner.

Our Main Page

We can tie the two partial views together to create our search page, as follows:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 < script type = "text/javascript" > function onSmartPage ( page ) { $( '#page' ).val(page); $( '#searchForm' ).submit(); } </ script > < h2 > Search Results Paging Example </ h2 > @using (Ajax.BeginForm("Index", null, new AjaxOptions { UpdateTargetId = "searchResultsDiv" }, new { Id = "searchForm" })) { @Html.Hidden("page", Model.CurrentPage) < div id = "searchResultsDiv" > @Html.Partial("/Views/Controls/SearchResults.cshtml") </ div > }

Notice the above simply wraps our partial view, for search results, within an ajax-compatible form. This will allow us to click the Smart Page pager links and seamlessly update the search results with an ajax callback to the controller. We’ve setup Smart Page to use the onclick event on page links, which calls our onSmartPage(page) javascript method. When activated, we’ll set the form’s hidden form field for “page” to the clicked page index, and then update our search results in the controller, based upon the page value. Our Main Page Controller Our controller code appears as follows:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public ActionResult Index ( int page) { SearchModel searchModel = SearchManager.Search(_treasureList, page, pageSize); if (Request.IsAjaxRequest()) { return PartialView ( "/Views/Controls/SearchResults.cshtml" , searchModel) ; } else { return View (searchModel) ; } }

In the above code, we simply take the page value from our hidden form field and pass this to our search engine to process and return results. We also include a check for ajax vs a regular page load to know which type of view to return.

Creating a Fake Search Engine

In the example project, we’ve implemented a simulation of a search engine. We simply create a static list of Treasure and provide a Search() method that returns results from this list, according to the page selected by the user from the Smart Page control, and the configured page size.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public static SearchModel Search(List<treasure> treasureList, int page, int pageSize) { SearchModel searchModel = new SearchModel(); int start = (page * pageSize) - pageSize; int end = start + pageSize; if ( end > treasureList.Count) { end = treasureList.Count; } for ( int i = start; i < end ; i++) { searchModel.TreasureList.Add(treasureList[i]); } return searchModel; }

Here is a screenshot of the example application.

Download @ GitHub

You can download the project source code on GitHub by visiting the project home page.

About the Author

This article was written by Kory Becker, software developer and architect, skilled in a range of technologies, including web application development, machine learning, artificial intelligence, and data science.