Provide Pagination for Your DataLists
With a little work, your DataLists can support paging, just like DataGrid controls.
October 30, 2009
CoreCoder
LANGUAGES: VB
TECHNOLOGIES: DataLists| Paging
ProvidePagination for Your DataLists
With alittle work, your DataLists can support paging, just like DataGrid controls.
By DinoEsposito
The DataListWeb control is one of three list-bound controls ASP.NET makes available todevelopers. The other two are the Repeater and DataGrid controls.Repeater is the simplest but most flexible of the three. DataGridhas the richest feature set but is the least flexible, tying the developer to amulticolumn, tabular view.
The DataListcontrol falls somewhere in between these two extremes, although it's closer to DataGrid.Like Repeater, DataList displays the contents of a data-boundlist through ASP.NET templates. But like DataGrid, the DataListcontrol supports selecting and in-place editing, and you can customize its lookand feel to some extent through style properties. In contrast to the Repeatercontrol, DataList supports predefined layouts and more advancedformatting capabilities. Compared with DataGrid, however, the DataListcontrol lacks a key feature: the ability to page through bound data.
In thisarticle, I'll discuss one possible way to add paging support to the DataListcontrol. First I'll implement pagination using plain ASP.NET code embedded in ahost page, then I'll show you how to incorporate that code into a reusable newcontrol, purposely called PagedDataList.
The Plain ASP.NET Way
The DataListcontrol has a more free-form user interface than the DataGrid control.This simple fact makes DataList particularly compelling to manydevelopers who need to create interactive reports outside the relativelystandard visualization pattern that grids employ. For example, no matter howpowerful and customizable a grid can be, you hardly can force it to display adata-source column in more columns of data.
Bycontrast, you can obtain that effect easily using DataList controls, butas soon as you scale your application to the next level, you inevitably missthe pagination feature.
Paginationis the control's ability to display equally sized blocks of data according toan internal index the user can modify through links. The DataGrid control'suser interface incorporates a pager-bar element, which is nothing more than atable row with links to internal pieces of code handling the page movements.The DataList control's user interface is less restrictive and does notprovide any predefined link bar for pagination. All that's necessary is for thehost page to include, in the body of the page, a couple links to move the DataListcontrol's data source back and forth:
Private SubOnPreviousPage(sender As Object, e As EventArgs)
CurrentPageIndex -= 1
RefreshPage()
End Sub
Private SubOnNextPage(sender As Object, e As EventArgs)
CurrentPageIndex += 1
RefreshPage()
End Sub
RefreshPage is a page-level routine. First, itgets the data to bind from the database or, more logically, from a server-sidecache. Next, it adjusts the page index - the global member CurrentPageIndex- and binds data to the original DataList control:
Private SubRefreshPage()
Dim dt As DataTable = LoadData()
AdjustPageIndex(dt.Rows.Count)
CurrentPage.Text = (CurrentPageIndex +1).ToString()
list.DataSource = GetPage(dt,CurrentPageIndex)
list.DataBind()
End Sub
The GetPagemethod is responsible for extracting from the DataTable object thesubset of rows that fit into the current page. Figure 1 shows one possibleimplementation 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 AsInteger = _
(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
Figure1. The GetPagemethod clones the original table of data and creates a new, smaller table withonly the rows that fit into the specified page index. The ImportRow methodallows you to duplicate and copy a new row from one DataTable object toanother.
A coupleglobal members, CurrentPageIndex and PageSize, play a key role inthis infrastructure. The former contains the zero-based index for the currentpage, and the latter defines the (constant) maximum number of rows permittedper page:
Public PageSize As Integer = 12
Public PropertyCurrentPageIndex As Integer
Get
ReturnViewState("CurrentPageIndex")
End Get
Set(ByVal value As Integer)
ViewState("CurrentPageIndex")= value
End Set
End Property
Interestingly,unlike PageSize, CurrentPageIndex must be persisted acrossmultiple page requests. You can accomplish this easily using the ViewStatecollection. As the previous code example clearly shows, the property's value isnot stored in a class member, only in the page's ViewState bag. Thisensures it's the property's value will always be current. Figure 2 shows theoutput of a sample page written according to these guidelines.
Figure 2. The more than 90 customers of theNorthwind database display one page at a time in three columns of data. Thelink buttons provide for page movements and cause the DataList to refresh itscontents.
Pack it Into a New Control
So far,so good. But can we really jump for joy with such a result? The amount of codeneeded, though not huge, is not easily extensible and replicable in otherpages. So, the perfect solution is to design a customized version of the DataListcontrol that packs all the necessary paging code into a compiled assemblylinked to the page. Enter the PagedDataList control:
Public Class PagedDataList
Inherits System.Web.UI.WebControls.DataList
...
End Class
Figure 3summarizes the changes I made to the original programming interface to make itwork 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 |
Figure3. This tablesummarizes the necessary updates to have the DataList control work as a pageddata-bound control. Overriding the base DataBind method is one of the keychanges.
When Itackled the design of the new DataList control, I identified a couplehurdles. The first had to do with the visual controls that actually provide forpagination - what the DataGrid control calls the pager bar. Thesecond issue revolves around the most effective way to implement pagination,namely extracting the subset of rows to fit into the current page.
The DataListhas a different graphical layout compared to the DataGrid control. Amongthe differences is that the DataList control has no pager bar and,subsequently, knows nothing about the page-level control that implements pagemovements. For this reason, I decided to introduce a pair of new methods, MovePreviousand MoveNext, which you won't find in the DataGrid control'sAPI. These methods can be called from the page in response to users clicking onany of the controls providing pagination. MovePrevious and MoveNexthave a simple implementation: They simply update the internal page counter, theCurrentPageIndex property.
The keyoperation for a pageable control is extracting the right subset of rowsrepresenting the current output. In the earlier plain ASP.NET example, I used aGetPage 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 DataBindmethod. This way, you realize a better encapsulation of the logic and are ableto limit the number of other changes you need to make to the control.
Override the DataBind Method
Withinthe body of the DataBind override, a few things happen. First, the newmethod adjusts the current index of the page. In particular, the method ensuresthe value of the CurrentPageIndex property does not exceed the maximumnumber 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 notcached, but it's calculated whenever the data is bound. An alternate approachwould be to use a read-only PageCount property, updated whenever PageSizeand DataSource are updated.
Calculatingthe 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 DataListevery time the host ASP.NET page is redrawn. The implementation of Thisarticle's sample application (see the Download box for details) supports onlydata sources: a DataTable or a DataView. The key code of the DataBindoverride comes down to these two lines:
SetPage()
MyBase.DataBind()
When DataBindis called, the DataSource has been set already. Thus, the first linemanipulates the current content of the DataSource property and extractsthe subset of rows that fit into the current page. The page content is storedin a temporary cloned DataTable object, which is assigned to the DataSourceoverriding the previous setting. In other words, SetPage replaces thedata-source object the user sets, with the subset of it that contains only therows for the current page.
Thisdesign allows for custom paging, too. You define a new Boolean AllowCustomPagingproperty, then, according to that value, you decide whether SetPage mustshrink the size of the DataSource (automatic paging) or leave it as is(custom paging). Finally, to have ASP.NET perform the standard processing andcontrol rendering, call the base DataBind method.
Toextract rows, I employed the same algorithm described earlier in the plainASP.NET code. The CurrentPageIndex and PageSize properties storeand retrieve their own values from the control's ViewState collection.Although the final code for the properties looks nearly identical in the twocases, the ViewState objects involved are different. In the plainASP.NET code, CurrentPageIndex is saved directly in the page's ViewStatecollection. When exposed as a control property, however, CurrentPageIndex- and any other stateful properties - are saved to the control's ViewStatebag. The control's state collection is a protected resource and is inaccessiblefrom page-level code. The page's ViewState collection, however, isfilled by pouring each constituent control's ViewState into it.
Justbefore refreshing the control's output, the overridden DataBind methodfires an event to let the host page know about the imminent page change:
Dim e As DataListPageChangedEventArgs
e = NewDataListPageChangedEventArgs()
e.NewPageIndex= CurrentPageIndex
e.IsLastPage =(CurrentPageIndex = (pageCount - 1))
RaiseEventPageIndexChanged(Me, e)
Thecustom event data structure contains two properties that inform the page aboutthe new page index and whether it will be the last page. Using thisinformation, 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 usethe PagedDataList control in an aspx page with the same syntax you wouldemploy for a DataList control. In addition, you can set a few extraproperties and events:
... OnPageIndexChanged="PageIndexChanged"> ... Figure 4 shows the control in action in the sample page.
Figure 4. The PagedDataList control fires anevent whenever the page is about to change. Using that information, the hostpage can update the label with the current page index and disable paging linksas appropriate. In thisarticle, I implemented two different solutions - one using plain ASP.NET codeand one based on a reusable custom control. I didn't make use of the PagedDataSourceclass, which provides paging services for data-bound controls automatically.Using that class would make implementing paging for custom controls even moregeneral and consistent. The project referenced in thisarticle is available for download. DinoEsposito is a trainer and consultant for Wintellect (http://www.wintellect.com) where hemanages the ADO.NET class. Dino writes the "Cutting Edge" column for MSDN Magazineand "Diving Into Data Access" for MSDN Voices. Dino is the author of Building Web Solutions with ASP.NET and ADO.NET and Applied XMLProgramming 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 aboutthis article to [email protected] include the article title and author.
About the Author
You May Also Like