CoreCoder
LANGUAGES: VB
TECHNOLOGIES: DataLists | Paging
Provide Pagination for Your DataLists
With a little work, your DataLists can support paging, just like DataGrid controls.
By Dino Esposito
The DataList Web control is one of three list-bound controls ASP.NET makes available to developers. The other two are the Repeater and DataGrid controls. Repeater is the simplest but most flexible of the three. DataGrid has the richest feature set but is the least flexible, tying the developer to a multicolumn, tabular view.
The DataList control falls somewhere in between these two extremes, although it's closer to DataGrid. Like Repeater, DataList displays the contents of a data-bound list through ASP.NET templates. But like DataGrid, the DataList control supports selecting and in-place editing, and you can customize its look and feel to some extent through style properties. In contrast to the Repeater control, DataList supports predefined layouts and more advanced formatting capabilities. Compared with DataGrid, however, the DataList control lacks a key feature: the ability to page through bound data.
In this article, I'll discuss one possible way to add paging support to the DataList control. First I'll implement pagination using plain ASP.NET code embedded in a host page, then I'll show you how to incorporate that code into a reusable new control, purposely called PagedDataList.
The Plain ASP.NET Way
The DataList control has a more free-form user interface than the DataGrid control. This simple fact makes DataList particularly compelling to many developers who need to create interactive reports outside the relatively standard visualization pattern that grids employ. For example, no matter how powerful and customizable a grid can be, you hardly can force it to display a data-source column in more columns of data.
By contrast, you can obtain that effect easily using DataList controls, but as soon as you scale your application to the next level, you inevitably miss the pagination feature.
Pagination is the control's ability to display equally sized blocks of data according to an internal index the user can modify through links. The DataGrid control's user interface incorporates a pager-bar element, which is nothing more than a table row with links to internal pieces of code handling the page movements. The DataList control's user interface is less restrictive and does not provide any predefined link bar for pagination. All that's necessary is for the host page to include, in the body of the page, a couple links to move the DataList control's data source back and forth:
Private Sub OnPreviousPage(sender As Object, e As EventArgs)
CurrentPageIndex -= 1
RefreshPage()
End Sub
Private Sub OnNextPage(sender As Object, e As EventArgs)
CurrentPageIndex += 1
RefreshPage()
End Sub
RefreshPage is a page-level routine. First, it gets the data to bind from the database or, more logically, from a server-side cache. Next, it adjusts the page index - the global member CurrentPageIndex - and binds data to the original DataList control:
Private Sub RefreshPage()
Dim dt As DataTable = LoadData()
AdjustPageIndex(dt.Rows.Count)
CurrentPage.Text = (CurrentPageIndex + 1).ToString()
list.DataSource = GetPage(dt, CurrentPageIndex)
list.DataBind()
End Sub
The GetPage method is responsible for extracting from the DataTable object the subset of rows that fit into the current page. Figure 1 shows one possible implementation of the GetPage method.
Function GetPage(ByVal dt As DataTable, _
ByVal pageIndex As Integer) As Object
If dt Is Nothing Then
dt = LoadData()
End If
Dim firstIndexInPage As Integer = _
(CurrentPageIndex*PageSize)
Dim rows As DataRowCollection = dt.Rows
Dim i As Integer
Dim target As DataTable = dt.Clone()
For i=0 To PageSize-1
Dim index As Integer = i+firstIndexInPage
If index < rows.Count Then
target.ImportRow(rows(i+firstIndexInPage))
Else
Exit For
End If
Next
Return target
End Function
Figure 1. The GetPage method clones the original table of data and creates a new, smaller table with only the rows that fit into the specified page index. The ImportRow method allows you to duplicate and copy a new row from one DataTable object to another.
A couple global members, CurrentPageIndex and PageSize, play a key role in this infrastructure. The former contains the zero-based index for the current page, and the latter defines the (constant) maximum number of rows permitted per page:
Public PageSize As Integer = 12
Public Property CurrentPageIndex As Integer
Get
Return ViewState("CurrentPageIndex")
End Get
Set(ByVal value As Integer)
ViewState("CurrentPageIndex") = value
End Set
End Property
Interestingly, unlike PageSize, CurrentPageIndex must be persisted across multiple page requests. You can accomplish this easily using the ViewState collection. As the previous code example clearly shows, the property's value is not stored in a class member, only in the page's ViewState bag. This ensures it's the property's value will always be current. Figure 2 shows the output of a sample page written according to these guidelines.
Figure 2. The more than 90 customers of the
Northwind database display one page at a time in three columns of data. The
link buttons provide for page movements and cause the DataList to refresh its
contents.
Pack it Into a New Control
So far, so good. But can we really jump for joy with such a result? The amount of code needed, though not huge, is not easily extensible and replicable in other pages. So, the perfect solution is to design a customized version of the DataList control that packs all the necessary paging code into a compiled assembly linked to the page. Enter the PagedDataList control:
Public Class PagedDataList
Inherits System.Web.UI.WebControls.DataList
...
End Class
Figure 3 summarizes the changes I made to the original programming interface to make it work as a pageable data-bound control.
Name |
Type |
Description |
CurrentPageIndex |
Property |
The index of the current page |
PageSize |
Property |
The maximum number of items to place onto a page |
MovePrevious |
Method |
Moves to the previous page, if any |
MoveNext |
Method |
Moves to the next page, if any |
DataBind |
Method override |
Manages the pagination when the DataList is refreshed |
PageIndexChanged |
Event |
Fires when the DataList is about to display a new page |
Figure 3. This table summarizes the necessary updates to have the DataList control work as a paged data-bound control. Overriding the base DataBind method is one of the key changes.
When I tackled the design of the new DataList control, I identified a couple hurdles. The first had to do with the visual controls that actually provide for pagination - what the DataGrid control calls the pager bar. The second issue revolves around the most effective way to implement pagination, namely extracting the subset of rows to fit into the current page.
The DataList has a different graphical layout compared to the DataGrid control. Among the differences is that the DataList control has no pager bar and, subsequently, knows nothing about the page-level control that implements page movements. For this reason, I decided to introduce a pair of new methods, MovePrevious and MoveNext, which you won't find in the DataGrid control's API. These methods can be called from the page in response to users clicking on any of the controls providing pagination. MovePrevious and MoveNext have a simple implementation: They simply update the internal page counter, the CurrentPageIndex property.
The key operation for a pageable control is extracting the right subset of rows representing the current output. In the earlier plain ASP.NET example, I used a GetPage method to clone a portion of the data source and bind it to the DataList. You can implement this behavior more effectively by overriding the base DataBind method. This way, you realize a better encapsulation of the logic and are able to limit the number of other changes you need to make to the control.
Override the DataBind Method
Within the body of the DataBind override, a few things happen. First, the new method adjusts the current index of the page. In particular, the method ensures the value of the CurrentPageIndex property does not exceed the maximum number of pages allowed by the current page size (the PageSize property) and that the index's value is not less than zero. The current page count is not cached, but it's calculated whenever the data is bound. An alternate approach would be to use a read-only PageCount property, updated whenever PageSize and DataSource are updated.
Calculating the page count is possible only once the data source is bound to the control. As you should know, the data source must be re-associated with the DataList every time the host ASP.NET page is redrawn. The implementation of This article's sample application (see the Download box for details) supports only data sources: a DataTable or a DataView. The key code of the DataBind override comes down to these two lines:
SetPage()
MyBase.DataBind()
When DataBind is called, the DataSource has been set already. Thus, the first line manipulates the current content of the DataSource property and extracts the subset of rows that fit into the current page. The page content is stored in a temporary cloned DataTable object, which is assigned to the DataSource overriding the previous setting. In other words, SetPage replaces the data-source object the user sets, with the subset of it that contains only the rows for the current page.
This design allows for custom paging, too. You define a new Boolean AllowCustomPaging property, then, according to that value, you decide whether SetPage must shrink the size of the DataSource (automatic paging) or leave it as is (custom paging). Finally, to have ASP.NET perform the standard processing and control rendering, call the base DataBind method.
To extract rows, I employed the same algorithm described earlier in the plain ASP.NET code. The CurrentPageIndex and PageSize properties store and retrieve their own values from the control's ViewState collection. Although the final code for the properties looks nearly identical in the two cases, the ViewState objects involved are different. In the plain ASP.NET code, CurrentPageIndex is saved directly in the page's ViewState collection. When exposed as a control property, however, CurrentPageIndex - and any other stateful properties - are saved to the control's ViewState bag. The control's state collection is a protected resource and is inaccessible from page-level code. The page's ViewState collection, however, is filled by pouring each constituent control's ViewState into it.
Just before refreshing the control's output, the overridden DataBind method fires an event to let the host page know about the imminent page change:
Dim e As DataListPageChangedEventArgs
e = New DataListPageChangedEventArgs()
e.NewPageIndex = CurrentPageIndex
e.IsLastPage = (CurrentPageIndex = (pageCount - 1))
RaiseEvent PageIndexChanged(Me, e)
The custom event data structure contains two properties that inform the page about the new page index and whether it will be the last page. Using this information, the page easily can gray out the link buttons for paging:
Sub PageIndexChanged(sender As Object, _
e As DataListPageChangedEventArgs)
PrevPage.Enabled = (e.NewPageIndex > 0)
NextPage.Enabled = Not e.IsLastPage
End Sub
You use the PagedDataList control in an aspx page with the same syntax you would employ for a DataList control. In addition, you can set a few extra properties and events:
... OnPageIndexChanged="PageIndexChanged"> ...
Figure 4 shows the control in action in the sample page.
Figure 4. The PagedDataList control fires an
event whenever the page is about to change. Using that information, the host
page can update the label with the current page index and disable paging links
as appropriate.
In this article, I implemented two different solutions - one using plain ASP.NET code and one based on a reusable custom control. I didn't make use of the PagedDataSource class, which provides paging services for data-bound controls automatically. Using that class would make implementing paging for custom controls even more general and consistent.
The project referenced in this article is available for download.
Dino Esposito is a trainer and consultant for Wintellect (http://www.wintellect.com) where he manages the ADO.NET class. Dino writes the "Cutting Edge" column for MSDN Magazine and "Diving Into Data Access" for MSDN Voices. Dino is the author of Building Web Solutions with ASP.NET and ADO.NET and Applied XML Programming for Microsoft .NET (Microsoft Press). He also is a cofounder of http://www.VB2TheMax.com. E-mail him at mailto:[email protected].
Tell us what you think! Please send any comments about this article to [email protected]. Please include the article title and author.