ASP.NET DataGrid Paging - Custom Paging w/ Caching & Numeric Links
By Curtis Swartzentruber
Published: 3/13/2002
Reader Level: Intermediate
Rated: 4.00 by 1 member(s).
Tell a Friend
Rate this Article
Printable Version
Discuss in the Forums

This tutorial is a variation on Doug Seven's article (ASP.NET DataGrid Paging Part 2 : Custom Paging) . You should read this article first to understand the code and concepts involved in my modification. While my implementation is specific to my particular needs, hopefully you can see how to apply this concept to many different types of result sets.

Variation on Custom Paging

In traditional ASP, one of the harder things to do was to implement an efficient paging system for displaying large amounts of data. So I was very excited about the DataGrid paging options available in ASP.NET. In most cases, the built-in paging fits my needs, but there are a few spots where I need something a bit more robust.

My company runs a small music retail ecommerce site. One feature we have always wanted to implement was letting users browse all the titles in a certain music genre. (The music titles we carry map to one or more GenreIDs, corresponding to Country:Americana or Jazz: Big Band or whatever.) It has been tough to find a traditional ASP solution that scales very well while maintaining at least a psuedo-realtime aspect of dynamic pages.

Now that I am porting our site to the .NET platform, I decided to revisit this problem. The built-in paging functionality in the DataGrid control still fails to scale well with large result sets. It processes all the data every time you navigate to a new page to figure out which records to display. Certain genres may contain thousands of rows, so that might cause some scalability issues.

Desired New Functionality

Fortunately, you can implement custom paging to solve some of these issues, for instance using a stored procedure to only bring back the records needed for the current page. Using Doug's article as an example, I started making some progress. However, there was some additional functionality that I wanted to achieve for my particular scenario.

  1. Limit the result set based on a particular GenreID

    This is a fairly insignificant issue. In Doug's article, he is paging through an entire table to keep his example simple. I just modified his example to fill the temporary table with my own select query, limited by GenreID and some other business logic specific to my needs. As long as your temporary table has the IDENTITY column, you can create and fill it with whatever data you wish to page through.
  2. Limit the impact of loading the complete result set into a temporary table in my stored procedure

    The IDENTITY field in your temporary table lets you choose which records you want to display for the current page (between this number and this number). The nice thing about this technique is you control the amount of data actually being passed to the DataGrid. This cuts down on the amount of data being passed around, especially in a network scenario and speeds up the actual processing time of your datagrid.

    However, each time the user navigates to a new page the stored procedure is called and your temporary table is reloaded.  So now your backend database is working harder. You've just moved your potential scalability problem to your data tier, especially if you try to work with large result sets.

    It seemed that the best solution to this problem was to utilize page caching of some sort. Each time a user can view a cached page, that is one less query that your database has to run.  Basically, a unique page was a combination of the GenreID and the page number.  As long as both the GenreID and page number are passed on the querystring, I can use those 2 values in the VaryByParam parameter for my cache.  Nothing too hard as long as my links were correct.

    Listing 1.1 - The Output Cache designation

    <# OutputCache Duration=x VaryByParam=GenreID;Page %>


    This means that each unique combination of GenreID and Page will be cached.
  3. Provide numeric page links for navigation through the result set, similar to the DataGrid's built-in numeric paging.

    Doug's solution uses a {FirstPage, LastPage, PreviousPage, NextPage} implementation, analogous to the DataGrid PagerStyle-Mode="NextPrev". I wished to use something more like the NumericPages" mode. This allows the user to pick the page number they want and provides a way to skip forward and backward in a large result set. This turned out to be relatively easy with ASP.NET, once I worked through the logic. 

    I implemented the bottom navigation using a single function that takes 2 parameters: the current page number and the total number of pages. The navigation always displays links for the first page and last page. I also hardcoded it to show up to 9 additional page links (the current page and up to 4 in either direction). So it looks something like this:

    Page 11 of 63
    1 ... 7 8 9 10 11 12 13 14 15 ... 63

Listing 1.2 - The "RenderNav" Output

<a href='GenreList_Page.aspx?GenreID=1006&Page=1'>1</a>
<a href='GenreList_Page.aspx?GenreID=1006&Page=6'>...</a>
<a href='GenreList_Page.aspx?GenreID=1006&Page=7'>7</a>
<a href='GenreList_Page.aspx?GenreID=1006&Page=8'>8</a>
<a href='GenreList_Page.aspx?GenreID=1006&Page=9'>9</a>
<a href='GenreList_Page.aspx?GenreID=1006&Page=10'>10</a>
11
<a href='GenreList_Page.aspx?GenreID=1006&Page=12'>12</a>
<a href='GenreList_Page.aspx?GenreID=1006&Page=13'>13</a>
<a href='GenreList_Page.aspx?GenreID=1006&Page=14'>14</a>
<a href='GenreList_Page.aspx?GenreID=1006&Page=15'>15</a>
<a href='GenreList_Page.aspx?GenreID=1006&Page=16'>...</a>
<a href='GenreList_Page.aspx?GenreID=1006&Page=63'>63</a>


