Skip navigation

Take Control with ASP.NET 3.5

Using the ASP.NET DataPager Control

From the Source

LANGUAGES: VB.NET

ASP.NET VERSIONS: 3.5

 

Take Control with ASP.NET 3.5

Using the ASP.NET 3.5 DataPager Control

 

By Mike Pope

As with every release, ASP.NET 3.5 includes new data controls. For example, there is a data source control for working with LINQ (the LinqDataSource control), and the new ListView control, which is like an ultra-smart DataList control. In keeping with the trend toward abstracting more functionality with every release, there also is a new DataPager control.

In essence, the DataPager control provides the paging functionality you probably already know from the GridView control, and packages that functionality into its own control. The idea is that you can add a DataPager control to a page and define its UI. You can then bind the pager control to a data control. In ASP.NET 3.5, the primary beneficiary of the pager control, so to speak, is the new ListView control. In this article, you ll see how to extend controls to be able to use the pager.

What the DataPager Control Does

What exactly does the DataPager control do? It provides a UI for paging this typically includes Next and Previous buttons, and, optionally, page numbers.

The DataPager control makes sure the UI is correct for the current page in the data control to which it is bound. For example, if a ListView control is displaying the first page, the DataPager control for that ListView control shows a Previous button, but the button is disabled. If you ve configured the pager to show page numbers, the pager shows the correct number of pages and enables and disables the page number links according to which page is currently displayed.

The pager control does not perform the data paging; it simply displays the UI that lets the user navigate. When a user clicks a pager element such as the Next button, the pager notifies the control to which it is bound that the user wants to see a specific page. It s up to the bound control to then display that page. The pager then resets its UI appropriately.

Defining the Pager UI

You define the UI of the pager by using fields. The pager includes two built-in fields. The NextPreviousPagerField class can display a First, Previous, Next, and Last button, or any combination of these. The NumericPagerField class displays page numbers. For both fields, buttons are normal ASP.NET buttons (Button, LinkButton, or ImageButton); you can configure the fields to display the button type you like.

You can combine NextPreviousPagerField and NumericPagerField instances to create combinations of First/Previous/Next/Last buttons with page numbers. Figure 1 shows the markup for a typical data pager layout.

<asp:DataPager ID="DataPager1" runat="server"

    PagedControlID="ListView1"

    PageSize="5" >

  <Fields>

  <asp:NextPreviousPagerField

    ButtonType="Link"

    ShowFirstPageButton="True"

    ShowNextPageButton="False"

    ShowPreviousPageButton="True"

    FirstPageText="<<"

    PreviousPageText="<"

    ButtonCssClass="PagerNumber" />

  <asp:NumericPagerField

    NumericButtonCssClass="PagerNumber"

    CurrentPageLabelCssClass="CurrentPagerNumber"/>

  <asp:NextPreviousPagerField

    ButtonType="Link"

    ShowLastPageButton="True"

    ShowNextPageButton="True"

    ShowPreviousPageButton="False"

    NextPageText=">"

    LastPageText=">>"

    ButtonCssClass="PagerNumber" />

  </Fields>

</asp:DataPager>

Figure 1: Markup for a typical data pager layout.

Notice that there is a separate NumericPreviousPagerField element for the Previous (and First) and the Next (and Last) buttons. This lets you create a layout with a separate Next and Previous button.

In the example, the DataPager control is bound to the ListView1 control by setting the PagedControlID property to the ID of the control you want to page (PagedControlID= ListView1 ). However, if the pager is inside a control that supports the pager (that is, that implements IPageableItemContainer, like ListView), you don t need to explicitly set the PagedControlID property. Instead, the DataPager control implicitly binds itself to its container control. This makes it easy to put the DataPager control inside a footer or other template.

As you see, you can specify display options for button text. You can style the buttons by defining CSS classes and assigning them to the ButtonCssClass, NumericButtonCssClass, and CurrentPageLabelCssClass properties.

Template Pager Field

The data pager control supports templates, which lets you create a custom UI and layout for the pager. In that case, you create a TemplatePagerField element and add the buttons (and optionally other controls) to the template.

Employing a template is particularly useful if you want to display information such as the current page number, total page numbers, and total row count. You get this information by using a data-binding expression that references the Container object, which points to the DataPager control. Figure 2 shows the markup for a data pager with a templated field that includes page and row count information.

