Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web / HTML

The XTabControl Server Control

4.56/5 (9 votes)
3 Aug 20056 min read 1   1.9K  
A custom server control similar to the VB6 TabControl.

Sample Image - xtabcontrol.gif

Introduction

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 TabStrip and 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.

Like the 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.

False Starts

The 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 OpenTable.

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 (Table -> TableRow -> TableCell), and I only needed two levels (XTabControl, XTab), so I based my control on the TableRow class.

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 IList, ICollection and IEnumerable. 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 Tabs and SelectedIndex and SelectedTab. Others were less so. The non-obvious properties I decided on were:

  • XTabControl.TabsPerRow
  • XTabControl.TabHeight
  • XTabControl.TabFontFamily
  • XTabControl.TabFontSize
  • XTab.BackColor
  • XTab.ForeColor
  • XTab.InnerWidth
  • XTab.InnerHeight

The TabsPerRow property was something I remembered from the VB6 TabControl. 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 TabFontFamily and 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 ForeColor and BackColor, I decided that the space available on the XTab shouldn't be limited by the dimensions of the XTabControl. By creating InnerWidth and 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 Pair, 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.

VB
''' <summary>
''' A series of calculations used to determine the width and position of
''' each Tab in the TabControl. Unless you enjoy math, don't worry about it.
''' </summary>
''' <returns>A jagged array that represents the tabs in their rows.</returns>
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 this is runtime, don't show Tabs that aren't visible
   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
         End If
      Next _tab
      _showTabCount = _visibleTabCount
   End If
   'get the number of tabs in full rows and their widths
   Dim _tabsInFullRow As Integer = TabsPerRow
   Dim _fullRowTabWidth As Unit = _
       Unit.Pixel(CInt(Math.Floor(_widthValue / _
       CDbl(_tabsInFullRow))))
   'get the number of rows, total
   Dim _tabRows As Integer = _
       CInt(Math.Ceiling(CDbl(_showTabCount) / _
       CDbl(_tabsInFullRow)))
   'get the number of tabs in a partial row (a row with 
   'fewer than TabsPerRow tabs)
   If _tabRows * _tabsInFullRow = _showTabCount Then
      _fullRows = _tabRows
   Else
      _fullRows = _tabRows - 1
   End If
   Dim _tabsInPartRow As Integer = _showTabCount - _
                         (_fullRows * _tabsInFullRow)
   'get the widths of tabs in a partial row
   If _tabsInPartRow > 0 Then
      _partRowTabWidth = _
         Unit.Pixel(CInt(Math.Floor(_widthValue / _
         CDbl(_tabsInPartRow))))
   Else
      _partRowTabWidth = _fullRowTabWidth
   End If
   'but just in case they don't divide roundly, we need the remainders
   Dim _fullRowRemainder As Integer = _
      CInt(_widthValue - _fullRowTabWidth.Value * _tabsInFullRow)
   Dim _partRowRemainder As Integer = _
      CInt(_widthValue - _partRowTabWidth.Value * _tabsInPartRow)
   'figure out the height of the masterPage
   _tabPageHeight = Unit.Pixel(CInt(Height.Value - _
                       (_tabRows * TabHeight.Value)))
   'let's make a jagged array that represents the tabs in their rows
   'we'll put the tab widths in Pair.Second
   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
               'add in the remainder
               _rowArray(i)(j) = New Pair(-5, _
                 Unit.Pixel(CInt(_partRowTabWidth.Value _
                 + _partRowRemainder)))
            Else
               _rowArray(i)(j) = New Pair(-4, _partRowTabWidth)
            End If
         Next j
      Else
         ReDim _rowArray(i)(_tabsInFullRow - 1)
         For j As Integer = 0 To _tabsInFullRow - 1
            If j = 0 Then
               'add in the remainder
               _rowArray(i)(j) = New Pair(-3, _
                  Unit.Pixel(CInt(_fullRowTabWidth.Value + _
                  _fullRowRemainder)))
            Else
               _rowArray(i)(j) = New Pair(-2, _fullRowTabWidth)
            End If
         Next j
      End If
   Next i
   'now let's fill that array with tab indices (that goes into Pair.First)
   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 _
                     Tabs(_tabCollectionCounter).Visible Then
         _rowArray(_rowCounter)(_tabCounter).First = _tabCollectionCounter
         If _tabCollectionCounter = SelectedIndex Then
            _selectedRow = _rowCounter
         End If
         _tabCounter += 1
         If _tabCounter = _tabsInFullRow Then
            _rowCounter -= 1
            _tabCounter = 0
         End If
      End If
      _tabCollectionCounter += 1
   Loop
End Sub 'GetTabRows

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.

VBScript
function XTabControl_SelectTab(obj)
   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)
   'select the panel
   selectedPanelName = TabControlName + "_panel_" + tabIndex
   for each i in objMasterPage.children
      if i.name = selectedPanelName then
         i.style.display = "inline"
      else
         i.style.display = "none"
      end if
   next
   '//set the masterPanel color
   objMasterPage.style.backgroundColor = obj.style.backgroundColor
   'format the tabs
  
   'move the row that contains the selected tab down to just above the masterPanel
   objTable.moveRow objRow.rowIndex, objTable.rows.length-2
   'make the selected tab "tabon" and the others in its row "taboff"
   for each myTab in objRow.cells(0).children
      if myTab.name = obj.name then
         myTab.style.borderBottom = "none"
      else
         myTab.style.borderBottom = "3px inset"
      end if
   next
  
   'make all the tabs that aren't in the selected tab's row "tabon"
   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"
         next
      next
   end if
  
   'set the helper field to the selectedindex
   objHelper.value = tabIndex
   set objRow = Nothing
   set objTable = Nothing
   set objMasterPage = Nothing
   set objHelper = Nothing
end function

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:

VB
'set the onclick depending on whether AutoPostBack is true or not
If _autoPostBack Then
   writer.AddAttribute(HtmlTextWriterAttribute.Onclick, _
       "jscript:" + ClientHelperID + ".value=" + _tabIdx.ToString _
       + ";" + Page.GetPostBackEventReference(Me, _tabIdx.ToString()), _
       False)
   writer.AddAttribute("onfocus", "jscript:" + ClientHelperID + _
        ".value=" + _tabIdx.ToString + ";" + _
        Page.GetPostBackEventReference(Me, _tabIdx.ToString()), False)
Else
   writer.AddAttribute(HtmlTextWriterAttribute.Onclick, _
          "vbscript:XTabControl_SelectTab(me)", False)
   writer.AddAttribute("onfocus", _
          "vbscript:XTabControl_SelectTab(me)", False)
End If

The HelperID referred to in this snippet is the hidden control on the page which holds the XTabControl's 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.

Known Issues

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 XTable.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here