CoreCoder
Languages: VB
Technologies: DataGrid
| Logical Pagination | Custom Control | Inheritance
Build a DataGrid With Tabs
Here's a new control to manage logical pages of data.
By Dino Esposito
In a previous installment of this column (formerly called
"DataBound"), I presented a DataGrid Web control with the special
ability to group and page data according to more sophisticated criteria than
the simple order position in the source (see Logical
Navigation). Normally, a DataGrid - the only ASP.NET data-bound
control with paging capabilities - shows pages representing sequential rows of
data. You select the page you want by clicking on the links in the pager bar.
The pager bar is one of the DataGrid's constituent items that you can
customize to some extent. The DataGrid's standard programming interface
allows you to choose between two options: a Next/Previous pair of buttons or a
numbered list of hyperlinks each pointing to a different page. In that article,
I illustrated some code that, working on top of the DataGrid control,
modifies the pager bar content to make it point to logical rather than physical
pages (see the sidebar "What's a Logical Page, Anyway").
If you read the June installment of this column, you
should know how to implement this functionality on top of a standard DataGrid
control. Some readers realized that, although functional, that code is tailored
to the specific sample and doesn't fit seamlessly into other scenarios. In this
article, I'll show you how to build a new Web control that encapsulates the
gist of logical pagination. The new control, called TabbedGrid, derives
from the DataGrid class, generalizes the mechanism for defining logical
pages, and accepts any input parameters through easy-to-use properties and
collection.
Overview of the TabbedGrid Control
See FIGURE 1 for a quick reminder of how the grid control
in the June installment of this column works.
FIGURE 1: Here is a tabbed grid control that groups data by month. Each
tab represents a logical page (orders per month) but is actually implemented
through a physical DataGrid page.
The difference now is we're using an all-encompassing Web
control that simplifies programming greatly. The TabbedGrid control is a
DataGrid control that gives its pager bar a tab-like look and feel and,
more importantly, lets you page through the whole data set by logical pages
(month, initials, day, any key) rather than page numbers. Normally, you keep the
size of each page constant and vary the pager buttons to cover the data source.
The TabbedGrid control does the reverse: It keeps the pager buttons
constant but provides variable-length pages.
The TabbedGrid control inherits from the DataGrid
class, so you should be familiar with the required programming model already:
Namespace BWSLib
Namespace Controls
Public Class
TabbedGrid
Inherits
DataGrid
...
End Class
End Namespace
End Namespace
The default programming interface of the DataGrid
is extended in two ways. First, the TabbedGrid control exposes a
collection property called Tabs that represents all the tabs you want
the final grid to display. Second, the DataBind method of the DataGrid
is overridden to set the correct number of pager buttons automatically based on
the tabs added.
From a programmer's standpoint, you should plan for a
couple things if you want to use the TabbedGrid control: You should add
as many tabs as needed upon page loading and provide an application-specific
method to query for page data. Such a method should take in a parameter being
the key value for the logical page to display. It goes without saying that the TabbedGrid
control supports only custom paging and has the pager bar always working in page
numeric mode.
Although the final user interface might make you think of
a radically different feature, each tab is simply an active link to an
available page. Subsequently, each click on a tab is perceived as a page change
event and fires the standard PageIndexChanged event.
The TabbedGrid control handles both the ItemCreated
and PageIndexChanged events internally. The ItemCreated event is
used to make up the pager bar and turn its fa ade into a tab strip. The PageIndexChanged
event sets the new page index and fires a tailor-made event called UpdateView
to force the client page to refresh the grid's view.
Public Sub Internal_PageIndexChanged( _
ByVal sender As Object, _
ByVal e As DataGridPageChangedEventArgs) _
Handles MyBase.PageIndexChanged
CurrentPageIndex =
e.NewPageIndex
Dim tguve As
TabbedGridUpdateViewEventArgs
tguve = New
TabbedGridUpdateViewEventArgs()
Dim dgpt As
DataGridPageTab = Tabs(CurrentPageIndex)
tguve.TabKeyValue =
dgpt.KeyValue
OnUpdateView(tguve)
End Sub
FIGURE 2: The internal handler for the
PageIndexChanged event adjusts the new page index and fires the custom
UpdateView event.
FIGURE 2 contains the TabbedGrid code that handles
the PageIndexChanged event in the derived class. Let's briefly discuss
the custom classes involved with this operation. The UpdateView event is
declared like this:
Public Delegate Sub TabbedGridUpdateViewEventHandler( _
ByVal sender As Object,
_
ByVal e As
TabbedGridUpdateViewEventArgs)
Public Event UpdateView As TabbedGridUpdateViewEventHandler
Sub OnUpdateView(ByVal e As TabbedGridUpdateViewEventArgs)
RaiseEvent
UpdateView(Me, e)
End Sub
You must use a made-to-measure delegate because you need
to pass specific data down to the client handler, which is the key value to use
to fetch data for the current page. The custom event data is grouped into a new
data structure called TabbedGridUpdateViewEventArgs. This class is quite
simple as this code clearly demonstrates:
Public NotInheritable Class TabbedGridUpdateViewEventArgs
Inherits EventArgs
Public TabKeyValue As
Object
End Class
Each tab is rendered using an instance of the DataGridPageTab
class, whose structure you can see in FIGURE 3.
Public NotInheritable Class DataGridPageTab
Public Text As String
= ""
Public SelectedText As
String = ""
Public TooltipText As
String = ""
Public KeyValue As
Object
Public Sub New()
End Sub
Public Sub New(ByVal
title As String, _
ByVal key As Object)
Text = title
SelectedText = title
KeyValue = key
End Sub
End Class
FIGURE 3: Here is the definition of the class that
represents an individual tab in the TabbedGrid control.
A DataGrid tab is characterized by two text strings
- one for the unselected state and one to use when the tab is selected. In
addition, a tab can have tool tip text that works only in unselected mode and
an object representing the tab's key value. The tab's key value would be the
index of the month if you were going to create logical pages based on month
names. In general, the tab's key value can be anything that allows you to set
up and execute a successful query to fill the current page.
Build the Tab Strip
As I discussed in the previous column, the tab strip atop
the DataGrid in Figure 1 is rather fictitious and obtained only with a
sapient use of Cascading Style Sheets (CSS) and table cell properties. The code
that creates the tab strip runs during the DataGrid's ItemCreated
event when the control is going to create the pager.
If the pager item being created is a LinkButton,
you're processing an unselected tab. The code draws the link button with an
opaque background and a border. You can set the color for the background
programmatically using the TabbedGrid's specific UnselectedTabColor
property. If the pager item is a Label, the item represents the current
page and gets rendered with a slightly higher height and the background color
of the header. The color of the borders is also adjusted so the bottom line
takes the same color as the background.
Bear in mind that a pager bar is made of a sequence of
link buttons (available pages) and one label (the current page) interspersed
with blank literal controls - the HTML escaped expression.
For a better graphical rendering, literal controls are set to this empty
string.
The text of each tab is modified by reading the contents
of the page-specific DataGridPageTab control. You use the Text
property whenever the control is not selected and SelectedText
otherwise. You also can give the anchor tag used for active link buttons a tool
tip if the TooltipText property is set to a non-empty value:
Dim dgpt As DataGridPageTab = CType(Tabs(i / 2), _
DataGridPageTab)
Dim o As Object = pager.Controls(i)
If TypeOf (o) Is LinkButton Then
Dim lb As LinkButton =
CType(o, LinkButton)
lb.Text =
"<span style=margin:2>" & dgpt.Text &
"</span>"
lb.BorderWidth =
Unit.Pixel(1)
lb.BorderColor =
Color.White
lb.BackColor =
UnselectedTabColor
lb.BorderStyle =
BorderStyle.Outset
lb.ToolTip =
dgpt.TooltipText
lb.Height =
Unit.Pixel(18)
Else
...
End If
The displayed text is padded with a few pixels on both
sides to make it legible. The color you define to be the background color of
all unselected tabs is stored in the UnselectedTabColor property. Such a
value is made persistent across multiple page requests using the grid's ViewState
bag property:
Public Property UnselectedTabColor() As Color
Get
Return
ViewState("UnselectedTabColor")
End Get
Set(ByVal Value As
Color)
ViewState("UnselectedTabColor")
= Value
End Set
End Property
Put It All Together
Let's see now how you actually use the TabbedGrid
control to page through the list of your customers, grouping them by initials.
You define the tabs by creating new instances of the DataGridPageTab
class and adding them to the control's Tabs collection. The Tabs
property is implemented through an ArrayList object and is not persisted
across multiple page requests. For this reason, you must reinitialize it at
each postback event. Also notice that if you want to change this code and make
the Tabs property go into the ViewState bag, you must first
declare the DataGridPageTab structure as serializable using the <serializable>
attribute.
This code shows how to insert a TabbedGrid control onto
an ASP.NET page; the only difference from an ordinary DataGrid control
is the OnUpdateView event and the tag name:
<expo:TabbedGrid runat="server" id="grid"
AutoGenerateColumns="false"
Font-Size="8pt" Font-Names="Verdana"
PageSize="100"
OnUpdateView="UpdateViewHandler">
...
</expo:TabbedGrid>
Bear in mind that in order to use a custom control in
ASP.NET, you must register it first. Here's the prototype of the code you use:
<%@ Register TagPrefix="expo"
Namespace="BWSLib.Controls" Assembly="TabbedGrid" %>
The content of the TagPrefix attribute is up to
you. Namespace must match the hosting namespace of the control class and
Assembly contains the name of the assembly without the extension..
The key operation you perform on the TabbedGrid
control is setting up the Tabs collection - an array of DataGridPageTab
objects. Because Tabs is not a persistent attribute, you must
reinitialize it every time the ASP.NET page is loaded. In FIGURE 4, you can see
the code for the sample page's Page_Load event.
Public Sub Page_Load(sender As Object, e As EventArgs)
Dim s As String
Dim o As DataGridPageTab
o = New
DataGridPageTab()
s = "A-D"
o.Text = s
o.KeyValue =
"A-B-C-D"
grid.Tabs.Add(o)
o = New
DataGridPageTab()
s = "E-K"
o.Text = s
o.KeyValue =
"E-F-G-H-I-J-K"
grid.Tabs.Add(o)
o = New
DataGridPageTab()
s = "L-R"
o.TooltipText =
"Customers ranging from L to R"
o.Text = s
o.KeyValue =
"L-M-N-O-P-Q-R"
grid.Tabs.Add(o)
o = New
DataGridPageTab()
s = "S-Z"
o.Text = s
o.KeyValue =
"S-T-U-V-W-X-Y-Z"
grid.Tabs.Add(o)
LoadData
End Sub
FIGURE 4: Initializing the tabs for the sample page.
The sample page displays the customers stored in the
Northwind database grouping them in four pages, each containing the names that
begin with a given range of initials. For example, the first page includes
customers with names beginning with the letters A through D whereas the second
page ranges from E through K and so on.
Each tab is characterized by a new instance of the DataGridPageTab
class whose Text property is set with the display text. SelectedText,
if set, is used to title the tab when selected. If it's not set, SelectedText
equals Text. The TooltipText is the balloon text that pops up
when the user hovers on a certain tab without selecting it. In the sample code,
only the third page (L through R) has a tool tip. Finally, the KeyValue
property contains any value that the application can use to retrieve the
physical data rows to display. In this case, KeyValue is set with a
dash-separated string of letters - all the initials for the customers that
belong to the page. This information is then passed to the code that performs
the query. When the page refreshes, this code runs:
Private Sub UpdateViewHandler(sender As Object, e As
TabbedGridUpdateViewEventArgs)
UpdateView(e.TabKeyValue)
End Sub
Private Sub UpdateView(letters As String)
grid.DataSource =
CreateDataSource(letters)
grid.DataBind()
End Sub
The CreateDataSource method takes the initials -
say A, B, C, and D - and sets up a SQL command that looks like this:
SELECT customerid, companyname FROM customers WHERE
companyname LIKE
'A%' OR
companyname LIKE
'B%' OR
companyname LIKE
'C%' OR
companyname LIKE 'D%'
The code splits the dash-separated string into an array,
loops on the items, and creates the WHERE clause dynamically. The
content of KeyValue and how you use it are programming aspects
completely up to you. For example, if you must group data by months, a good
value for KeyValue is the number of the month, which might lead you to a
SQL command like this:
SELECT * FROM SomeTable WHERE
Month(SomeDateField) =
month
FIGURE 5 shows the customers.aspx page that makes use of
the TabbedGrid control.
FIGURE 5: The Customers.aspx sample page in action. Notice the
letter-based grouping and the tool tips.
In the accompanying source code, you'll find the Visual
Basic .NET source code of the TabbedGrid control and a couple sample
pages. The control's source code is inserted in a Visual Studio project that
creates a Web Class Library. To run it, you must create a virtual directory
that points to the installation path of the project and adjust the URL for
debugging in the Configuration Manager window of the project.
The TabbedGrid control doesn't solve what appears
to be the toughest issue of logical pagination: being able to page through the
page content in case the query selects too many items. This is not exactly a trivial point and must be carefully
addressed. Stay tuned!
The files referenced in this
article are available for download.
Dino Esposito is a trainer and consultant for Wintellect (http://www.wintellect.com) where he
manages the ADO.NET class. Dino is the author of Building Web Solutions with ASP.NET and ADO.NET and
the upcoming Applied
XML Programming for Microsoft .NET both from Microsoft Press. Dino is
also cofounder of http://www.VB2TheMax.com.
E-mail him at mailto:dinoe@wintellect.com.
Tell us what you think! Please send any comments about this
article to mailto:feedback@aspnetPRO.com.
Please include the article title and author.
What's a Logical Page, Anyway?
A page is formed by a block of contiguous records whose
number is determined by the fixed page size. The records that actually fall in
a given page occupy a certain range of positions according to the current
order. In general, a logical page is the result of a query performed on the
data source. The number of records returned is not known beforehand unless use
special clauses such as SQL Server's TOP. Unlike physical pages, which
are selected by page number, logical pages are selected using ad-hoc
information such as a month's name, a day of the week, or a range of initials.