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:mike.pope@microsoft.com, 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