65.9K
CodeProject 正在变化。 阅读更多。
Home

XTabControl 服务器控件

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.56/5 (9投票s)

2005年8月3日

7分钟阅读

viewsIcon

94362

downloadIcon

1900

一个自定义服务器控件,类似于 VB6 的 TabControl。

Sample Image - xtabcontrol.gif

引言

在我还在做纯粹的 ASP(啊,那些日子)的时候,我曾捣鼓过创建一个 DHTML 标签控件,就像我在 VB6 中用过的类型。多行标签,当你点击它们时会自动重新排列,以及类似的功能。

当我开始用 ASP.NET 编程时,我发现事情并没有那么灵活。我想要标签控件的便利性,但我能找到的只有微软那些被弃用的 TabStripMultiPage 控件,它们必须安装在服务器上,使用 .htc 文件,需要单独处理,而且确实不怎么友好。

我喜欢友好的控件。

我在网上到处找,但什么都没有找到与我所寻找的相似之处,所以我决定自己做一个。

XTable 控件一样,这个控件可能不具备跨浏览器兼容性。不过,我实际上并没有在其他浏览器上试过,所以也许会有惊喜。

失败的尝试

XTabControl 是我构建的第三个标签控件。前两个是可怕的、笨拙的东西。我学到的有趣的事情之一与集合有关。

微软经常建议人们让他们的集合继承自 CollectionBase。如果你希望集合的元素能够包含其他控件,那就不要这样做。我第一次就是这么做的,虽然每个标签上的控件都显示得很好,但它们不在控件层次结构中,并且在回发时被忽略了。我尝试使用比喻性的绳子和胶带来强制控件保持其状态并传递数据,并且我获得了微小的成功。但我无法,无论如何,让它们在服务器上引发事件。

最后,我决定利用我已知能工作的代码。

这才是更像样的

在我制作 XTable 控件时,我曾使用 Lutz Roeder 的 Reflector 来获取标准微软 Table 服务器控件的代码副本。我在源代码中包含了这个,命名为 OpenTable

我认为,放入 TableCell 的控件不会有我遇到的问题,所以我可以将该代码作为我的标签控件的基础。当然,Table 控件有一个三级层次结构(Table -> TableRow -> TableCell),而我只需要两级(XTabControlXTab),所以我基于 TableRow 类构建了我的控件。

其架构与我以前的尝试完全不同。TableCellCollection 类,后来成为我的 XTabCollection,被定义为一个独立的类,但 TableRow 类,后来成为我的 XTable 类,还包含了一个嵌套的 CellControlCollection,后来成为我的 XTabControlCollection。术语有点令人困惑,因为它听起来像是一个 XTabControl 的集合,但我想既然它嵌套在 XTabControl 中,应该足够清楚了。

XTabCollection 没有继承任何东西,但实现了 IListICollectionIEnumerableXTabControl.XTabControlCollection 继承自 ControlCollection,这确保了每个 XTab 以及每个 XTab 的所有子控件都将成为控件层次结构的一部分。

设计和属性

VB6 TabControl 中真正让我恼火的一件事是无法更改标签的背景颜色。我决心不在我的控件上犯同样的错误。

一些属性是显而易见的,比如 TabsSelectedIndexSelectedTab。另一些则不那么明显。我决定的不那么明显的属性是

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

TabsPerRow 属性是我从 VB6 TabControl 继承下来的。TabHeight 将决定所有 XTab 的高度。这必须是全局的,因为让 XTab 的高度不同,如果不是不可能的话,肯定会不必要地复杂化。我可能可以在单个 XTab 上设置 TabFontFamilyTabFontSize 属性,但我认为那样会很难看。

除了允许用户选择每个标签的颜色 ForeColorBackColor 外,我还决定 XTab 上的可用空间不应受 XTabControl 尺寸的限制。通过在每个 XTab 上创建 InnerWidthInnerHeight 属性,可以利用比可用空间多或少的空间。演示项目展示了这一点,文章开头的 GIF 也是如此。

当然,最复杂的问题是如何调整标签的大小并将其拟合到正确的默认位置,以及允许它们随着用户的点击而移动。在标准的标签控件中,如果你点击后排的标签,整行会下降到标签页正上方的那一行,我需要模拟这种行为。

对于标签的定位和尺寸,我编写了一个方法,该方法计算完全行(包含允许数量标签的行)中每个标签所需的宽度,以及后排的非完全行(如果有的话)中每个标签所需的宽度。

例如,如果 TabsPerRow 设置为 3,并且控件中有 5 个 XTab,则将有一行 3 个 XTab 和一行 2 个 XTab。这相当占用数学计算。然后我创建了一个 Pair 的交错数组来保存对正确 XTab 的引用。对于每个 PairFirst 将保存 XTab.Index 值,Second 将保存该 XTab 的宽度。在渲染过程中,我只需从这个数组中读取。

''' <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

我希望开发者可以选择 XTabControl 在用户选择 XTab 时是否进行回发。这意味着需要有一种客户端方法来从一个标签移动到另一个标签。我在项目中嵌入了一个 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

我决定使用内联 CSS 来设置样式,而不是样式表。你无法控制页面上可能存在的其他样式表,为了避免样式名称冲突,我选择了内联方式。

在渲染过程中,我使用了以下代码来确定控件在被点击时将如何表现

'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

此代码片段中提到的 HelperID 是页面上保存 XTabControlSelectedIndex 值的隐藏控件。我在 XTable 中使用它来存储滚动位置,并且它是直接从微软不受支持的 TabStrip 控件内部代码借鉴来的,就像 SelectedIndex 属性周围的许多机制一样。

已知问题

VS.NET 更令人恼火的一点是,它有时会忽略包含的子控件。例如,如果你将一个 TextBox 添加到 TableCell 中,你就无法在设计视图中编辑 TextBox 的属性了。你可以看到 TextBox,但点击它只会选中它所在的 Table。同样,如果你在 HTML 视图中将一个 TextBox(或任何其他控件)添加到 TableCell 中,VS.NET 不会自动在代码隐藏文件的“Web Form Designer Generated Code”区域添加一个 Protected WithEvents 声明。

这种令人烦恼的行为,当然也适用于 XTabControl。当微软在他们那边修复这个问题时,我希望它也能自动为 XTabControl 修复这个问题。在此期间,这并不是什么你可能不习惯的事情。

我考虑过尝试使用 ReadWriteControlDesigner 类,但决定我没有足够的热情。而且由于控件的构建方式,将其用于我的设计器可能会允许用户随意更改控件的组成,所以我对此持谨慎态度。但如果有人想尝试一下,我很乐意听听结果。如果结果好,我可能会为 XTable 做同样的事情。

© . All rights reserved.