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