As you can see, you can click the ellipses to move everything forward or backward in a large result set. You could easily modify my code to make the number of links shown more dynamic.

Listing 1.3 - The Navigational Label

'[Doug's ASPX Navigation Code]

<asp:LinkButton runat="server" CssClass="pageLinks"
  id="FirstPage" Text="[First Page]"
  OnCommand="NavigationLink_Click"
  CommandName="First" />
 <asp:LinkButton runat="server" CssClass="pageLinks"
  id="PreviousPage" Text="[Previous Page]"
  OnCommand="NavigationLink_Click"
  CommandName="Prev" />
 <asp:LinkButton runat="server" CssClass="pageLinks"
  id="NextPage" Text="[Next Page]"
  OnCommand="NavigationLink_Click"
  CommandName="Next" />
 <asp:LinkButton runat="server" CssClass="pageLinks"
  id="LastPage" Text="[Last Page]"
  OnCommand="NavigationLink_Click"
  CommandName="Last" />

'I replaced with:

<asp:Label id="LblNav" runat="server"
   cssclass="pageLinks">
</asp:Label>

Listing 1.4 - Populating the Navigational Label

'Doug's "BindData" Sub:

If _currentPageNumber = 1 Then    

  PreviousPage.Enabled = False

  If _totalPages > 1 Then
    NextPage.Enabled = True
  Else    
    NextPage.Enabled = False
  End If

Else

  PreviousPage.Enabled = True

  If _currentPageNumber = _totalPages Then
    NextPage.Enabled = False
  Else
    NextPage.Enabled = True        
  End If

End If

'I replaced with:

'(VB.NET)

If _totalPages > 1 Then
  LblNav.Text = RenderNav(_currentPageNumber, Convert.ToInt32(_totalPages))
End If

//(C#)

if (_totalPages > 1)
{
  LblNav.Text = RenderNav(_currentPageNumber, Convert.ToInt32(_totalPages))
}

Listing 1.5 - My RenderNav Function (VB.NET)

Private Function RenderNav(ByVal CurrentPage As Integer, _
  ByVal TotalPage As Integer) As String

  Dim i As Integer
  Dim Lower1 As Integer
  Dim Upper1 As Integer
  Dim Lower2 As Integer
  Dim Upper2 As Integer
  Dim LowerSpread As Integer = CurrentPage - 1
  Dim UpperSpread As Integer = TotalPage - CurrentPage
  Dim LastReplace As Integer
  Dim ShowUpperEllipses As Boolean
  Dim ShowLowerEllipses As Boolean
  Dim z As StringBuilder = New StringBuilder()
  Dim LinkTemplate As String = "<ahref='GenreList_Page.aspx?GenreID=" & _
      iGenreID.ToString & "&Page=%%%'>%%%</a
  Dim LinkEllipses As String = "<a href= 'GenreList_Page.aspx?GenreID = " & _
      iGenreID.ToString & "&Page= %%%'>...</a>"

  ' calculate ellipses
  ' calculate lower

  If ((CurrentPage - 1) > 1) Then
    Upper1 = CurrentPage - 1
  End If
  If LowerSpread < 3 Then
    Lower1 = Upper1
  Else
    If LowerSpread > 5 Then
      Lower1 = CurrentPage - 4
      ShowLowerEllipses = True
    Else
      Lower1 = CurrentPage - (LowerSpread - 1)
    End If
  End If

  ' calculate upper
  If ((CurrentPage + 1) < TotalPage) Then
    Lower2 = CurrentPage + 1
  End If
  If UpperSpread < 3 Then
    Upper2 = Lower2
  Else
    If UpperSpread > 5 Then
      Upper2 = CurrentPage + 4
      ShowUpperEllipses = True
    Else
      Upper2 = CurrentPage + (UpperSpread - 1)
    End If
  End If


  ' render Page 1 link
  If CurrentPage = 1 Then
    z.Append("1 ")
  Else
    z.Append(LinkTemplate.Replace("%%%", "1"))
    z.Append(" ")
  End If

  ' render Lower Ellipses
  If Lower1 > 0 Then
    If ShowLowerEllipses Then
      z.Append(LinkEllipses.Replace("%%%", (Lower1 - 1).ToString))
      z.Append(" ")
    End If
    For i = Lower1 To Upper1
      z.Append(LinkTemplate.Replace("%%%", i.ToString))
      z.Append(" ")
    Next
  End If


  ' render Current Page link
  If ((CurrentPage > 1) And (CurrentPage <> TotalPage)) Then
    z.Append(CurrentPage.ToString)
    z.Append(" ")
  End If

  ' render Upper Ellipses
  If Lower2 > 0 Then
    For i = Lower2 To Upper2
      z.Append(LinkTemplate.Replace("%%%", i.ToString))
      z.Append(" ")
    Next
    If ShowUpperEllipses Then
      z.Append(LinkEllipses.Replace("%%%", (Upper2 + 1).ToString))
      z.Append(" ")
    End If
  End If


  ' render Last Page link
  If TotalPage > 1 Then
    If CurrentPage = TotalPage Then
      z.Append(TotalPage.ToString)
    Else
      z.Append(LinkTemplate.Replace("%%%", TotalPage.ToString))
    End If
  End If


  Return z.ToString

