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

XList 服务器控件

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.74/5 (15投票s)

2005年8月24日

11分钟阅读

viewsIcon

128855

downloadIcon

1833

一个列表控件,结合了 ListBox 和 DropDownList,支持 <optgroup> 标签,并允许(仅限 IE)在 ListBox 模式下水平滚动。

Sample Image - xlist.gif

引言

从我开始接触 HTML 开始,我就一直对 <select> 元素缺乏水平滚动条感到恼火。所以我尝试将 <select> 放到一个 <div> 里,然后花了一些时间调整 JavaScript,就得到了一个解决方案。

第一个浮现出来的问题是,如果你用方向键向下选择列表项,你可能会滚出 <div> 的底部。没有任何东西能让选中的项保持在 <div> 的可见区域内。我也在 JavaScript 中解决了这个问题。

然后就是转向 ASP.NET。我希望将我的解决方案封装成一个服务器控件,但我想,既然我要这么做,我不如顺便修正 Microsoft 的 ListBox 服务器控件中三个更令人恼火的方面:

  • ListBoxDropDownList,尽管在几乎所有方面都是同一个控件,但它们是独立的服务器控件。
  • ListBox(以及 DropDownList)不支持 <optgroup> 标签,而这是 <select> 元素对象模型的一部分。
  • ListBox(和 DropDownList)存在一个 Microsoft 称之为“按设计”的 bug。你可以为 ListItem 添加 Attributes,但是
    1. 它们不会渲染,并且
    2. 它们不会保存在 ViewState 中。

    这就引出了一个问题,为什么它们一开始要在 ListItem 类中提供一个 Attributes 属性。在我看来,它们似乎开始实现了,但没有完成。

合并 ListBox 和 DropDownList

Microsoft 的 ListControl 被用作 ListBoxDropDownListCheckBoxListRadioButtonList 的基础。我不得不推测,由于后两个在页面上渲染时没有使用 <select> 作为 HTML 元素,它们就决定单独处理所有控件。因为 ListBoxDropDownList 之间的区别很小。

正如我以前做过的,我使用了 Lutz Roeder 的 Reflector 来获取构成 ListBox 控件的代码。这是必要的,因为 ListItem 不可继承,稍后你会看到我们需要对该类进行一些修改。

ListBox 继承自 ListControlDropDownList 也是如此。我检查了两者之间的区别,并简单地让这些区别依赖于我创建的一个 Enum

Public Enum XListType
   ListBox = 0
   DropDownList = 1
End Enum 'XListType

我在 XList 中基于这个 Enum 创建了一个属性。

<DefaultValue(0), Category("Behavior"), Description("XList_XListType")> _
Public Overridable Property XListType() As XListType
   Get
      Dim obj1 As Object = Me.ViewState("XListType")
      If (Not obj1 Is Nothing) Then
         Return CType(obj1, XListType)
      End If
      Return XListType.ListBox
   End Get
   Set(ByVal value As XListType)
      If ((value < XListType.ListBox) OrElse (value > _
                          XListType.DropDownList)) Then
         Throw New ArgumentOutOfRangeException("value")
      End If
      Me.ViewState("XListType") = value
   End Set
End Property 'XListType

你可以查看我包含在源代码中的 OpenList,了解我在何处根据此属性使代码条件化。例如,在 AddAttributesToRender 重写中,有一段代码与 SelectionMode 属性有关,而该属性仅与 ListBox 相关。在 DropDownList 中你无法进行多选。

If Me.XListType = XListType.ListBox Then
   writer.AddAttribute(HtmlTextWriterAttribute.Size, _
          Me.Rows.ToString(NumberFormatInfo.InvariantInfo))
   If (Me.SelectionMode = ListSelectionMode.Multiple) Then
      writer.AddAttribute(HtmlTextWriterAttribute.Multiple, "multiple")
   End If
End If

正如你所见,我只是将这段代码包装在一个条件语句中,以便仅当 XList 控件处于 ListBox 模式时运行。

