Wrox Press
ASPToday
       974 Articles
  in the Solutions Library
  Log off
 
 
 
ASPToday Subscriber's Article Michael Ferrini
Pagination Using A Repeater
by Michael Ferrini
Categories: Site Design, .NET Framework
Article Rating: 3.5
Published on November 7, 2002
 
Content Related Links Discussion Comments Index Entries Downloads
 
Abstract
Under classic ADO, pagination (the effect of splitting a set of data across multiple web pages) has been accounted for with the ability to partition a Recordset, using properties such as PageSize and AbsolutePage. Under the .NET initiative, basic pagination is built into the DataGrid web control.

Unfortunately for me, the DataGrid does not fit the bill in most cases. At my job site, we have a designer and two programmers, including myself. The designer designs away and eventually we take over his static code to make it dynamic. Usually, the HTML is so granular that I am forced to use a Repeater control, because of its flexibility. However, the Repeater control does not support pre-built features for pagination, which is needed in most cases.

In this article I will demonstrate a technique that I developed, while struggling over several projects. This method generates a fully integrated pagination system, using mere Repeaters and an imagination. In the end you will discover that the solution is very scalable, as database resources are preserved between post-backs.
 

Please rate this article using the form below. By telling us what you like and dislike about it we can tailor our content to meet your needs.

Article Information
Author Michael Ferrini
Chief Technical Editor John R. Chapman
Project Manager Helen Cuthill
Reviewers Brady Gaster & Dave Schultz

If you have any questions or comments about this article, please contact the technical editor.

 
Article

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="<<&nbsp;" 
         Visible="False" 
         Runat="server" />
. . .
<asp:LinkButton ID="lnkNext" 
         CommandName="next" 
         Text="&nbsp;>>" 
         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.

 
 
 
Rate this Article
How useful was this article?
Not useful Very useful
Brief Reader Comments: Read Comments
Your name (optional):
 
 
Content Related Links Discussion Comments Index Entries Downloads
 
Back to top