End Function

Listing 1.6 - My RenderNav Function (C#)

I am not nearly as conversant in C#, so you C# guys out there can probably improve on this. But the function does work as written.

private string RenderNav (int CurrentPage, int TotalPage)
{
  Int32 i;
  Int32 Lower1;
  Int32 Upper1 = 0;
  Int32 Lower2 = CurrentPage;
  Int32 Upper2;
  Int32 LowerSpread = CurrentPage - 1;
  Int32 UpperSpread = TotalPage - CurrentPage;
  Booelan ShowUpperEllipses = false;
  Boolean ShowLowerEllipses = false;
  StringBuilder z = new StringBuilder("");
  String LinkTemplate =
    "<ahref='GenreList_Page.aspx?GenreID=" +
    iGenreID.ToString() +
    "&Page=%%%'>%%%</a>";
  String LinkEllipses =
    "<ahref='GenreList_Page.aspx?GenreID=" +
    iGenreID.ToString() +
    "&Page=%%%'>...</a>";

  // calculate ellipses
  // calculate lower

  if ((CurrentPage - 1) > 1)
  {
    Upper1 = CurrentPage - 1;
  }

  if (LowerSpread < 3)
  {
    Lower1 = Upper1;
  }
  else
  {
    if (LowerSpread > 5)
    {
       Lower1 = CurrentPage - 4;
       ShowLowerEllipses = true;
    }
    else
    {
       Lower1 = CurrentPage - (LowerSpread - 1);
    }
  }
  // calculate upper
  if ((CurrentPage + 1) < TotalPage)
  {
    Lower2 = CurrentPage + 1;
  }
  if (UpperSpread < 3)
  {
    Upper2 = Lower2;
  }
  else
  {
    if (UpperSpread > 5)
    {
      Upper2 = CurrentPage + 4;
      ShowUpperEllipses = true;
    }
    else
    {
      Upper2 = CurrentPage + (UpperSpread - 1);
    }
  }

  // render Page 1 link
  if (CurrentPage == 1)
  {
    z.Append("1 ");
  }
  else
  {
    z.Append(LinkTemplate.Replace("%%%", "1"));
    z.Append(" ");
  }

  // render Lower Ellipses

  if (Lower1 > 0)
  {
    if (ShowLowerEllipses)
    {
      z.Append(LinkEllipses.Replace("%%%", (Lower1 - 1).ToString()));
      z.Append(" ");
    }

    for(i=Lower1;i<=Upper1;i++)
    {
      z.Append(LinkTemplate.Replace("%%%", i.ToString()));
      z.Append(" ");
   }
  }
  // render Current Page link
  if ((CurrentPage > 1) && (CurrentPage != TotalPage))
  {
    z.Append(CurrentPage.ToString());
    z.Append(" ");
  }
  // render Upper Ellipses
  if ((Lower2 > 0) && (Upper2 != CurrentPage))
  {
    for (i = Lower2; i<=Upper2; i++)
    {
       z.Append(LinkTemplate.Replace("%%%", i.ToString()));
       z.Append(" ");
    }
    if (ShowUpperEllipses)
    {
       z.Append(LinkEllipses.Replace("%%%", (Upper2 + 1).ToString()));
       z.Append(" ");
    }
  }
  // render Last Page link
  if (TotalPage > 1)
  {
    if (CurrentPage == TotalPage)
    {
      z.Append(TotalPage.ToString());
    }
    else
    {
      z.Append(LinkTemplate.Replace("%%%", TotalPage.ToString()));
    }
  }

  return z.ToString();

}


Marketplace
(Sponsored Links)
What are the green links?
   



 
Copyright © 2007 CMP Tech LLC |
Privacy Policy (4/10/06) | Your California Privacy Rights (4/10/06) | Terms of Service | Advertising Info | About Us | Help