还有一些其他的绊脚石。原始代码中的一些引用在其他类中标记为 Friend 的方法。这对于 Microsoft 的控件来说没问题,因为它们总是在同一个程序集中。但我的控件在单独的程序集中,所以我得到了一个错误,告诉我该方法不可访问。

我真的不想重写 Page 控件,这是第一个引起我这个问题的控件。所以我使用了 Reflection 来处理它。

OnPreRender 重写包含这行代码:

Me.Page.RegisterPostBackScript()

我用这个替换了它:

Dim methodInfo As methodInfo = _
  Me.Page.GetType.GetMethod("RegisterPostBackScript", _
  BindingFlags.Instance Or BindingFlags.NonPublic)
If Not (methodInfo Is Nothing) Then
   methodInfo.Invoke(Me.Page, New Object() {})
End If

在处理自定义控件时,你可能会经常遇到这个问题。这是解决办法。

此外,Microsoft 的代码反复使用了 TryCast 方法。我明白这将在 .NET 的下一个版本中提供,但这对我没有帮助。所以我不得不将这些实例转换为我实际上可以使用。的代码。

例如,在 OnDataBinding 重写中,出现了这行代码:

Dim collection1 As ICollection = _
          TryCast(enumerable1, ICollection)

我用这个替换了它:

Dim collection1 As ICollection
If TypeOf enumerable1 Is ICollection Then
   collection1 = CType(enumerable1, ICollection)
Else
   collection1 = Nothing
End If

因为这本质上就是 TryCast 所做的。如果可以,它会转换为你想要的 Type,如果不行,它会返回 Nothing

这就是我合并这两个控件所做的大部分工作。正如我所说,你可以在源代码中找到它,名为 OpenList

添加 <optgroup> 支持

ListBoxDropDownList 是独立控件,这其实不是什么大问题。但对于像我这样懒惰的人来说,为同一个修改而修改两个控件的前景似乎毫无意义。而且,由于 ListBoxDropDownList 都应该支持 <optgroup> 标签,合并它们为我节省了时间和精力。

首先,我不得不决定如何实现。在 XTable 控件中,我添加了对 <thead>、<tbody> 和 <tfoot> 标签的支持,我决定在 TableTableRow 之间添加一个层次结构,我称之为 TableRowGroup。我曾考虑在这里做同样的事情;让 XListItem 成为 OptGroup 类的子项,并在 XList 中有一个 OptGroupCollection 类。

幸运的是,我在那样做之前恢复了理智,决定仅仅在 XListItem 中添加一个 OptGroup 属性。这将允许控件的数据绑定功能像以前一样继续,而让 XListItem 位于单独的 OptGroup 中会让数据绑定变得一团糟。我可以在 XListRenderContents 方法中处理 <optgroup> 标签。所以我创建了这个 XListItem 中的属性:

<DefaultValue("")> _
Public Property OptGroup() As String
   Get
      Return Me._optGroup
   End Get
   Set(ByVal value As String)
      Me._optGroup = value
      If Me.IsTrackingViewState Then
         Me._misc.Set(4, True)
      End If
   End Set
End Property 'OptGroup

你可能会想这是什么?

Me._misc.Set(4, True)

看起来 Microsoft 的程序员决定在他们的 ListItem 类中放置一个全局的 BitArray 变量。他们在类中放置了 Const 声明,用于标识这个 BitArray 中各个元素所表示的内容。

Private Const _SELECTED As Integer = 0
Private Const _MARKED As Integer = 1
Private Const _TEXTISDIRTY As Integer = 2
Private Const _VALUEISDIRTY As Integer = 3

出于某种原因,他们选择不使用这些常量,而是直接使用它们的数值,但这有助于我了解 _misc 中的项的含义。我添加了以下内容:

Private Const _OPTGROUPISDIRTY As Integer = 4

因此,当通过 Set 访问器修改 OptGroup 属性时,它会将 XListItem 标记为已修改。TextValue 属性已经为 _misc(2)_misc(3) 提供了此功能。

