Smart Page - an MVC C# .NET Pager Control for Search Results

modified

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
{
/// <summary>
/// Displays paging for a list of search results.
/// Created by Kory Becker http://www.primaryobjects.com
/// Modified code, based upon http://www.davidpirek.com/blog/aspnet-mvc-paging-digg-style
/// Example display: Prev 1 2 3 4 5 ... 6 7 8 9 ... 10 11 12 Next
/// </summary>
///<param name="helper" />HtmlHelper
///<param name="intCurrentPage" />Current page index
///<param name="intPerPage" />Number of results per page
///<param name="intNumberofItems" />Total number of results
///<param name="pageNumberPrefix" />Text to place in front of page numbers
///<param name="linkUrl" />Link url to insert into a href="X"
/// (use [PAGE] to replace in the current page)
///<param name="onClick" />Text to include within the onclick property of the link (use [PAGE] to replace in the current page)
///<param name="previousText" />Text to show for "Previous" link
///<param name="nextText" />Text to show for "Next" link
///<param name="minPagesForPaging" />Minimum number of pages in order for paging to display, otherwise all pages are displayed.
///<param name="adjacentPageCount" />Number of pages to show around active page index (including left + right + index).
///For example: 3 => 1, 2, 3 | 2, [3], 4 | 3, [4], 5 | 48, 49, [50]
///<param name="nonAdjacentPageCount" />Number of pages to show for non-active page indexes (such as right-most numbers, if a left-most number is active)
///<param name="pageCalculation" />Optional anonymous method, allowing you alter the returned page index for each link
///by specifying a function that receives the page index and returns the "modified" page index.
/// For example, converting 2 => 21 or converting 272 => 5421. Set to NULL to use the original page index.
/// <returns>string html</returns>
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;

//hide paging if only one page
if (number_of_pages > 1)
{
//previous record
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)
{
// Display all pages, no paging.
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)
{
// Scenario 1: Left-most numbers are active.

// Left-most page numbers.
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>");

// Right-most page numbers.
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))
{
// Scenario 2: Right-most numbers are active.

// Left-most page numbers.
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>");

// Right-most page numbers.
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
{
// Scenario 3: Middle numbers are active.

// Draw left-most numbers.
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>");

// Draw middle numbers.
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>");

// Draw right-most numbers.
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>");
}

}
}
}

//next record
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' -->");

//builds string
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)
{
// Get search results.
SearchModel searchModel = SearchManager.Search(_treasureList, page, pageSize);

// Return results to view.
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();

// Set search points.
int start = (page * pageSize) - pageSize;
int end = start + pageSize;
if (end > treasureList.Count)
{
end = treasureList.Count;
}

// Run search.
for (int i = start; i < end; i++)
{
searchModel.TreasureList.Add(treasureList[i]);
}

// Return results.
return searchModel;
}

Here is a screenshot of the example application.

Screenshot of the example app running SmartPage. Notice the randomly generated treasure items. A nice touch!

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.

Share