<asp:TemplatePagerField

  OnPagerCommand="TemplatePagerField_OnPagerCommand">

  <PagerTemplate>

    <asp:Button ID="buttonFirst" runat="server"

      Text="First"

      CommandName="First" />

    <asp:Button ID="buttonPrevious" runat="server"

      Text="Previous"

      CommandName="Previous" />

    <asp:Label runat="server" ID="CurrentPageLabel"

      Text[A1]="<%# IIf(Container.TotalRowCount>0, 

        (Container.StartRowIndex /

        Container.PageSize) + 1 , 0) %>" />

    of

    <asp:Label runat="server" ID="TotalPagesLabel"

      Text="<%# Math.Ceiling 

        (System.Convert.ToDouble(Container.TotalRowCount) /

        Container.PageSize) %>" />

    (<asp:Label runat="server" ID="TotalItemsLabel"

      Text="<%# Container.TotalRowCount%>" /> records)

    <asp:Button ID="buttonNext" runat="server"

      Text="Next"

    CommandName="Next" />

    <asp:Button ID="buttonLast" runat="server"

      Text="Last"

      CommandArgument="Last"

      CommandName="Last" />

  </PagerTemplate>

</asp:TemplatePagerField>

Figure 2: Markup for a data pager with a templated field that includes page and row count information.

The row count and page count are created by using data-binding expressions. In the expressions, the Container variable gets a reference to the pager control. This in turn gives you access to the StartRowIndex and TotalRowCount properties, which you can use to calculate the current page number.

Although a templated field gives you great flexibility, there is a down side if you create a template pager field, you take over responsibility for the paging logic. You must handle the TemplatePagerField class PagerCommand event, which is raised when any button in the template pager field is clicked. Typically, the buttons in a template pager field pass information to the event handler by using their CommandName or CommandArgument properties.

In the PagerCommand handler, the pager passes you a DataPagerCommandEventArgs object that contains useful information like the total row count and the maximum size (the page size). You determine which button was clicked, then set the NewStartRowIndex and NewMaximumRows properties of the event argument to the first row of the new page. The simplest part of the logic you need to implement paging in a handler for the PagerCommand handler is shown here:

If e.CommandName = "First" Then

    e.NewStartRowIndex = 0

End If

As noted, you are responsible for the logic that figures out the correct row to display. To support a full complement of First, Previous, Next, and Last buttons, your handler might look something like the code shown in Figure 3.

Protected Sub TemplatePagerField_OnPagerCommand(ByVal sender As Object, _

      ByVal e As DataPagerCommandEventArgs)

  Dim newIndex As Integer

  Dim lastPageNumber As Integer

  Select Case e.CommandName

    Case "Next"

      newIndex = e.Item.Pager.StartRowIndex + e.Item.Pager.PageSize

      If newIndex <= e.TotalRowCount Then

        e.NewStartRowIndex = newIndex

      End If

    Case "Previous"

      e.NewStartRowIndex = e.Item.Pager.StartRowIndex -  _

         e.Item.Pager.PageSize

    Case "First"

      e.NewStartRowIndex = 0

    Case "Last"

    ' Integer division

    lastPageNumber = e.TotalRowCount \ e.Item.Pager.PageSize

        If (e.TotalRowCount Mod e.Item.Pager.PageSize) = 0 Then

            lastPageNumber -= 1

        End If

    e.NewStartRowIndex = lastPageNumber * e.Item.Pager.PageSize

  End Select

  e.NewMaximumRows = e.Item.Pager.MaximumRows

End Sub

Figure 3: Support a full complement of First, Previous, Next, and Last buttons.

On the plus side, because you are handling the event anyway, you can do interesting things. For example, you can add a TextBox or other control that lets users jump to an arbitrary page.

To implement the TextBox, you can substitute the markup shown in Figure 4 for the markup used earlier to display the 1 of 6 page information.

Jump to page: <asp:TextBox runat="server" ID="textNewPageNumber"

    Height="16px" Width="42px"

    Text="<%# IIf(Container.TotalRowCount>0,

        (Container.StartRowIndex /

        Container.PageSize) + 1 , 0) %>"

/>