我还为 OptGroup 添加了一些自定义状态管理代码。在 LoadViewStateSaveViewState 中,我将 state 对象替换为 Object(),并让 state(0) 替换了用于 _misc 状态的 state 变量。我使用 state(1) 来存储 OptGroup 属性。

XListRenderContents 重写中,我在渲染每个 <option> 标签之前立即添加了这段代码:

'render optgroups if they're enabled
If Me.EnableOptGroups Then
   Dim sPrevOptGroup As String
   Dim sOptGroup As String = item1.OptGroup
   'if the optgroup has changed, unless it's the first
   'optgroup, end the previous optgroup
   If Not sOptGroup = sPrevOptGroup And Not num2 = 0 Then
      writer.WriteEndTag("optgroup")
      writer.WriteLine()
   End If
   'if it's the first optgroup, or if the optgroup
   'has changed, start a new optgroup
   If Not sOptGroup = sPrevOptGroup Or num2 = 0 Then
      writer.WriteBeginTag("optgroup")
      writer.WriteAttribute("label", sOptGroup)
      writer.Write(">"c)
      writer.WriteLine()
      sPrevOptGroup = sOptGroup
   End If
End If

完成这些后,XList 控件就能毫无问题地支持 <optgroup> 标签了。但我认为,将数据绑定功能扩展到使用 OptGroup 以及 TextValue 来填充控件可能会更好。所以我添加了以下两个属性:

<Description("XList_DataOptGroupField"), _
     Category("Data"), DefaultValue("")> _
Public Overridable Property DataOptGroupField() As String
   Get
      Dim obj1 As Object = Me.ViewState("DataOptGroupField")
      If (Not obj1 Is Nothing) Then
         Return CType(obj1, String)
      End If
      Return String.Empty
   End Get
   Set(ByVal value As String)
      Me.ViewState("DataOptGroupField") = value
   End Set
End Property 'DataOptGroupField
<Description("XList_DataOptGroupFormatString"), _
               DefaultValue(""), Category("Data")> _
Public Overridable Property DataOptGroupFormatString() As String
   Get
      Dim obj1 As Object = Me.ViewState("DataOptGroupFormatString")
      If (Not obj1 Is Nothing) Then
         Return CType(obj1, String)
      End If
      Return String.Empty
   End Get
   Set(ByVal value As String)
      Me.ViewState("DataOptGroupFormatString") = value
   End Set
End Property 'DataOptGroupFormatString

我修改了各种涉及数据绑定的方法,以包含这些属性。你可以查看源代码,了解具体是如何实现的。

啊,我的属性们,你们都去哪儿了……?

我不知道为什么 Microsoft 决定不支持 ListItemAttributes。我在我的 <select> 控件中使用 background-colorcolor 等样式。我甚至有时使用 label 属性来存储数据库中的额外数据。令我非常恼火的是,Microsoft 知道这项功能缺失,甚至不承认这是一个 bug。

所以我决定绕过它。你可能会认为 Attributes 属性已经保留了状态,因为它看起来是这样的:

<Browsable(False), _
  DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)> _
Public ReadOnly Property Attributes() As System.Web.UI.AttributeCollection
   Get
      If (Me._attributes Is Nothing) Then
         Me._attributes = New _
           System.Web.UI.AttributeCollection(New StateBag(True))
      End If
      Return Me._attributes
   End Get
End Property 'Attributes

毕竟,New StateBag 的意义何在,如果它实际上不维护状态?但它显然不这样做,我不得不另辟蹊径。我不能仅仅在 SaveViewState 中保存 Attribute 属性,并在 LoadViewState 中加载它,因为 AttributeCollection 是不可序列化的(这也许是它一开始就不维护状态的原因,现在想想)。所以我决定使用一个 Pair 数组。我本可以用一个二维的 String 数组,但我觉得差别不大,而且我很少有机会玩 Pair

首先,我添加了一个新的全局常量,并将 _misc 改为 BitArray(6)

Private Const _ATTRIBUTESISDIRTY As Integer = 5

然后,我在 IAttributeAccessor.SetAttribute 方法中添加了这一行,以便让属性更改使 XListItem 变得“脏”,就像其他属性一样:

