XTabControl 服务器控件






4.56/5 (9投票s)
2005年8月3日
7分钟阅读

94362

1900
一个自定义服务器控件,类似于 VB6 的 TabControl。
引言
在我还在做纯粹的 ASP(啊,那些日子)的时候,我曾捣鼓过创建一个 DHTML 标签控件,就像我在 VB6 中用过的类型。多行标签,当你点击它们时会自动重新排列,以及类似的功能。
当我开始用 ASP.NET 编程时,我发现事情并没有那么灵活。我想要标签控件的便利性,但我能找到的只有微软那些被弃用的 TabStrip
和 MultiPage
控件,它们必须安装在服务器上,使用 .htc 文件,需要单独处理,而且确实不怎么友好。
我喜欢友好的控件。
我在网上到处找,但什么都没有找到与我所寻找的相似之处,所以我决定自己做一个。
与 XTable
控件一样,这个控件可能不具备跨浏览器兼容性。不过,我实际上并没有在其他浏览器上试过,所以也许会有惊喜。
失败的尝试
XTabControl
是我构建的第三个标签控件。前两个是可怕的、笨拙的东西。我学到的有趣的事情之一与集合有关。
微软经常建议人们让他们的集合继承自 CollectionBase
。如果你希望集合的元素能够包含其他控件,那就不要这样做。我第一次就是这么做的,虽然每个标签上的控件都显示得很好,但它们不在控件层次结构中,并且在回发时被忽略了。我尝试使用比喻性的绳子和胶带来强制控件保持其状态并传递数据,并且我获得了微小的成功。但我无法,无论如何,让它们在服务器上引发事件。
最后,我决定利用我已知能工作的代码。
这才是更像样的
在我制作 XTable
控件时,我曾使用 Lutz Roeder 的 Reflector 来获取标准微软 Table
服务器控件的代码副本。我在源代码中包含了这个,命名为 OpenTable
。
我认为,放入 TableCell
的控件不会有我遇到的问题,所以我可以将该代码作为我的标签控件的基础。当然,Table
控件有一个三级层次结构(Table
-> TableRow
-> TableCell
),而我只需要两级(XTabControl
,XTab
),所以我基于 TableRow
类构建了我的控件。
其架构与我以前的尝试完全不同。TableCellCollection
类,后来成为我的 XTabCollection
,被定义为一个独立的类,但 TableRow
类,后来成为我的 XTable
类,还包含了一个嵌套的 CellControlCollection
,后来成为我的 XTabControlCollection
。术语有点令人困惑,因为它听起来像是一个 XTabControl
的集合,但我想既然它嵌套在 XTabControl
中,应该足够清楚了。
XTabCollection
没有继承任何东西,但实现了 IList
、ICollection
和 IEnumerable
。XTabControl.XTabControlCollection
继承自 ControlCollection
,这确保了每个 XTab
以及每个 XTab
的所有子控件都将成为控件层次结构的一部分。
设计和属性
VB6 TabControl
中真正让我恼火的一件事是无法更改标签的背景颜色。我决心不在我的控件上犯同样的错误。
一些属性是显而易见的,比如 Tabs
、SelectedIndex
和 SelectedTab
。另一些则不那么明显。我决定的不那么明显的属性是
XTabControl.TabsPerRow
XTabControl.TabHeight
XTabControl.TabFontFamily
XTabControl.TabFontSize
XTab.BackColor
XTab.ForeColor
XTab.InnerWidth
XTab.InnerHeight
TabsPerRow
属性是我从 VB6 TabControl
继承下来的。TabHeight
将决定所有 XTab
的高度。这必须是全局的,因为让 XTab
的高度不同,如果不是不可能的话,肯定会不必要地复杂化。我可能可以在单个 XTab
上设置 TabFontFamily
和 TabFontSize
属性,但我认为那样会很难看。
除了允许用户选择每个标签的颜色 ForeColor
和 BackColor
外,我还决定 XTab
上的可用空间不应受 XTabControl
尺寸的限制。通过在每个 XTab
上创建 InnerWidth
和 InnerHeight
属性,可以利用比可用空间多或少的空间。演示项目展示了这一点,文章开头的 GIF 也是如此。
当然,最复杂的问题是如何调整标签的大小并将其拟合到正确的默认位置,以及允许它们随着用户的点击而移动。在标准的标签控件中,如果你点击后排的标签,整行会下降到标签页正上方的那一行,我需要模拟这种行为。
对于标签的定位和尺寸,我编写了一个方法,该方法计算完全行(包含允许数量标签的行)中每个标签所需的宽度,以及后排的非完全行(如果有的话)中每个标签所需的宽度。
例如,如果 TabsPerRow
设置为 3,并且控件中有 5 个 XTab
,则将有一行 3 个 XTab
和一行 2 个 XTab
。这相当占用数学计算。然后我创建了一个 Pair
的交错数组来保存对正确 XTab
的引用。对于每个 Pair
,First
将保存 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
是页面上保存 XTabControl
的 SelectedIndex
值的隐藏控件。我在 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
做同样的事情。