Back when I was doing plain old ASP (ah, those were the days), I fooled around with creating a DHTML tab control like the kind I used to use in VB6. Multiple rows of tabs which rearranged themselves as you clicked them, and that sort of thing.
When I started programming in ASP.NET, I found that things weren't quite as flexible. I wanted the convenience of a tab control, but all I could find were Microsoft's orphaned
MultiPage controls, which have to be installed on the server, which use .htc files, which have to be dealt with separately, and which really aren't very friendly.
I like friendly controls.
I looked all over the Internet and found nothing even remotely similar to what I was looking for, so I decided to make one myself.
XTable control, this one is probably not cross-browser compatible. But then, I haven't actually tried it on other browsers, so it might surprise me.
XTabControl is the third tab control I built. The first two were horrendous, kludgy things. One of the interesting things that I learned had to do with collections.
Microsoft often recommends that people have their collections inherit from
CollectionBase. Don't do that if you want the elements of your collection to be able to contain other controls. That was what I did my first time though, and while the controls on each tab showed up nicely, they weren't in the control hierarchy, and they got ignored on postback. I tried using metaphorical twine and duck tape to force the controls to maintain their state and for their data to come through, and I was marginally successful. But I could not, for love or money, get them to raise their events on the server.
Finally, I decided to cannibalize code that I already knew worked.
That's More Like It
I had used Lutz Roeder's Reflector to obtain an open copy of the code for the standard Microsoft
Table server control, when I was making the
XTable control. I've included this in the source code as
The way I figured it, controls put into
TableCells don't have the problem I was running into, so I could use that code as a base for my tab control. Of course, the
Table control has a three-level hierarchy (
TableCell), and I only needed two levels (
XTab), so I based my control on the
The architecture was entirely different from what I'd tried before. The
TableCellCollection class, which became my
XTabCollection, was defined as a class of its own, but the
TableRow class, which became my
XTable class, also contained a nested
CellControlCollection, which became my
XTabControlCollection. The terminology got a little confusing, because it sounded as though this was a collection of
XTabControls, but I figured since it was nested within
XTabControl, it would be clear enough.
XTabCollection didn't inherit from anything, but did implement
XTabControl.XTabControlCollection inherited from
ControlCollection, which made sure that each
XTab, and all the child controls of each
XTab, would be part of the control hierarchy.
Design and Properties
One of the things that really irked me about the VB6
TabControl was the inability to change the background color of the tabs. I was determined not to make the same mistake with my control.
Some of the properties were obvious, like
SelectedTab. Others were less so. The non-obvious properties I decided on were:
TabsPerRow property was something I remembered from the VB6
TabHeight would determine the height of all the
XTabs. This had to be global, because having
XTabs of varying heights would have been, if not impossible, certainly unnecessarily complicated. I probably could have put the
TabFontSize properties on individual
XTabs, but I thought that would be ugly.
Aside from allowing the user to choose the color of each tab, both
BackColor, I decided that the space available on the
XTab shouldn't be limited by the dimensions of the
XTabControl. By creating
InnerHeight properties on each
XTab, it's possible to use more or less space than is available. The demo project demonstrates this, as does the GIF at the beginning of this article.
Of course, the most complex issues were sizing and fitting the tabs into their correct default positions, and allowing them to move as the user clicks them. On a standard tab control, if you click on a tab in a back row, that entire row drops down to be the row immediately above the tab pages, and I needed to emulate this behavior.
For the positioning and sizing of the tabs, I wrote a method which calculated the width required for each tab in full rows (rows which contained the full allowable number of tabs), and that required for each tab in the partial row, if there was one, at the back.
For example, if
TabsPerRow is set to 3, and there are 5
XTabs in the control, there would be one full row of 3
XTabs and one partial row of 2
XTabs. It's fairly math heavy. Then I created a jagged array of
Pairs to hold references to the correct
XTabs. For each
First would hold the
XTab.Index value, and
Second would hold the width of that
XTab. During rendering, I simply read out of this array.
Private Sub GetTabRows(ByRef _rowArray As Pair()(), _
ByRef _selectedRow As Integer, _
ByRef _tabPageHeight As Unit)
Dim _allTabCount As Integer = Tabs.Count
Dim _showTabCount As Integer = _allTabCount
Dim _fullRows As Integer
Dim _partRowTabWidth As Unit
Dim _widthValue As Integer = CInt(Width.Value)
If Not HttpContext.Current Is Nothing Then
Dim _visibleTabCount As Integer = 0
For Each _tab As XTab In Tabs
If _tab.Visible Then
_visibleTabCount += 1
_showTabCount = _visibleTabCount
Dim _tabsInFullRow As Integer = TabsPerRow
Dim _fullRowTabWidth As Unit = _
Unit.Pixel(CInt(Math.Floor(_widthValue / _
Dim _tabRows As Integer = _
CInt(Math.Ceiling(CDbl(_showTabCount) / _
If _tabRows * _tabsInFullRow = _showTabCount Then
_fullRows = _tabRows
_fullRows = _tabRows - 1
Dim _tabsInPartRow As Integer = _showTabCount - _
(_fullRows * _tabsInFullRow)
If _tabsInPartRow > 0 Then
_partRowTabWidth = _
Unit.Pixel(CInt(Math.Floor(_widthValue / _
_partRowTabWidth = _fullRowTabWidth
Dim _fullRowRemainder As Integer = _
CInt(_widthValue - _fullRowTabWidth.Value * _tabsInFullRow)
Dim _partRowRemainder As Integer = _
CInt(_widthValue - _partRowTabWidth.Value * _tabsInPartRow)
_tabPageHeight = Unit.Pixel(CInt(Height.Value - _
(_tabRows * TabHeight.Value)))
ReDim _rowArray(_tabRows - 1)
For i As Integer = 0 To _tabRows - 1
If _fullRows < _tabRows And i = 0 Then
ReDim _rowArray(i)(_tabsInPartRow - 1)
For j As Integer = 0 To _tabsInPartRow - 1
If j = 0 Then
_rowArray(i)(j) = New Pair(-5, _
_rowArray(i)(j) = New Pair(-4, _partRowTabWidth)
ReDim _rowArray(i)(_tabsInFullRow - 1)
For j As Integer = 0 To _tabsInFullRow - 1
If j = 0 Then
_rowArray(i)(j) = New Pair(-3, _
Unit.Pixel(CInt(_fullRowTabWidth.Value + _
_rowArray(i)(j) = New Pair(-2, _fullRowTabWidth)
Dim _tabCollectionCounter As Integer = 0
Dim _tabCounter As Integer = 0
Dim _rowCounter As Integer = _tabRows - 1
Do While _tabCollectionCounter < _allTabCount
If HttpContext.Current Is Nothing Or _
_rowArray(_rowCounter)(_tabCounter).First = _tabCollectionCounter
If _tabCollectionCounter = SelectedIndex Then
_selectedRow = _rowCounter
_tabCounter += 1
If _tabCounter = _tabsInFullRow Then
_rowCounter -= 1
_tabCounter = 0
_tabCollectionCounter += 1
I wanted the developer to be able to choose whether or not the
XTabControl would postback when a user selects an
XTab. This meant having a client-side way to move from one tab to another. I embedded a VBScript file in my project, which is written to the page when it loads.
set objRow = obj.parentElement.parentElement
set objTable = objRow.parentElement
set objMasterPage = objTable.rows(objTable.rows.length-1).cells(0).children(0)
TabControlName = left(obj.name, instrrev(obj.name, "tab_") - 2)
helperControlName = "__" & TabControlName & "_State__"
set objHelper = document.getElementById(helperControlName)
tabIndex = mid(obj.name, instrrev(obj.name, "tab_") + 4)
selectedPanelName = TabControlName + "_panel_" + tabIndex
for each i in objMasterPage.children
if i.name = selectedPanelName then
i.style.display = "inline"
i.style.display = "none"
objMasterPage.style.backgroundColor = obj.style.backgroundColor
objTable.moveRow objRow.rowIndex, objTable.rows.length-2
for each myTab in objRow.cells(0).children
if myTab.name = obj.name then
myTab.style.borderBottom = "none"
myTab.style.borderBottom = "3px inset"
if objTable.rows.length > 2 then
for rowIdx = 0 to objTable.rows.length-3
for each myTab in objTable.rows(rowIdx).cells(0).children
myTab.style.borderBottom = "none"
objHelper.value = tabIndex
set objRow = Nothing
set objTable = Nothing
set objMasterPage = Nothing
set objHelper = Nothing
I decided to use inline CSS for my styles, rather than stylesheets. You can't control what other stylesheets may be on a page, and rather than risk conflicting names of styles, I went with the inline method.
During rendering, I used the following code to determine how the control would behave when clicked:
If _autoPostBack Then
"jscript:" + ClientHelperID + ".value=" + _tabIdx.ToString _
+ ";" + Page.GetPostBackEventReference(Me, _tabIdx.ToString()), _
writer.AddAttribute("onfocus", "jscript:" + ClientHelperID + _
".value=" + _tabIdx.ToString + ";" + _
Page.GetPostBackEventReference(Me, _tabIdx.ToString()), False)
HelperID referred to in this snippet is the hidden control on the page which holds the
SelectedIndex value. I used this in
XTable to store the scroll positions, and it's cribbed brazenly from the innards of Microsoft's non-supported
TabStrip control, as is a lot of the mechanics surrounding the
SelectedIndex property in general.
One of the more annoying things about VS.NET is that contained child controls can be overlooked. If you add a
TextBox to a
TableCell, for instance, you can no longer edit the
TextBox properties in design view. You can see the
TextBox, but clicking on it only selects the
Table it's in. Similarly, if you add a
TextBox (or any other control, for that matter) to a
TableCell in HTML view, VS.NET will not automatically add a
Protected WithEvents declaration for the
TextBox in the "Web Form Designer Generated Code" region of the code-behind.
This irksome behavior, of course, extends to
XTabControl. When Microsoft fixes this on their end, I hope that it will automatically fix the problem for
XTabControl. Meanwhile, it's nothing you're not probably used to.
I considered trying out the
ReadWriteControlDesigner class, but decided that I wasn't feeling frisky enough. And the way the control is built, using that for my designer might allow users to fiddle with the composition of the control, so I'm wary of trying this. But if anyone out there wants to give it a whirl, I'd be happy to hear what the results were. If they're good, I might do the same for