Me._misc.Set(5, True)

在那之后,就只是修改 SaveViewStateLoadViewState 了,如下所示:

Friend Sub LoadViewState(ByVal state As Object)
   Dim arrState As Object() = CType(state, Object())
   If (Not arrState(0) Is Nothing) Then
      If TypeOf arrState(0) Is Pair Then
         Dim pair1 As Pair = CType(arrState(0), Pair)
         If (Not pair1.First Is Nothing) Then
            Me.Text = CType(pair1.First, String)
         End If
         Me.Value = CType(pair1.Second, String)
      Else
         Me.Text = CType(arrState(0), String)
      End If
   End If
   'custom state management for OptGroup
   If Not arrState(1) Is Nothing Then
      Me.OptGroup = CType(arrState(1), String)
   End If
   'custom state management for Attributes
   If Not arrState(2) Is Nothing Then
      If TypeOf arrState(2) Is Pair() Then
         Dim colAttributes As Pair() = CType(arrState(2), Pair())
         For i As Integer = 0 To colAttributes.Length - 1
            Me.Attributes.Add(colAttributes(i).First.ToString, _
                                colAttributes(i).Second.ToString)
         Next i
      End If
   End If
End Sub 'LoadViewState

我将 Pair 数组加载到 XListItemAttributes 属性中。

Friend Function SaveViewState() As Object
   Dim arrState(2) As Object
   If (Me._misc.Get(2) AndAlso Me._misc.Get(3)) Then
      arrState(0) = New Pair(Me.Text, Me.Value)
   ElseIf Me._misc.Get(2) Then
      arrState(0) = Me.Text
   ElseIf Me._misc.Get(3) Then
      arrState(0) = New Pair(Nothing, Me.Value)
   Else
      arrState(0) = Nothing
   End If
   'custom state management for OptGroup
   arrState(1) = Me.OptGroup
   ''custom state management for Attributes
   If Me.Attributes.Count > 0 Then
      ReDim _attributes2(Me.Attributes.Count - 1)
      Dim i As Integer = 0
  
      Dim keys As IEnumerator = Me.Attributes.Keys.GetEnumerator
      Dim key As String
      While keys.MoveNext()
         key = CType(keys.Current, String)
         _attributes2(i) = New Pair(key, Me.Attributes.Item(key))
         i += 1
      End While
  
      arrState(2) = _attributes2
   End If
   Return arrState
End Function 'SaveViewState

我将 AttributesXListItem 复制到一个 Pair 数组中,并将其保存到 XListItem 的 ViewState 中。

ListItemAttributes 保持状态所需的更改微不足道,我实在不明白为什么 Microsoft 的控件不这样做。

水平滚动

之前的更改都是跨浏览器兼容的。这个不是。

XList 控件可以在任何浏览器中使用,但启用 EnableHScroll 会使其在大多数非 IE6+ 浏览器中无法工作。

<Category("Appearance"), DefaultValue(False), Description("XList_EnableHScroll")> _
Public Overridable Property EnableHScroll() As Boolean
   Get
      Dim obj1 As Object = Me.ViewState("EnableHScroll")
      If (Not obj1 Is Nothing) Then
         Return CType(obj1, Boolean)
      End If
      Return False
   End Get
   Set(ByVal value As Boolean)
      Me.ViewState("EnableHScroll") = value
   End Set
End Property 'EnableHScroll

ListBox 中并不总是设置 HeightWidth 属性。通常,Height 会被忽略,而由 Size 代替,SizeListItem 的高度为单位设置 ListBox 的高度。当使用 Width 时,它通常设置得相当宽,以免截断任何过长的 ListItem

对于我们带滚动的 XList,需要 HeightWidth。所以我们在构造函数中添加了默认值,如下:

If Me.EnableHScroll Then
   If Me.Width.IsEmpty Then Me.Width = Unit.Pixel(100)
   If Me.Height.IsEmpty Then Me.Height = Unit.Pixel(100)
End If

