home | articles | site map | contacts
about us
consulting
client login
support
contacts
Smart Page - an MVC C# .NET Pager Control for Search Results
2/20/2012 (Modified 8/28/2012)
Follow PrimaryObjects on Twitter Subscribe to Primary Objects via RSS More Software Articles
by Primary Objects
enter email address

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, 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, 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:
    public static class Html
    {
        /// <summary>
        /// Displays paging for a list of search results.
        /// Created by  http://www.primaryobjects.com/articledirectory.aspx
        /// 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 "";
            }
        }
    }
</int,>
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:
@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:
@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:
<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:
        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.
        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;
        }

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 , Microsoft certified software developer and architect, providing C# ASP .NET web application development, database design, and mobile software development across a variety of domains for clients in both the business and consumer sectors.

   
comments powered by Disqus
Profile
Learn more about Primary Objects and our goals ..  More
12/28/2012
Primary Objects publishes node.js app, RedAnt, a REST web service .. More
09/21/2012
Primary Objects publishes node.js app, CD Early Withdrawal Calculator .. More
Home | About Us | Services | Client Login | Job Opportunities | Contact Us
Copyright © Primary Objects 2013
Privacy Policy
Follow us on Twitter