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:
Public Class TabbedGrid
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) _
CurrentPageIndex = e.NewPageIndex
Dim tguve As TabbedGridUpdateViewEventArgs
tguve = New TabbedGridUpdateViewEventArgs()
Dim dgpt As DataGridPageTab = Tabs(CurrentPageIndex)
tguve.TabKeyValue = dgpt.KeyValue
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)
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
Public TabKeyValue As Object
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()
Public Sub New(ByVal title As String, _
ByVal key As Object)
Text = title
SelectedText = title
KeyValue = key
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), _
Dim o As Object = pager.Controls(i)
If TypeOf (o) Is LinkButton Then
Dim lb As LinkButton = CType(o, LinkButton)
lb.Text = "" & dgpt.Text & ""
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)
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
Set(ByVal Value As Color)
ViewState("UnselectedTabColor") = Value
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
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:
AutoGenerateColumns="false" Font-Size="8pt" Font-Names="Verdana" PageSize="100" OnUpdateView="UpdateViewHandler"> ...
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"
o = New DataGridPageTab()
s = "E-K"
o.Text = s
o.KeyValue = "E-F-G-H-I-J-K"
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"
o = New DataGridPageTab()
s = "S-Z"
o.Text = s
o.KeyValue = "S-T-U-V-W-X-Y-Z"
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)
Private Sub UpdateView(letters As String)
grid.DataSource = CreateDataSource(letters)
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:[email protected].
Tell us what you think! Please send any comments about this article to [email protected]. Please include the article title and author.
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.