我们重写了 HeightWidth 属性,以确保它们永远不为空。

<Browsable(True), _
  DesignerSerializationVisibility(DesignerSerializationVisibility.Content)> _
Public Overrides Property Width() As Unit
   Get
      If Me.EnableHScroll AndAlso MyBase.Width.IsEmpty Then
         Return Unit.Pixel(100)
      End If
      Return MyBase.Width
   End Get
   Set(ByVal Value As Unit)
      MyBase.Width = Value
   End Set
End Property 'Width

<div> 放在控件周围意味着要重写 RenderBeginTag,就像我们在 XTable 中所做的一样。同样,我们使其条件化,因为只有当 EnableHScrollTrue 时才需要它。并且我们以类似的方式修改 RenderEndTag

Public Overrides Sub RenderEndTag(ByVal writer _
                            As System.Web.UI.HtmlTextWriter)
   MyBase.RenderEndTag(writer)
   If Me.EnableHScroll Then
      writer.RenderEndTag()
   End If
End Sub 'RenderEndTag

除了我用于修改 RenderBeginTag 的标准代码外,我还为 <select> 添加了两个事件:

MyBase.Attributes.Add("onchange", "javascript:XList_ShowOption(this);")
MyBase.Attributes.Add("onresize", _
         "javascript:XList_ResizeSelect(this);XList_ShowOption(this);")

这些事件触发使控件正常工作的一个或两个函数。XList_ResizeSelect 使 <select> 具有适当的大小。如果 <select> 的宽度小于 <div> 的宽度,该函数会增加 <select> 的宽度,直到它填满 <div> 的可见空间。高度同理。如果 <select> 中的 <option> 元素数量在 <select> 底部和 <div> 底部之间留有空间,该函数会增加 <select> 的高度以适应。否则,<select> 的大小将设置为 <select><option> 元素的总数。

function XList_ResizeSelect(objSelect){
   //check to see if the object is visible
   if (objSelect.offsetHeight == 0){
      return;
   }
   //remove the onresize event so that it doesn't loop forever
   objSelect.onresize = null;
   //make sure it's a listbox and not a dropdown
   objSelect.size = objSelect.options.length < 2 ? 2 : _
                                   objSelect.options.length;
   if (objSelect.offsetHeight < _
       objSelect.parentElement.offsetHeight - scrollbarWidth){
     objSelect.style.height = _
       objSelect.parentElement.offsetHeight - scrollbarWidth / 2;
   }
   objSelect.style.width = "";
   if (objSelect.offsetWidth < objSelect.parentElement.offsetWidth_
                                                    - scrollbarWidth){
      objSelect.style.width = (objSelect.parentElement.offsetWidth_
                                           - scrollbarWidth) + "px";
   } else {
      objSelect.style.width = "auto";
   }
}

一个实用函数在页面加载时计算滚动条的宽度。我从 Scott Isaac 关于 DHTML 滚动条的文章 中借鉴了这项有用技术。

我使用了 resize 事件来触发此操作,因为它最合理。如果你在客户端添加或删除 <select> 中的 <option>,你应该在自己的客户端代码中调用 XList_ResizeSelect 来确保 <select> 根据其新内容进行调整。大小。

XList_ShowOption 稍微复杂一些。正如我在本文开头提到的(还记得很久以前吗?<grin>),如果你使用此技术并向下滚动到 <div> 的底部,你选择的 <option> 将会消失。所以我需要找到一种方法来使 <div> 滚动,以便选中的 <option> 保持可见。如果能使用 scrollIntoView 方法就好了,但该方法不适用于 <option> 元素。

这是使 <option> 进入视区的代码。我将计算分开以使其更易于理解(并且在我最初处理它时更容易调试):