<asp:button runat="server" ID="buttonGoToPageNumber"

    Text="Go"

    CommandName="GoToPage" />

Figure 4: Use this markup to implement the TextBox shown in Figure 7.

You can then extend the logic in the PagerCommand event by adding code that is something like that shown in Figure 5.

Case "GoToPage"

  Dim textNewPageNumber = _

    CType(e.Item.FindControl("textNewPageNumber"), TextBox)

  Dim newPageNumber = CInt(textNewPageNumber.Text)

  e.NewStartRowIndex = (newPageNumber - 1) * e.Item.Pager.PageSize

  If e.NewStartRowIndex > e.TotalRowCount Then

    ' Integer division

    lastPageNumber = e.TotalRowCount \ e.Item.Pager.PageSize

    If (e.TotalRowCount Mod e.Item.Pager.PageSize) = 0 Then

        lastPageNumber -= 1

    End If

    e.NewStartRowIndex = lastPageNumber * e.Item.Pager.PageSize

  End If

Figure 5: Extend the logic in the PagerCommand event.

Paging with URLs

By default, the DataPager control performs a postback (an HTTP POST command) when users click a paging button. Information about what page to display is passed as part of the POST form data. You can specify that the control instead use a GET command when users click a button. In that case, information about what page to display is passed in a query string. For example, the URL might end up looking like this: http://contoso.com/MySite/DisplayData.aspx?page=3. This approach has some advantages. It enables search engine bots to index individual data pages. It also makes it easy for users to send a specific data page in e-mail or IM.

To page with URLs, set the data pager s QueryStringField property to the name of the variable you want to use in the query string. In the URL example in the previous paragraph, the QueryStringField is set to page . As long as QueryStringField is set to a string other than an empty string ( ) or null (Nothing), the pager will page by using the URL.

The value you use for QueryStringField is arbitrary, but of course must be a name that can be used in a URL. The only time you really need to worry about what value you are using is when you have multiple DataPager controls on the page, in which case:
       If all the DataPager controls are bound to the same data control, they should all have the same QueryStringField value.
       If multiple DataPager controls are bound to different data controls, make sure they have different QueryStringField values.

If you have multiple DataPager controls on the page and they re all bound to the same data control, they should all have the same PageSize properties. If they have different page sizes, the last control to be initialized (specifically, the last control to call SetPageProperties) will determine the actual page size for the associated control.

Using the Pager with Data Controls

The DataPager control can provide paging for any control that implements the IPageableItemContainer interface. As suggested earlier, the only control in ASP.NET 3.5 that meets this criterion is the ListView control. Other data controls, such as DataList and DataGrid, were not retrofitted to implement this interface.

If you are comfortable with creating a custom ASP.NET Web control, you can create a new control that derives from an existing data control and that implements the IPageableItemContainer interface. The interface requires only a few members, which are all straightforward:
       MaximumRows. A property that specifies the page size. The information for the property is passed from the pager; you set it internally in your control.
       StartRowIndex. A property that specifies the index of the first item on the current page. Also passed from the pager, and also set internally.
       SetPageProperties. A method that is called to alert the control that paging is occurring or that page properties (such as page size) have changed.
       TotalRowCountAvailable. An event you raise in your control when you know the total number of rows in the data set.

MaximumRows and StartRowIndex are read-only properties. The information for these properties isn t actually owned by your control; the data pager passes this information to you. However, the properties let your control expose this information publicly to other objects.

Your SetPageProperties method is called by the data pager when a paging event occurs (i.e., the user clicks a navigation button). In your implementation, you get the parameters that are sent to the method, which tell you the maximum rows (page size) and start-row index value. Based on this information, you can determine which data items to display.

Finally, you implement the TotalRowCountAvailable event. You raise this event any time you get data and can calculate how many data items there are altogether. Typically, you raise this event immediately after you ve finished executing a query. The TotalRowCountAvailable event is handled by the DataPager control so that it knows how many pages there will be and can display the correct UI.

In essence, communication between the pager and your control is via the method and the event. When you know how much data there is, you raise the TotalRowCountAvailable event to alert the pager. When the user wants to see a new page, the pager calls your SetPageProperties method to alert you.

