Defining the Problem
Pagination has become a sub-feature of many of the projects that
I have worked on. Any time data is being returned in large
quantities, I have to think about splitting it across multiple
pages. Under ASP.NET, Microsoft has provided pagination features
intrinsic to the DataGrid web control.
The DataGrid is the most complex
web control, but is not necessarily the most flexible. Typically a
designer at my work builds a complex layout with intermixed
cascading styles. Once completed, these layouts are injected into my
project and my job is to make them dynamic. Nine times out of ten it
becomes nearly impossible to incorporate these layouts into a DataGrid. So what about that ASP.NET Repeater web control?
Using a Repeater arguably provides
the most flexibility, when it comes to HTML layouts. However, the
problem here is the lack of intrinsic support for pagination. There
is not a single Repeater property or
method that caters to such features, but shortly you will see that
this is solved simply with a couple of Repeaters, some Labels, and straightforward programming
techniques.
System Requirements
In order to attract the largest audience, database examples were
written with the System.Data.OleDb
namespace. If using SQL Server as your data source, I recommend
switching to System.Data.SQLClient for
performance reasons. This entails changing the imports statement and replacing the OleDb
object prefixes with the SQL prefix. Furthermore, eliminate the
provider portion from the connection string located in the projects
Web.Config file. Microsoft Access users will need to change
the connection string completely.
Since this article is not focused on database programming, I
decided to keep data access simplified. Reasons are that we will be
focusing on the mechanics of custom pagination. Naturally, stored
procedures should be used in place of inline SQL statements in
full-scale applications. Note that Microsoft Access does not support
stored procedures, so the code example can be used as is.
Next, the example was written with Visual Studio.NET using the
code-behind model. If you do not use Visual Studio.NET there should
be no problem copying and pasting code snippets to fit the classic
ASP inline model.
Lastly, you need IIS 5 along with ASP.NET configured to run these
examples. Just place the files in the web root or a virtual
directory and make sure the permissions are set correctly. If you
are browsing the files locally under a virtual directory called
Pager, you most likely can view the example at http://localhost/Pager?WROXEMPTOKEN=239300ZCbsIOhkV1n2R5LnEK22.
Defining the Solution
To begin, let me show you the complete solution. To get to the
screen shot below, open the default.aspx page of the example
and type the letter 'M' in the search
box.
After clicking Search, you should
see something similar to the screenshot below:
In the screenshot you can see many features of a simple
pagination system. Included in this conglomeration are search
results, a status of what page you are on, a record count, page
numbers and a navigation arrow.
Near the top of the web page you can observe that 34 records were
returned, but why do only 5 of them show up? To put it simply, a
pagination system should only show a limited view of the records per
page. You do not want to return a large amount of records on any
particular page, especially if this number can be in the hundreds or
even thousand as the load time would be too astronomical in many
cases. Later you will learn how to split a large record return
across multiple web pages, effectively reducing the overhead.
Next, notice that the current page, which is page one, is not a
link. This was a personal touch, because it makes no sense to have a
hyperlink pointing to itself. Besides, this minor effect adds a
professional touch that acts as a current page indicator for the
end-user. Later you will see this effect implemented with a Label control.
Furthermore, notice that we are missing the "previous" navigation
arrow. Since this screen captures the first page in the pagination
system, we logically have no previous page. While surfing through
the pagination, you will discover that a previous link arrow will in
fact appear beyond page one. On the last page, as you'd expect, the
"next" arrow disappears.
The last thing I want to bring up is the number of hyperlinked
page numbers within the pager. In the first screen capture near the
top it says Page: 1 of 7, but the
pager displays only pages 1 through 5. Why's that then? Well, if you
were to continually navigate to page 5 in this example, you would
still have yet another next arrow, indicating that there are still
more pages. Clicking this next arrow would then bring up the
following screenshot:
How interesting; the pager now displays pages 6 to 7, with 6
being the selected page. This is just another one of my special
touches. You could coin it as changing pager levels, but there is no
exact name for this pager style. Moving on, the reasoning behind
this pager style has to do with instances where a search result can
return an overabundance of records. This can cause the pager to be
loaded with an unappealing amount of page numbers. Trust me; it is
not pleasant to see 50 page numbers jumbled in one area. On top of
that, the designer may want to build a layout so tight that only 5
or 10 page numbers can show up in an allotted spot.
Now that you have seen the solution, let me break it down for
you. First the search results are displayed using a Repeater - remember, flexibility is needed.
Next, the navigation arrows are just link buttons. They could easily
be image buttons in your solution. Finally, the pager is just a
Repeater, which can be easily
constructed, as you will soon see.
Getting the Initial Record Count
Obtaining a record count is the basis for building the rest of
the pagination system. By knowing how many records are returned for
a search result, we can use basic math to later determine how many
pages will make up the system. Since obtaining the record count will
require a database hit, we want to make sure we acquire it on the
first page post and eventually save it in ViewState for later use. Below is the
routine that returns the record count.
(Return.aspx.vb - GetRecordCount Function) Private Function GetRecordCount(ByVal strCriteria As String) As Integer
Dim strSQL As String = String.Format(SQL_COUNT, strCriteria)
Dim objCmd As New OleDbCommand(strSQL, New OleDbConnection(mCon))
objCmd.Connection.Open()
GetRecordCount = CInt(objCmd.ExecuteScalar)
objCmd.Connection.Close()
End Function
In the GetRecordCount function we
have an argument called strCriteria,
which accepts a string value. This is where the value of the search
gets passed. For our example we performed a search on the letter
'M'.
The brunt of the work is accomplished with a SQL statement that
performs the count operation. We need to know how many records are
returned, so that a page count can later be determined. In the
example, since the search was on the letter 'M', the SQL aggregate statement will look
like this: SELECT COUNT(ContactName) FROM Customers WHERE ContactName LIKE '%M%'
Executing the SQL statement above will return a database field,
with a total count of records that contain the letter 'M'. Using the command objects ExecuteScalar method is perfect for this
scenario, because it returns the first field value of the first
record, which in our case is the record count. If Option Strict is on, casting to an integer
here will not be optional.
In the page's Load event, GetRecordCount gets called on the first page
post and its result is immediately stored in a member-wide variable
called mRecordCount. A later
demonstration will show how that value gets placed in ViewState and reused with every
post-back.
Determining the Number of Pages
If a record count can be established, then getting the page count
is the next step. First we must have some value that dictates the
maximum amount of records that can show up on a page. Under classic
ADO, such a property was called PageSize, which was part of the Recordset object. Sticking with the same
theme, a decision was made to declare a variable called mPageSize. To keep this example simple, this
variable was set equal to 5 in the declarations section. If you want
to try different page sizes, just change the value in the mPageSize variables initializer and
recompile, or save in the case of inline code. In a full-scale
solution, I encourage you to make this property more dynamic by
adding code that reacts to users manually selecting a page size.
This supports the scenario where search engines allow you to pick
10, 20, or 50 records to display at a time.
Once we have the page size in place, we have enough information
to mathematically calculate a page count: mPageCount = CInt(Math.Ceiling(mRecordCount / mPageSize))
There is a lot to say about this one line of code. Say you
perform a search that returns 50 records exactly. We can easily
deduce that this produces 10 pages, because of 50 divided by 5. That
was easy to digest, but what happens when 51 records are returned?
Obviously, 10 pages will not be sufficient to store 51 records - we
need an extra page to host that last record. Here comes the Math.Ceiling method to the rescue. Its
purpose in life is to take a decimal value and raise it to the next
whole number, always rounding up, of course. In the case of 51
records, Math.Ceiling converts 10.2
pages to 11 pages.
Returning the Right Data
Pop quiz; if 34 records are returned with a page size of 5 and
the end-user moves to page 2, how do you display the correct
records? There must be some way to get only what is needed from a
DataSet. There are actually two
solutions I know of, but I will only show you one. First, check out
the routine below and then I will elaborate:
(Return.aspx.vb - BindData
Sub) Private Sub BindData(ByVal strCriteria As String, _
ByVal intStart As Integer, _
ByVal intSize As Integer)
' SQL Statement
Dim strSQL As String = SQL_SEARCH & " ORDER BY ContactName"
' Plug search criteria into the SQL command
strSQL = String.Format(strSQL, strCriteria)
' Prepare dataset data storage
Dim objCmd As New OleDbCommand(strSQL, New OleDbConnection(mCon))
Dim objDA As New OleDbDataAdapter(objCmd)
Dim objDS As New DataSet()
' Fill only the data needed for a given page
objDA.Fill(objDS, intStart, intSize, "Return")
' Display Data in repeater
rptResults.DataSource = objDS.Tables("Return").DefaultView
rptResults.DataBind()
' Check to see if any records exist
If rptResults.Items.Count > 0 Then
' Display page number pager
rptPages.DataSource = Pages()
rptPages.DataBind()
Else
' Display default message
With lblNoResults
.ForeColor = Color.Red
.Font.Size = FontUnit.Point(10)
.Font.Name = "Verdana"
.Visible = True
End With
End If
End Sub
To understand what is happening in the BindData routine, you really only need to
observe the code portions of the procedure, highlighted below: ...
Dim strSQL As String = SQL_SEARCH & " ORDER BY ContactName"
...
This segment builds the SQL statement for the search. Based on
the example given at the beginning, the SQL_SEARCH constant will amount to: SELECT * FROM Customers WHERE ContactName LIKE '%M%'
Next, the ORDER BY clause is
dynamically added. It is important that we sort the search results,
so the correct set of records is returned on a given page and in the
right order. The snippet below demonstrates how the DataSet is filled: . . .
' Fill only the data needed for a given page
objDA.Fill(objDS, intStart, intSize, "Return")
' Display Data in repeater
rptResults.DataSource = objDS.Tables("Return").DefaultView
rptResults.DataBind()
. . .
As you can see in the section above, by using the data adapter's
Fill method, you can easily partition
search returns. The overloaded Fill
method accepts four arguments, but only the first three arguments
are important for the pagination system. Argument one accepts a
DataSet to be filled. Argument two,
which receives the intStart variable,
determines when to start filling records. If the value of this
argument were 5, then the first 5
records would be skipped and ignored, effectively starting the fill
process at record 6. The third argument, set by the intSize variable, indicates when to stop
filling records. Making this argument 7, for instance, would only load 7 records
into the DataSet. Understand that; if
there are not enough records to satisfy the intSize argument, the remaining records will
still be returned. To clarify, say that you are on the last page of
the pagination system with 3 records left; passing an intSize of 7 will fill those last 3 records
without an error. Later you will see that the intSize variable will always be the page
size and the intStart variable is
dynamically set based on the current page.
As a final note, using the Fill
method is scalable, but is slightly surpassed by Vinod Kumar's super
query, described in "Effective
Data Presentation using ASP.Net Server Control Part-I". This is
that second method I was speaking of before. However, I chose the
first method for my example because the super query tends to become
unwieldy when many parameters need to be searched against. Basically
I picked ease over performance. Personally I use both for different
occasions and it is undoubtedly easy to implement the super query in
this pagination system.
Using a Repeater to Build a Pager
The pager is the component that displays links to the different
pages in the pagination. In the example, the pager is at the bottom
of the screen shot. Clicking on one of the page numbers should take
you to that corresponding page. As an added touch, this pager makes
the current page a non-link for professional appeal. The cool thing
is that a pager component can easily be built with a Repeater web control. Take a look at the
presentational code below:
(Return.aspx - Building the
Pager) <asp:Repeater ID="rptPages" Runat="server">
<ItemTemplate>
<asp:Label ID="lblPage"
Text='<%# Container.DataItem %>'
Visible="False"Runat="server" />
<asp:LinkButton ID="lnkPage"
Text='<%# Container.DataItem %>'
Visible="False" Runat="server" />
</ItemTemplate>
<SeparatorTemplate>,</SeparatorTemplate>
</asp:Repeater>
That's odd; there is a Label and a
LinkButton within the Repeaters item template, but invisible by
default. These child controls happen to participate in the mechanism
of displaying page numbers. Notice the data binding expressions
implanted within the Text properties.
These expressions become the meat and potatoes of the whole
operation. Once bound, the Text
properties will hold the actual page number. The Label control becomes the visible control
for the current page number, making the LinkButton visible for all other page
numbers. Below shows a screen shot of how this looks on page 2 of
the example.
Storing the Page Numbers
To put it simply, a Repeater needs
a data source in order to display anything. In many cases this data
source is a DataView. For our
purposes, an ArrayList is used. The
ArrayList is used to store the page
numbers for a given page. Check out the function below, to see how
this data source is being built:
(Return.aspx.vb - Pages
Function) Private Function Pages() As ArrayList
Dim i As Integer
Dim arr As New ArrayList()
For i = mPageOffset To (mPageOffset + mPagePerLevel) - 1
If i <= mPageCount Then
arr.Add(i)
Else
Exit For
End If
Next
Return arr
End Function
Inside this function we see a loop, which places page numbers
inside the ArrayList. The first detail
is the mPageOffset variable, which was
mentioned earlier. Remember the demonstration above on how you moved
passed page 5 to reveal pager links 6 to 7. Well mPageOffset plays a role in that process. To
clarify, you must understand the purpose of the mPagesPerLevel variable first.
Storing a static value, such as 5, within the mPagesPerLevel variable means you are
determining how many pager numbers should show up at a given point.
As an example, say you set mPagesPerLevel equal to 5, and 50 records
are returned. Clearly we have 10 pages again, but since mPagesPerLevel is set to 5, you will
initially see a pager filled with pages 1 through 5. Traversing past
page 5 will then reveal a pager with pages 6 through 10. Each time
the pager level changes like this, the mPageOffset is either decremented or
incremented by mPagesPerLevel. You
will see this logic when I show you the Arrow_Command event handler.
With that explanation out of the way, I can proceed to tell you
that mPageOffset is an important
factor for dynamically generating page numbers. Using the recent
scenario; the loop within the Pages
function on page 1 is really stating: For i = 1 to 5
. . .
Next
After traversing pass page 5 in the scenario, the loop in the
Pages function is now stating: For i = 6 to 10
. . .
Next
Now that you understand the data source, you can bind it to a
Repeater like so: rptPages.DataSource = Pages()
rptPages.DataBind()
Displaying A Label Or Link
How do we determine when to display the LinkButton or Label, for a given page number? The answer
to that is the ItemDataBound event for
the Repeater. This event fires when
each item in the data source is bound to a RepeaterItem. You'd think that in most
cases, if 5 elements are being bound, then 5 events would fire. That
would be true if you were just hosting item templates in a Repeater, which we are not. We are also
hosting a separator template and the ItemDataBound event unfortunately fires for
each one of those. Initially this does not seem to present a
problem, until we try to reference the LinkButton and Label children controls. These child
controls exist for the Item template,
but do they exist for the Separator
template? The answer to that question is "no", but a solution is
shown below:
(Return.aspx.vb - rptPages_ItemDataBound Handler) Private Sub rptPages_ItemDataBound( _
ByVal sender As Object, _
ByVal e As System.Web.UI.WebControls.RepeaterItemEventArgs) _
Handles rptPages.ItemDataBound
' NOTE: All templates are accounted for when databinding.
' Each template item will have a literal control
Dim objLit As LiteralControl = CType(e.Item.Controls(0), _
LiteralControl)
' If there is a comma then this current item is a separator template
If objLit.Text.IndexOf(",") = -1 Then
' Get reference to controls within item template
Dim lblPage As Label = CType(e.Item.FindControl("lblPage"), Label)
Dim lnkPage As LinkButton = CType(e.Item.FindControl("lnkPage"), _
LinkButton)
' Display a label for the currently selected page
' Otherwise display a link button
If mCurrentPage = CInt(e.Item.DataItem) Then
lblPage.Visible = True
lblPage.Font.Bold = True
lnkPage.Visible = False
Else
lnkPage.Visible = True
lblPage.Visible = False
End If
End If
End Sub
Each template, when created, has a LiteralControl. By checking the Text property of a Literal control for the presence of a comma,
we can determine if the ItemDataBound
event is firing for an item template or separator template.
Once established, we can safely grab references to the child
controls and execute the logic for displaying a Label for the current page and LinkButtons for all other page numbers. Note
that the DataItem represents a page
number stored in the ArrayList, which
happens to be the currently bound data source.
Another solution is to place the code in the Try and Catch
exception handler, instead of searching for the comma. In the case
of the separator template, an exception would be thrown when trying
to reference the child controls. The exception is ignored if you
leave the Catch block empty. This
solution permits a designer to change the separator character
without breaking the program. However, exception blocks add
performance overhead, especially when the ItemDataBound event may be fired several
times before the page renders.
Handling Page Number Clicks
At this point, we have a pager, a data source for that pager, and
a means of displaying search results. Now we need to be able to
switch pages by clicking on various pager links. The code snippet
below takes advantage of event bubbling to complete this task:
(Return.aspx.vb - rptPages_ItemCommand Handler) Private Sub rptPages_ItemCommand(ByVal source As Object, _
ByVal e As System.Web.UI.WebControls.RepeaterCommandEventArgs) _
Handles rptPages.ItemCommand
' Set the new current page
mCurrentPage = CInt(e.Item.ItemIndex)
mCurrentPage += mPageOffset
' Set the new start record
mStart = CInt(e.Item.ItemIndex) * mPageSize
' Rebind search results
Call BindData(Request.Params("ct"), mStart, mPageSize)
' Display naviagtion
Call DisplayArrows()
End Sub
When a page number link is clicked, a click event is bubbled up
to its parent Repeater control, which
in turns fires the ItemCommand event.
Inside the routine we can gather information about which link was
clicked and then easily determine the new value for the mCurrentPage variable. Notice that we are
revisited by the mPageOffset variable
again. To easily digest this segment, just understand that mPageOffset will always equal 1, when on the first level of page numbers.
Next, note the use of the BindData
routine. The mStart variable is being
passed, but not without a little general math: . . .
mStart = CInt(e.Item.ItemIndex) * mPageSize
. . .
Multiplying the Repeater's item
index with the page size, magically calculates the correct start
value. For instance, if you were on page 2 with a page size of 5, the formula would read: . . .
mStart = CInt(1) * 5
. . .
The item index is zero based, so clicking on page number 2 will return a 1. With that in mind, is it safe to say that
record 5 is the correct starting record? The answer is yes, because
when the data adapter uses that overloaded Fill method, you are also working with zero
based records. The Fill method in this
case will be starting at the 6th record, which is the correct
starting point for page 2, with a page size of 5. Whew, say that 10
times fast.
Navigation Arrows
By now you can click page numbers to jump to a specific page in
the system and view the correct records. However, we can improve
this scenario by adding navigation arrows. These should be links
that navigate to the next or previous page. Besides that, lacking a
next button would make it impossible to traverse to the next level
of pager links.
To make things compact, an event handler is associated with two
LinkButtons. Below are two web
controls that make up the navigation arrows, followed by its mutual
event handler:
(Return.aspx - Builds next and previous navigation
arrows) <asp:LinkButton ID="lnkPrev"
CommandName="prev"
Text="<< "
Visible="False"
Runat="server" />
. . .
<asp:LinkButton ID="lnkNext"
CommandName="next"
Text=" >>"
Visible="False"
Runat="server" />
(Return.aspx.vb - Arrow_Command Handler) Private Sub Arrow_Command(ByVal sender As Object, _
ByVal e As System.Web.UI.WebControls.CommandEventArgs) _
Handles lnkPrev.Command, lnkNext.Command
Select Case e.CommandName
Case "prev"
mCurrentPage -= 1
If mCurrentPage Mod mPagePerLevel = 0 Then
mPageOffset -= mPagePerLevel
End If
Case "next"
mCurrentPage += 1
If (mCurrentPage - 1) Mod mPagePerLevel = 0 Then
mPageOffset += mPagePerLevel
End If
End Select
' Set start record
mStart = mCurrentPage - 1
mStart *= mPageSize
' Rebind search results
Call BindData(Request.Params("ct"), mStart, mPageSize)
' Display naviagtion
Call DisplayArrows()
End Sub
First, look at how more than one control is handled by the same
routine, the secret is in the Handles
statement. Because the CommandName
property was declaratively set for each navigation link, we can
easily determine which LinkButton to
handle. Knowing this, we can conditionally increment or decrement
the mCurrentPage variable. Next comes
that math again for determining where to start filling records;
stored under the mStart variable.
Finally we can pass a newly defined mStart to the BindData routine and we are good to go.
A final thing to understand in this routine is how the mPageOffset variable is changed. By using
modulus math we can check to see if the current page overlaps the
pager level. Say we set mPagesPerLevel
equal to 5, and 50 records are
returned once more. Moving to page 6 from page 5 should increase the
page offset. Check out the snippet from the Arrow_Command routine below: . . .
mCurrentPage += 1
If (6 - 1) Mod 5 = 0 Then
mPageOffset += mPagePerLevel
End If
. . .
First off, I translated the variables to their integral values.
For instance, mCurrentPage became 6,
because we moved to page 6, and mPagesPerLevel became 5 because that is how
it is set in the declarations section. Now 5
Mod 5 satisfies the equation, successfully raising the mPageOffset variable by 5. The snippet below
illustrates how the loop inside the Pages function should now look: . . .
For i = 6 To (6 + 5) - 1
If i <= mPageCount Then
arr.Add(i)
Else
Exit For
End If
Next
. . .
Again I replaced variables with corresponding integral values,
but now you should clearly see how pages 6 to 10 make up the second
new pager level.
Hiding Arrows
Scattered through the code, you should have seen various calls to
the DisplayArrows routine. This
procedure is basically responsible for hiding or displaying
navigation arrows based on the current page. Take a peek at the code
below:
(Return.aspx.vb - DisplayArrows Routine) Private Sub DisplayArrows()
If rptResults.Items.Count > 0 Then
' Display arrows based on current page
Select Case mCurrentPage
Case 1
lnkPrev.Visible = False
' If there are not enough records to fill a second page
' then remove the next link
If mRecordCount > mPageSize Then
lnkNext.Visible = True
End If
Case mPageCount
lnkNext.Visible = False
lnkPrev.Visible = True
Case Else
lnkNext.Visible = True
lnkPrev.Visible = True
End Select
End If
End Sub
There is really nothing spectacular here, just a plain old select-case statement that checks the
current page and hides the arrows respectively. The only important
detail is the conditional that executes when you are on page 1. . . .
If mRecordCount > mPageSize Then
lnkNext.Visible = True
End If
. . .
This conditional hides the "next" arrow link in situations where
there are not enough records to fill a second page.
Adding Effects
Welcome to the artistic license section. This is the part where
you add additional features that benefit the end-user and make you
look professional. Effects can be anything from displaying the total
record count, to displaying the current page number. The beautiful
thing is that previously defined properties can be used to create
these effects.
I challenge everyone to come up with more effects. If you need
any ideas just look at various search engines, such as
Google. Even ASPToday has some unique touches applied
to its search pagination. Personally I like the scrolling pager
effect and the ability to jump to the first and last page. Below is
a snippet that displays the simple effects that were applied in the
example solution: lblPageCount.Text = "Page: " & mCurrentPage & " of " & mPageCount & _
" / Records: " & mRecordCount
A page count within a page range is concatenated to a record
count and is further stored in the Text property of a Label. You can find this code in the pages
PreRender event handler of the code
example. The code was placed there because that is where all
properties will be most current. All control events fire before the
PreRender event; meaning properties
could be changed during those processes.
Notes About Post-Back
There are many properties that must be set on the first page post
and many that must be persisted between post-backs. To start, the
mRecordCount variable should be set
once, because propagating it is a database hit. The mPageCount variable needs to be calculated
once, as well, because it will never change. Other properties such
as mCurrentPage, mStart and mPageOffset can change, but still should be
persisted for post-backs to maintain accuracy.
With that said, we can use ViewState to take care of all of this. The
aforementioned properties are all saved to ViewState under the pages PreRender event:
(Return.aspx.vb - Page_PreRender Handler) Private Sub Page_PreRender(ByVal sender As Object, _
ByVal e As System.EventArgs) Handles MyBase.PreRender
' Store pager values in viewstate to preserve database hits
Viewstate("RecordCount") = mRecordCount
ViewState("PageCount") = mPageCount
viewstate("CurrentPage") = mCurrentPage
ViewState("PageOffsett") = mPageOffset
. . .
End Sub
Saving values to ViewState under
the PreRender event is the best
opportunity. However, another plus is that ViewState has not serialized into hidden
fields yet. This means that you have one last chance to persist your
properties. On post-back, those aforementioned properties need to be
restored with their previously saved values. This is accomplished in
the pages Load event below:
(Return.aspx.vb - Page_Load
Handler) Private Sub Page_Load(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles MyBase.Load
If Not Page.IsPostBack Then
. . .
Else
' Get saved pager properties from View-State
mRecordCount = CInt(Viewstate("RecordCount"))
mPageCount = CInt(ViewState("PageCount"))
mCurrentPage = CInt(ViewState("CurrentPage"))
mPageOffset = CInt(ViewState("PageOffsett"))
End If
End Sub
With this simple system in place, property values should flow
smoothly on a page-by-page basis, perfectly maintaining their
correct values.
Conclusion
This article explains one of many possible solutions to
pagination. Borrowing ideas from this article and others, I
guarantee anybody can come up with stronger variations.
Nevertheless, not everyone works with a strict layout, so I cannot
dictate your choice of web controls. Fortunately, techniques
discussed in this article will also work for a DataList and DataGrid.
To conclude, as robust as the example was, you can always take
things to a higher level. Code from this article can easily be
encapsulated in a Web Control class and extended days on
end. |