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.
-
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.
-
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.
- 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
<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" />
<asp:Label id="LblNav" runat="server"
cssclass="pageLinks">
</asp:Label> |
Listing 1.4 - Populating the Navigational Label
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
If _totalPages > 1 Then
LblNav.Text = RenderNav(_currentPageNumber, Convert.ToInt32(_totalPages))
End If
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>"
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
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
If CurrentPage = 1 Then
z.Append("1 ")
Else
z.Append(LinkTemplate.Replace("%%%", "1"))
z.Append(" ")
End If
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
If ((CurrentPage > 1) And
(CurrentPage <> TotalPage)) Then
z.Append(CurrentPage.ToString)
z.Append(" ")
End If
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
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>";
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);
}
}
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);
}
}
if (CurrentPage == 1)
{
z.Append("1 ");
}
else
{
z.Append(LinkTemplate.Replace("%%%", "1"));
z.Append(" ");
}
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(" ");
}
}
if ((CurrentPage > 1) &&
(CurrentPage != TotalPage))
{
z.Append(CurrentPage.ToString());
z.Append(" ");
}
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(" ");
}
}
if (TotalPage > 1)
{
if (CurrentPage
== TotalPage)
{
z.Append(TotalPage.ToString());
}
else
{
z.Append(LinkTemplate.Replace("%%%",
TotalPage.ToString()));
}
}
return z.ToString();
} |