The task of actually getting the data and displaying the correct data items is left up to you. When the SetPageProperties method is called, you need to execute a query or iterate through a collection or do whatever your control does to get the data it displays. A smart control might be able to use the start-row index and page size to construct a SQL query or to invoke a parameterized method of a data-source control. A more brute-force approach might involve re-fetching all the data, picking out the items for the current page, then discarding the rest. The exact strategy is dependent on the control and how it interacts with its data source.

In addition to implementing these members, you must override a few methods from the base control in order to perform some housekeeping. Specifically, you must tuck away the total row count in view state (or control state) for use during postback.

Using the DataPager Control

Listing One shows a custom ASP.NET server control that can use the data pager. The control derives from BulletedList and implements the IPageableItemContainer interface. The example is very simple; it was selected because the control and its data display are easy, and therefore requires little code to create a pageable version of the base control.

A lot of the work is done by the base control. The real tasks here are to communicate with the DataPager control, determine which records to display, then adjust the data set to contain only the appropriate records.

The sample control overrides the OnDataBinding event in order to implement the paging logic. In the method, it gets the entire data set by calling the equivalent member of the base control, which loads the Items collection with the data that the control would normally display. In the example, the data is cached in a local variable (_dataset); the reason for this will be explained momentarily. In the derived control, logic in the handler uses the current page information to determine which items constitute the current page. It then clears and rebuilds the Items collection with only those records.

After getting the data set, the derived control calculates the total record count. It stores this count in control state and overloads the SaveControlState and LoadControlState methods to write and read this value. The control uses control state instead of view state in case view state is disabled for the control. Using control state requires that you register this fact in the control initialization, so there s also an override of the OnInit method in order to perform the registration.

The control-state methods simply add or get the total row count from the view state information that s already maintained by the base control (if any). It s important that you save the total row count across postbacks and alert the DataPager control about the total row count as soon as you can load control state.

Whenever the control gets a total row count, it calls the RaiseTotalRowCountAvailable helper method, which raises the OnTotalRowCountAvailable event. You raise the event after control state is loaded to make sure that the pager field controls exist in time for them to handle postback events. (Hence the need to persist the total row count in control state.) Raising the event again in the OnDataBinding methods lets the pager render its current state.

Finally, the control implements the SetPageProperties method, which provides the information that you need to determine which items to display. (If you are wondering, the DataPager control s PageSize and MaximumRows properties are essentially the same thing.) This SetPageProperties method is called any time a new page of data should be displayed. Therefore, you must set the current control s RequiresDataBinding property to true so the control goes through its data-binding cycle (where the calculations are done). By the way, don t call your control s DataBind method directly; depending on where you call it, this can either put your control into an infinite loop or, at a minimum, result in calling OnDataBinding more often than needed.

When you create a control that implements IPageableItemContainer, remember there might not actually be postbacks if the DataPager control s QueryStringField property is set, paging is done by using HTTP GET commands. In other words, the page is new every time. When the DataPager control is in this mode, your control goes through its normal data binding, and at the end, raises the OnTotalRowCountAvailable event for the first time. Now the DataPager control creates its pager fields, which can then handle the paging parameters from the query string. This results in a second call to SetPageProperties, which in turn generates a second call to OnDataBinding. That s why the example code caches the data in a local member variable if the pager is using query strings, data binding is guaranteed to be called twice, and therefore caching the data is a small efficiency. (You could cache it in the ASP.NET cache, which would let you skip the data query altogether after the first time.)

Although there are a few things to think about, adding DataPager compatibility to an existing control is quite possible. Creating a version of the DataList or Repeater control that can use the pager would be slightly more complex, because those controls create child controls. But the principle is the same.

The new DataPager control is not quite as revolutionary as technologies such as LINQ, but it s another step forward in giving you control over the look and behavior of data on your Web pages. Out of the box it makes it somewhat simpler to work with the new ListView control. And with a little bit of work, you can use the capabilities of the DataPager control with a data control with which you re already familiar.

The files referenced in this article is available for download.

Mike Pope is a member of the ASP.NET user education team at Microsoft. He has been involved with the ASP.NET documentation since version 1.0 of the .NET Framework. He previously worked with other Microsoft products, such as Visual InterDev, Visual Basic, and Visual FoxPro. You can reach Mike at mailto:[email protected], or through his blog at www.mikepope.com/blog.

