XTable 扩展表格服务器控件





4.00/5 (2投票s)
2005年8月2日
5分钟阅读

56787

857
一个支持 THead、TBody 和 TFoot 标签,以及冻结顶部和左侧表头的 Table 控件。
引言
不久前,我遇到一个情况,希望在打印时表格的表头能在每页顶部重复显示。为此,我需要对表格的 <thead>
元素使用 CSS,但由于我使用的是 ASP.NET,我无法做到这一点,因为微软的 Table
服务器控件不支持 <thead>
、<tbody>
和 <tfoot>
标签。
当我发现 Table
控件的某些部分(RowCollection
和 CellCollection
类)不可继承时,我决定放弃。但最近,我找到了一个绕过这个问题的方法,于是我决定构建这个控件。
我还偶然发现了一种在表格中冻结顶部和左侧表头的巧妙方法,我觉得将这两种“升级”功能都整合到标准的 Table
控件中会很不错。
由于我工作在仅限 IE 的环境中,因此我并不担心跨浏览器兼容性问题。不过,碰巧的是,这次修改所增加的额外标签没有使用 CSS 和脚本,所以它完全是跨浏览器兼容的。
添加 <thead>、<tbody> 和 <tfoot> 标签
我的第一步是使用 Lutz Roeder 的 Reflector 来获取标准微软 Table
服务器控件的开放代码副本。我已将此代码作为 OpenTable
包含在源代码中。
<thead>
、<tbody>
和 <tfoot>
标签的共同点是它们都是表格行(<tr>
)的分组。因此,我扩展了微软的代码,在 Table
和 TableRow
层之间增加了一个 RowGroup
层。
例如,Table
类按如下方式重写了 CreateControlCollection
Protected Overrides Function CreateControlCollection() As ControlCollection
Return New RowControlCollection(Me)
End Function 'CreateControlCollection
所以 XTable
类这样做
Protected Overrides Function CreateControlCollection() As ControlCollection
Return New RowGroupControlCollection(Me)
End Function 'CreateControlCollection
Table
的 Rows
属性是这样的
<Description("Table_Rows"), _
PersistenceMode(PersistenceMode.InnerDefaultProperty), _
MergableProperty(False)> _
Public Overridable ReadOnly Property Rows() As TableRowCollection
Get
If Me._rows Is Nothing Then
Me._rows = New TableRowCollection(Me)
End If
Return Me._rows
End Get
End Property 'Rows
所以现在 XTable
的 RowGroups
属性是这样的
<Description("XTable_RowGroups"), _
PersistenceMode(PersistenceMode.InnerDefaultProperty), _
MergableProperty(False)> _
Public Overridable ReadOnly Property RowGroups() As XTableRowGroupCollection
Get
If Me._rowGroups Is Nothing Then
Me._rowGroups = New XTableRowGroupCollection(Me)
End If
Return Me._rowGroups
End Get
End Property 'RowGroups
这个改动相当简单。只有两个地方需要一些复杂的操作。第一个是 Designer
中的 GetDesignTimeHtml
重写。一旦理解了该方法的逻辑,这里也变得很简单,但添加额外的层次确实增加了一些复杂性。我不会在这里详细介绍整个方法,但如果你比较 OpenTable
和 XTable
中的代码,应该会相当清楚。
另一个地方是 RowGroups
类的构造函数。Rows
类的构造函数是这样的
Public Sub New()
MyBase.New(HtmlTextWriterTag.Tr)
End Sub 'New
然而,在 XTable
的情况下,RowGroups
可以被渲染为 <thead>
、<tbody>
或 <tfoot>
标签。所以我将其改为使用 HtmlTextWriterTag.Tbody
作为默认值,并让渲染方法来决定实际渲染哪个标签。
为此,我向 XTable
添加了一些属性。如果 EnableTHead
设置为 True
,则会导致第一个 RowGroup
渲染为 <thead>
Public Property EnableTHead() As Boolean
Get
Return CType(Me.ViewState("EnableTHead"), Boolean)
End Get
Set(ByVal Value As Boolean)
If Not Value Then
Me.FreezeTop = False
End If
Me.ViewState("EnableTHead") = Value
End Set
End Property 'EnableTHead
EnableTFoot
属性对最后一个 RowGroup
也是如此,不过如果只有一个 RowGroup
且 EnableTHead
和 EnableTFoot
都设置为 True
,该 RowGroup
会被渲染为 <thead>
。而 EnableTBodies
属性(默认为 True
)决定是否为 RowGroup
渲染任何标签。如果将其设置为 False
,则不会渲染任何 <tbody>
标签。
这是 RenderStartTag
和 RenderEndTag
的代码
Public Overrides Sub RenderBeginTag(ByVal writer As HtmlTextWriter)
Me.AddAttributesToRender(writer)
Dim _tag As HtmlTextWriterTag
Dim _table As XTable = CType(Parent, XTable)
If _table.EnableTHead And Me Is _table.RowGroups(0) Then
writer.RenderBeginTag(HtmlTextWriterTag.Thead)
ElseIf _table.EnableTFoot And Not _table.EnableTHead _
And Me Is _table.RowGroups(_table.RowGroups.Count - 1) Then
writer.RenderBeginTag(HtmlTextWriterTag.Tfoot)
ElseIf _table.EnableTFoot And Not _table.RowGroups.Count < 2 _
And Me Is _table.RowGroups(_table.RowGroups.Count - 1) Then
writer.RenderBeginTag(HtmlTextWriterTag.Tfoot)
ElseIf _table.EnableTBodies Then
writer.RenderBeginTag(HtmlTextWriterTag.Tbody)
End If
End Sub 'RenderBeginTag
Public Overrides Sub RenderEndTag(ByVal writer As HtmlTextWriter)
Dim _table As XTable = CType(Parent, XTable)
If _table.EnableTHead Or _table.EnableTBodies Or _table.EnableTFoot Then
writer.RenderEndTag()
End If
End Sub 'RenderEndTag
用伪代码表示,它看起来像这样
If <thead>s are enabled AND this rowgroup is the first one:
Render as <thead>
Otherwise if <tfoot>s are enabled AND <thead>s
are not, AND this rowgroup is the last one:
Render as <tfoot>
Otherwise if <tfoot>s are enabled AND there
are at least 2 rowgroups, AND this is the last one:
Render as <tfoot>
Otherwise if <tbodies> are enabled:
Render as <tbody>
只有当至少一种 RowGroup
标签类型被启用时,才会为 RowGroup
渲染结束标签。
我本可以到此为止,这样我就有了一个支持这三个额外标签的可用 Table
服务器控件。它也会完全跨浏览器兼容,因为如我之前提到的,没有涉及 CSS 和脚本。但我还想实现冻结表头的功能。
冻结表头
我曾偶然看到 Brett Merkey 的一个绝佳示例,该示例展示了如何使用 CSS 表达式在 HTML 表格中冻结表头。他的示例使用了样式表,但我对使用样式表感到不安,因为我无法控制用户可能使用的其他样式表,这些样式表可能会与控件的样式表冲突。我更喜欢使用内联 CSS。
首先,我创建了五个新属性
OuterHeight
和OuterWidth
是包裹我们表格的<div>
的尺寸。FreezeTop
是一个Boolean
值,用于确定第一个RowGroup
是否要被冻结。FreezeLeft
是一个Integer
值,用于确定左侧要冻结多少列。FreezeStyle
继承自TableItemStyle
,用于确定冻结区域中任何单元格的样式。
原始的 Table
控件在 TableCell
中重写了 AddAttributesToRender
方法来添加 Colspan
和 Rowspan
属性。我更进一步
Protected Overrides Sub AddAttributesToRender(ByVal writer As HtmlTextWriter)
'*********
'Original Microsoft code
'*********
MyBase.AddAttributesToRender(writer)
Dim i As Integer = Me.ColumnSpan
If i > 0 Then
writer.AddAttribute(HtmlTextWriterAttribute.Colspan, _
i.ToString(NumberFormatInfo.InvariantInfo))
End If
i = Me.RowSpan
If i > 0 Then
writer.AddAttribute(HtmlTextWriterAttribute.Rowspan, _
i.ToString(NumberFormatInfo.InvariantInfo))
End If
'*********
My added code
'*********
Dim _row As XTableRow = CType(Parent, XTableRow)
Dim _rowgroup As XTableRowGroup = CType(Parent.Parent, XTableRowGroup)
Dim _table As XTable = CType(Parent.Parent.Parent, XTable)
Dim isFrozenTop As Boolean = (_table.FreezeTop AndAlso _
_table.EnableTHead AndAlso _rowgroup Is _table.RowGroups(0))
Dim isFrozenLeft As Boolean = (_table.FreezeLeft > 0 _
AndAlso _row.Controls.IndexOf(Me) < _table.FreezeLeft)
If isFrozenTop And isFrozenLeft Then
writer.AddStyleAttribute("z-index", "30")
writer.AddStyleAttribute("top", _
"expression(parentNode.parentNode.parentNode.parentNode.scrollTop-2)")
writer.AddStyleAttribute("left", _
"expression(parentNode.parentNode.parentNode.parentNode.scrollLeft-2)")
ElseIf isFrozenTop Then
writer.AddStyleAttribute("z-index", "20")
writer.AddStyleAttribute("top", _
"expression(parentNode.parentNode.parentNode.parentNode.scrollTop-2)")
ElseIf isFrozenLeft Then
writer.AddStyleAttribute("z-index", "10")
writer.AddStyleAttribute("left", _
"expression(parentNode.parentNode.parentNode.parentNode.scrollLeft-2)")
End If
If isFrozenTop Or isFrozenLeft Then
writer.AddStyleAttribute("position", "relative")
_table.FreezeStyle.AddAttributesToRender(writer)
End If
End Sub 'AddAttributesToRender
在回发时保留滚动位置
如果每次页面回发时,表格都滚动回其原始位置,那么使用这个新控件将会非常烦人。因此,我借鉴了微软现已不再支持的 TabStrip
控件中使用的功能,并为己所用。基本上,我在页面上创建了一个隐藏控件,并向 <div>
标签添加了一个 onscroll
事件,该事件将滚动位置写入隐藏控件。一个典型的值可能是 34:50,表示垂直滚动偏移了 34 像素,水平滚动偏移了 50 像素。
Private Sub RegisterScript()
Dim divID As String = Me.ID + "_div"
Dim _scrollPos() As String = Me.HelperData.Split(":"c)
Dim script As String = "<script language='javascript' type='text/javascript' >" _
+ ControlChars.CrLf _
+ "<!--" _
+ ControlChars.CrLf _
+ divID + ".scrollTop = " + _scrollPos(0) + ";" _
+ ControlChars.CrLf _
+ divID + ".scrollLeft = " + _scrollPos(1) + ";" _
+ ControlChars.CrLf _
+ "//-->" _
+ ControlChars.CrLf _
+ "</script>"
Page.RegisterStartupScript(Me.ID, script)
script = Nothing
End Sub 'RegisterScript
此方法会在页面的 </form>
标签之前为页面上的每个 XTable
写入脚本。它根据从隐藏控件回发的信息来设置 scrollTop
和 scrollLeft
的值。现在,滚动位置在回发过程中得以保留。当然,只有在 OuterHeight
或 OuterWidth
有值时才会调用此方法。如果这两个属性都为空,则根本不会渲染包含的 <div>
,因为它无关紧要。
始终在启用 OptionExplicit 和 OptionStrict "ON" 的情况下进行编译
我通过惨痛的教训了解到,如果你在编译时没有同时启用 OptionExplicit
和 OptionStrict
,而使用该控件的项目却将这些选项设置为 ON,那么控件很可能会崩溃。我发布的示例代码是在这两个选项都为 ON 的情况下编译的。
已知问题
由于 <select>
标签是一个窗口控件,它最后才被渲染。这意味着它的 z-index 总是会大于你分配给表格单元格的 z-index。也就是说,当你滚动时,下拉框和列表框会从表头的上方经过,而不是下方。
我见过的唯一解决这个问题的方法是
- 不要在这些表格中使用
<select>
, - 使用一些脚本,当
<select>
侵入冻结单元格时,使其不可见,或者 - 接受这个小小的图形显示问题。
就我个人而言,我选择 (c)。