function XList_ShowOption(objSelect){
   idx = objSelect.selectedIndex
   if (idx == -1){
      return;
   }
   if (objSelect.length == 0){
      return;
   }
   objDiv = objSelect.parentElement;
   HeightOfSelect = objSelect.clientHeight;
   OptionsInSelect = objSelect.options.length;
   HeightOfOption = HeightOfSelect / OptionsInSelect;
   HeightOfDiv = objDiv.clientHeight;
   OptionsInDiv = HeightOfDiv / HeightOfOption;
   OptionTopFromTopOfSelect = HeightOfOption * idx;
   OptionTopFromTopOfDiv = OptionTopFromTopOfSelect - objDiv.scrollTop;
   OptionBottomFromBottomOfDiv = HeightOfDiv - _
               OptionTopFromTopOfDiv - HeightOfOption;
   if (OptionTopFromTopOfDiv < 0) {
      objDiv.scrollTop = OptionTopFromTopOfSelect;
   } else if (OptionBottomFromBottomOfDiv < 0 && _
          OptionBottomFromBottomOfDiv > 0 - HeightOfOption) {
      objDiv.scrollTop = objDiv.scrollTop + HeightOfOption;
   } else if (OptionBottomFromBottomOfDiv < 0) {
      objDiv.scrollTop = OptionTopFromTopOfSelect;
   }
}

这两个函数,以及计算滚动条宽度的实用程序,都会在页面上编写一次,而不管页面上有多少个 XList 实例。我为此使用了 RegisterClientScriptBlock。但我们还需要对脚本做另一件事,而且它必须为每个实例单独完成,这就是 RegisterStartupScript 的作用。

页面加载时,需要调整 <select> 的大小。这必须为控件的每个实例发生。此外,如果控件的 SelectedIndex 大于 -1,则页面必须将正确的 <option> 标记为选中,并运行 XList_ShowOption,以确保其可见。你可以在源代码中看到正在注册的脚本。

然而,在设计环境中,脚本不会运行。所以我稍微修改了 XListDesignerGetDesignTimeHtml 方法在两个地方调用 MyBase.GetDesignTimeHtml。我用对新创建的名为 GetDesignTimeResize 的方法的调用替换了这些调用。这是修改后的 GetDesignTimeHtml 和新的 GetDesignTimeResize

Public Overrides Function GetDesignTimeHtml() As String
   Dim collection1 As XListItemCollection = Me._xList.Items
   If (collection1.Count > 0) Then
      'Return MyBase.GetDesignTimeHtml
      Return GetDesignTimeResize()
   End If
   If Me.IsDataBound Then
      collection1.Add("bound")
   Else
      collection1.Add("unbound")
   End If
   'Dim text1 As String = MyBase.GetDesignTimeHtml
   Dim text1 As String = GetDesignTimeResize()
   collection1.Clear()
   Return text1
End Function 'GetDesignTimeHtml

Public Overridable Function GetDesignTimeResize() As String
   Dim _xList As XList = CType(Component, XList)
   If _xList.EnableHScroll Then
      Dim str As String = MyBase.GetDesignTimeHtml
      str = Replace(str, "<select", _
            "<select style=width:" & _xList.Width.ToString)
      Dim _itemCount As Integer = _xList.Items.Count
      Dim _selectTagEnd As Integer = InStr(str, ">")
      _selectTagEnd = InStr(_selectTagEnd + 1, str, ">")
      Dim _sizeAttribute As Integer = InStr(str, " size=")
      If _sizeAttribute > 0 And _sizeAttribute < _selectTagEnd Then
         str = Replace(str, "size=""4""", "size=""" & _
               IIf(_itemCount < 2, 2, _itemCount).ToString & """")
      Else
         str = Replace(str, "<select", "<select size=""" _
               & IIf(_itemCount < 2, 2, _itemCount).ToString & """")
      End If
      Return str
   Else
      Return MyBase.GetDesignTimeHtml
   End If
End Function 'GetDesignTimeResize

我唯一觉得需要做的事情是在 PreFilterProperties 的末尾添加几行代码,以确保 DataOptGroupField 被视为与 DataTextFieldDataValueField 相同。

结论

差不多就是这样。该控件有三种模式:DropDownListListBox、滚动 ListBox,在每种情况下,你都可以选择是否渲染 OptGroup。所以它有点像六合一控件。

© . All rights reserved.