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

XTable 扩展表格服务器控件

starIconstarIconstarIconstarIconemptyStarIcon

4.00/5 (2投票s)

2005年8月2日

5分钟阅读

viewsIcon

56787

downloadIcon

857

一个支持 THead、TBody 和 TFoot 标签,以及冻结顶部和左侧表头的 Table 控件。

Sample Image - xtable.gif

引言

不久前,我遇到一个情况,希望在打印时表格的表头能在每页顶部重复显示。为此,我需要对表格的 <thead> 元素使用 CSS,但由于我使用的是 ASP.NET,我无法做到这一点,因为微软的 Table 服务器控件不支持 <thead><tbody><tfoot> 标签。

当我发现 Table 控件的某些部分(RowCollectionCellCollection 类)不可继承时,我决定放弃。但最近,我找到了一个绕过这个问题的方法,于是我决定构建这个控件。

我还偶然发现了一种在表格中冻结顶部和左侧表头的巧妙方法,我觉得将这两种“升级”功能都整合到标准的 Table 控件中会很不错。

由于我工作在仅限 IE 的环境中,因此我并不担心跨浏览器兼容性问题。不过,碰巧的是,这次修改所增加的额外标签没有使用 CSS 和脚本,所以它完全是跨浏览器兼容的。

添加 <thead>、<tbody> 和 <tfoot> 标签

我的第一步是使用 Lutz Roeder 的 Reflector 来获取标准微软 Table 服务器控件的开放代码副本。我已将此代码作为 OpenTable 包含在源代码中。

<thead><tbody><tfoot> 标签的共同点是它们都是表格行(<tr>)的分组。因此,我扩展了微软的代码,在 TableTableRow 层之间增加了一个 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

TableRows 属性是这样的

<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

所以现在 XTableRowGroups 属性是这样的

<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 重写。一旦理解了该方法的逻辑,这里也变得很简单,但添加额外的层次确实增加了一些复杂性。我不会在这里详细介绍整个方法,但如果你比较 OpenTableXTable 中的代码,应该会相当清楚。

另一个地方是 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 也是如此,不过如果只有一个 RowGroupEnableTHeadEnableTFoot 都设置为 True,该 RowGroup 会被渲染为 <thead>。而 EnableTBodies 属性(默认为 True)决定是否为 RowGroup 渲染任何标签。如果将其设置为 False,则不会渲染任何 <tbody> 标签。

这是 RenderStartTagRenderEndTag 的代码

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。

首先,我创建了五个新属性

  • OuterHeightOuterWidth 是包裹我们表格的 <div> 的尺寸。
  • FreezeTop 是一个 Boolean 值,用于确定第一个 RowGroup 是否要被冻结。
  • FreezeLeft 是一个 Integer 值,用于确定左侧要冻结多少列。
  • FreezeStyle 继承自 TableItemStyle,用于确定冻结区域中任何单元格的样式。

原始的 Table 控件在 TableCell 中重写了 AddAttributesToRender 方法来添加 ColspanRowspan 属性。我更进一步

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 写入脚本。它根据从隐藏控件回发的信息来设置 scrollTopscrollLeft 的值。现在,滚动位置在回发过程中得以保留。当然,只有在 OuterHeightOuterWidth 有值时才会调用此方法。如果这两个属性都为空,则根本不会渲染包含的 <div>,因为它无关紧要。

始终在启用 OptionExplicit 和 OptionStrict "ON" 的情况下进行编译

我通过惨痛的教训了解到,如果你在编译时没有同时启用 OptionExplicitOptionStrict,而使用该控件的项目却将这些选项设置为 ON,那么控件很可能会崩溃。我发布的示例代码是在这两个选项都为 ON 的情况下编译的。

已知问题

由于 <select> 标签是一个窗口控件,它最后才被渲染。这意味着它的 z-index 总是会大于你分配给表格单元格的 z-index。也就是说,当你滚动时,下拉框和列表框会从表头的上方经过,而不是下方。

我见过的唯一解决这个问题的方法是

  1. 不要在这些表格中使用 <select>
  2. 使用一些脚本,当 <select> 侵入冻结单元格时,使其不可见,或者
  3. 接受这个小小的图形显示问题。

就我个人而言,我选择 (c)。

© . All rights reserved.