Begin Listing One

Imports Microsoft.VisualBasic

Imports System.Web

Imports System.Web.UI

Imports System.Web.UI.WebControls

Namespace MyControls

  Public Class PagedBulletedList

    Inherits BulletedList

    Implements IPageableItemContainer

    Private _maximumRows As Integer

    Private _startRowIndex As Integer

    Private _totalRowCount As Integer

    Private _dataset As ListItemCollection

    Protected Overrides Sub OnInit(ByVal e As EventArgs)

      ' Required in order to be able to use control state.

      MyBase.OnInit(e)

      Page.RegisterRequiresControlState(Me)

    End Sub

    Protected Overrides Function SaveControlState() As Object

      ' Add total row count to base control's

      ' saved state (if any).

      Dim additionalState As New Pair

      additionalState.First = MyBase.SaveControlState()

      additionalState.Second = _totalRowCount

      Return additionalState

    End Function

    Protected Overrides Sub LoadControlState(ByVal savedState

                                             As Object)

      If (savedState IsNot Nothing) Then

        ' Reload saved state, which includes total row count.

        Dim additionalState As Pair = savedState

        MyBase.LoadControlState(additionalState.First)

        _totalRowCount = additionalState.Second

        RaiseTotalRowCountAvailable()

      End If

    End Sub

    Protected Overrides Sub OnDataBinding(ByVal e

                                          As EventArgs)

      ' Cache data, because OnDataBinding will be called

      ' twice if the DataPager control is in QueryString

      ' mode.

      If _dataset Is Nothing Then

        _dataset = New ListItemCollection

        ' Fetch the data by invoking the base control's

        ' corresponding method. This loads the

        ' Items collection.

        MyBase.OnDataBinding(e)

        ' Load data into cache variable.

        For Each li In MyBase.Items

          _dataset.Add(li)

        Next

      End If

      _totalRowCount = _dataset.Count

      Dim lastRowIndex As Integer = (_startRowIndex +

                                     _maximumRows) - 1

      ' Adjust in case the last page has fewer items

      ' than the page size.

      If lastRowIndex >= _totalRowCount Then

        lastRowIndex = _totalRowCount - 1

      End If

      ' Recreate list of items to display based on

      ' just one page of data.

      Me.Items.Clear()

      For currentItemIndex As Integer =

        _startRowIndex To lastRowIndex

        Me.Items.Add(_dataset(currentItemIndex))

      Next

      ' Calls a method that notifies the data pager

      ' about the currently available data (incl. total

      ' row count).

      RaiseTotalRowCountAvailable()

    End Sub

    Private Sub RaiseTotalRowCountAvailable()

      Dim pagedEventArgs As PageEventArgs = _

       New PageEventArgs(_startRowIndex, _maximumRows,

                         _totalRowCount)

      OnTotalRowCountAvailable(pagedEventArgs)

    End Sub

    Public ReadOnly Property MaximumRows() As Integer _

        Implements IPageableItemContainer.MaximumRows

      Get

        Return _maximumRows

      End Get

    End Property

    Public ReadOnly Property StartRowIndex() As Integer _

        Implements IPageableItemContainer.StartRowIndex

      Get

        Return _startRowIndex

      End Get

    End Property

    Public Sub SetPageProperties(ByVal startRowIndex _

         As Integer, ByVal maximumRows As Integer, _

         ByVal databind As Boolean) _

         Implements IPageableItemContainer.SetPageProperties

      _startRowIndex = startRowIndex

      _maximumRows = maximumRows

      Me.RequiresDataBinding = True

    End Sub

    Public Event TotalRowCountAvailable(ByVal sender _

      As Object, ByVal e As PageEventArgs) _

      Implements IPageableItemContainer.TotalRowCountAvailable

    Protected Overridable Sub OnTotalRowCountAvailable( _

        ByVal e As PageEventArgs)

      RaiseEvent TotalRowCountAvailable(Me, e)

    End Sub

  End Class

End Namespace

End Listing One

Hide comments

Comments

  • Allowed HTML tags: <em> <strong> <blockquote> <br> <p>

Plain text

  • No HTML tags allowed.
  • Web page addresses and e-mail addresses turn into links automatically.
  • Lines and paragraphs break automatically.
Publish