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

ComboBox WebControl

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.72/5 (64投票s)

2003年7月14日

8分钟阅读

viewsIcon

1108851

downloadIcon

19982

IE ComboBox WebControl

引言

我一直想知道为什么微软,或者其他任何人都从未提供原生的 HTML ComboBox 元素。在网上研究了一番后,我决定自己动手。我想要一个在视觉上区别于默认 HTML Select 元素,并提供自动完成和列表验证等功能的控件。本文将详细介绍构成该控件的 JavaScript 编写的 View Link Behavior 和 C# 代码。

历史

此 Web 控件的 1.0 版本基于 Jeremy Bettishttp://www.deadbeef.com/ 编写的元素行为。虽然 Jeremy 的行为运行良好,但它不包含我想要的组合框的所有功能。此次更新是对该行为的全面重写,并解决了以下功能请求和 bug 修复。

  1. ListItem 值检索 - 组合框的值现在映射到 ListItem (Option) 的 value 属性。
  2. 自动验证 - 您现在可以指定组合框将用户输入的文本与 ListItem 中的文本进行验证。
  3. 文本字段 OnChange 事件 - 文本字段的 OnChange 事件会冒泡到组合框控件。
  4. Z 索引 - 下拉箭头将不再与其他 HTML 元素重叠。
  5. 设计器模式下的数据绑定支持。
  6. 与 HTML Select 元素在视觉上的区别。
  7. 定位(Bug 修复)- 组合框现在可以正确打印或绝对定位。
  8. 第一个项目选择(Bug 修复)- 您现在可以选择下拉列表中的第一个项目。

使用 ComboBox 控件

注意:此控件是在 IE 6.0 下开发和测试的。但它可能兼容 IE 5.5,但不能与更旧版本或任何非微软浏览器一起使用。

对于所有新手,请仔细阅读并遵循说明!对于本节已解答的任何疑问,我将不再回复。

首先,我们需要将运行时文件,包括 combobox.htcimages 目录,复制到它们的默认虚拟目录 /$wwwroot/webctrl_client/progstudios/1_1/。您可以通过向 web.config 中的 appSettings 集合添加新元素来指定其他位置。

<appSettings>
  <add key="PROGWEBCONTORLS_COMMONFILEPATH" value="MyVirtualPath" />
</appSettings>

源代码中包含的二进制和项目文件基于 .NET Framework 1.1 和 Visual Studio 2003。它们与 1.0 或 VS.NET 2002 不兼容,因此您需要自己重新编译程序集。要使用 VS.NET 2002 重新编译项目,请按照以下步骤操作

  1. 创建一个新的 C# "Web Control Library" 项目;将其命名为 "ProgStudios.WebControls" 或任何您喜欢的名称。
  2. 右键单击项目并选择 "Properties"。
  3. 确保 "Assembly Name" 和 "Default Namespace" 属性设置为 "ProgStudios.WebControls"。
  4. 将 ZIP 文件中的文件拖放到项目中。
  5. 右键单击位图图像,并在属性窗口中将 "Build Action" 属性设置为 "Embedded Resource"。
  6. 右键单击解决方案并选择 "Properties"。在 "Control Properties/Configuration" 下,确保配置设置为 "Release",然后按 "OK"。
  7. 右键单击项目并选择 "Build",或按 "ctrl+shift+b" 来构建项目。
  8. 输出的 DLL 将位于 bin/release 目录。
  9. 将控件添加到工具箱。
    1. 右键单击要放置控件的选项卡,然后选择 "Add/Remove Items"。
    2. 单击 "Browse" 并找到 DLL。
  10. 完成了!

要将程序集添加到 Web 项目,请右键单击解决方案资源管理器中的 "References" 并通过单击 "Browse" 来找到 DLL,或在 "Projects" 选项卡下添加项目。如果您不使用 Visual Studio,请确保将 DLL 复制到 bin 目录。安装控件后,您可以将控件从设计模式拖放到网页上。以下是生成的 HTML 代码。

第一行注册了程序集,并将 "cc1" 标签前缀与程序集命名空间关联起来。"cc1:combobox" 代表实际的控件。子元素 asp:ListItem 可以替换为传统的 <option> 标签。

<%@ Register TagPrefix="cc1" Namespace="ProgStudios.WebControls"
  Assembly="ProgStudios.WebControls" %>
<html>
  <body>
    <cc1:combobox id="ComboBox1" runat="server">
      <asp:ListItem>ComboBox</asp:ListItem>
    </cc1:combobox>
  </body>
</html>

您可以手动添加新项目,或者像在演示项目中一样,将其绑定到 DataView。与 HtmlSelectDropDown 控件一样,ComboBox 可以绑定到任何实现了 IListSourceIListIEnumerable 的实例。

  <cc1:ComboBox id="ComboBox1" runat="server" DataSource="<%# dataView1 %>" 
    DataValueField="ProductID" DataTextField="ProductName">
  ...
  protected DataView dataView1;
  protected void Page_Load(object sender, System.EventArgs e) {
    if (!this.IsPostBack) {
      DataSet ds = new DataSet();
      FileStream fs = new FileStream(
           Server.MapPath("newdataset.xml"),
           FileMode.Open,FileAccess.Read);
      StreamReader reader = new StreamReader(fs);
      ds.ReadXml(reader);
      fs.Close();
      dataView1 = new DataView(ds.Tables[0]);
      ComboBox1.DataBind();
    }
  }
...
<prog:combobox id="ComboBox1" runat="server"/>

该控件还公开了一个服务器端事件 ServerChange。您可以通过挂接 OnServerChange 事件处理程序来附加到该事件。如果在回发时值发生更改,将触发此事件。您还可以通过将 AutoPostBack 属性设置为 true 来指定控件自动回发。

<script runat="server" language="c#">
  void ComboBox1_OnServerChange(object sender, System.EventArgs e) {
    Msg.InnerText = "ServerChange Event Fired: Value=" + ComboBox1.Value;
  }
</script>
....

<prog:combobox id="ComboBox1" runat="server" 
AutoPostBack="true" OnServerChange="ComboBox1_OnServerChange">

AutoValidate 属性用于确保用户键入的文本与列表项集合中的文本匹配。如果不匹配,selectedIndex 将设置为 -1,值为一个空字符串。您可以通过设置 ErrorMessage 属性,在 ComboBox 失去焦点时显示错误消息。

在 C# Web 控件代码内部

为了获得一些免费功能,我决定继承自 WebControl 类。这个决定很容易,因为基类有一个 AttributesCollection 类成员。这很重要,因为所有 HTML 元素都支持 expando 属性,这些属性基本上是您可以附加到元素上的任意属性,并且可以通过客户端脚本访问。任何服务器控件无法识别的属性都将被添加到属性集合中,并在客户端呈现到根元素。

其余代码非常直接。除了公开 Web 客户端上可用的相应服务器端属性、方法和事件之外,我还必须实现某些方法来处理所有底层工作。控件必须参与回发以处理状态,因此,实现了 IPostBackDataHandler.RaisePostDataChangedEventIPostBackDataHandler.LoadPostData 接口方法来加载已发布的 قيمة(value)。

...
public virtual void RaisePostDataChangedEvent() {
  this.OnServerChange(EventArgs.Empty);
}
public virtual bool LoadPostData(string postDataKey, 
  NameValueCollection postCollection) {
  string sValue = this.Value;
  string sPostedValue = postCollection.GetValues(postDataKey)[0];
  if (!sValue.Equals(sPostedValue)) {
    this.Value = sPostedValue;
    return true; 
  }
  return false;
}
...

我还必须重写 SaveViewStateLoadViewState 方法,以便在回发时管理 ListItem

...
protected override void LoadViewState(object savedState) {
  if (savedState != null) {
    object[] State = (object[])savedState;
    this.Value = (string) State[0];
    ArrayList ItemsList = (ArrayList) State[1];
    foreach (object itemText in ItemsList) {
      if (this.Items.FindByText((string)itemText)==null) {
        ListItem item = new ListItem();
        item.Text = (string) itemText;
        this.Items.Add(item);
      }
    }
  }
}
protected override object SaveViewState() {
  ArrayList ItemsList = new ArrayList();
  foreach (ListItem item in this.Items) {
    ItemsList.Add(item.Text);
  }
  object[] savedState = new object[2];
  savedState[0] = this.Value;
  savedState[1] = ItemsList;
  return savedState;
}
...

编写控件生成器类也非常直接。我继承了 ControlBuilder 并重写了 GetChildControlType 方法,只允许 ListItemOption 子元素。

...
public override Type GetChildControlType(string tagName, 
  System.Collections.IDictionary attribs) {
  string szTagName = tagName.ToLower();
  int colon = szTagName.IndexOf(':');
  if ((colon >= 0) && (colon < (szTagName.Length + 1))) {
    // Separate the tagname from the namespace
    szTagName = szTagName.Substring(colon + 1, 
        szTagName.Length - colon - 1);
  }
  if (String.Compare(szTagName, "option", true, 
       System.Globalization.CultureInfo.InvariantCulture) == 0 ||
       String.Compare(szTagName, "listitem", true, 
       System.Globalization.CultureInfo.InvariantCulture) == 0 ) {
    return typeof(System.Web.UI.WebControls.ListItem); 
  }
  // No Type was found, throw an exception
  throw new Exception(String.Format(
    "Invalid child with tagname \"{0}\"", tagName));
}

ComboBox Link View Behavior

好的,现在我们来看看文章中最有趣的部分。本节将在一定程度上揭示我朋友称之为网页开发黑魔法的行为编程。当前版本的 ComboBox 采用了元素行为中的 ViewLink 功能,该功能允许您将文档片段封装在 HTML 组件中。ViewLink 元素行为(也称为主元素)也维护自己的文档树。这意味着我们现在可以使用 HTML 创建元素和样式表属性,并通过 HTC 文件中的脚本来操作这些对象,而无需担心名称冲突。

例如,ComboBox 定义了一个 ID 为 "textField" 的文本 input 字段。如果使用传统的元素行为,我们必须使用 document.createElement 方法来定义元素,然后使用 element.appendChild 方法将其添加到主文档树中。然而,如果我们使用 View Link 功能,我们只需使用 HTML 来创建和定义我们的元素。此外,如果我们想在页面上使用多个 ComboBox,或者使用另一个定义了具有相同 ID 的完全不同元素的行为,该怎么办?我们会遇到名称冲突。

<PUBLIC:COMPONENT tagName="COMBOBOX">
...
  <STYLE TYPE="text/css">
    .clsTextField {border:none;margin-right:1px;margin-left:1px;}
    .clsTextFieldCell {background-color:white;border:ridge 1px buttonface;}
    .clsTextFieldCell_hover {background-color:white;border:solid 1px navy;}
    ...
</STYLE>
  <body>
    <table unselectable="on" ID="tblCombobox" 
           cellspacing="0" cellpadding="0" border="0">
      <tr>
        <td unselectable="on" class="clsTextFieldCell" 
            id="textFieldCell">
          <input class="clsTextField" id="textField" 
             type="text" NAME="textField">
        </td>
        <td id="dropDownArrowCell" class="clsDropDownCell">
           <img width=5 height=3 
          id="imgArrow" src="images/down_arrow.gif" vspace="2" 
                  hspace="3"/></td>
      </tr>
    </table>
  </body>
</PUBLIC:COMPONENT>

您还会注意到我在 HTC 文件中定义了 CSS 样式类。在 mouseovermouseoutclick 事件处理程序中会交换 className 属性。因此,如果您想修改外观和感觉,就必须在这里进行。

图 1

主元素的尺寸由其内容片段定义。动态创建的元素,无论是否绝对定位,都会影响并受这些尺寸的限制。对于 ComboBox,我们创建了一个下拉列表,其中包含一个 HTML 表格,该表格代表 ListItems 集合。每个表格单元格都会捕获窗口事件,例如 mouseoutmouseoverclick。当添加到主元素的文档树时,新的下拉列表的尺寸会比初始尺寸大很多。正如您在图 1 中看到的,ComboBox 位于 HTML 表格内,单击时,下拉列表会扩展表格单元格的高度。下拉列表的右边缘也会被裁剪,因为没有显示滚动条。

正如您所见,这样做行不通。我们需要将新创建的表格附加到主文档树中。这样做可以让我们绝对定位,而不会改变主元素的尺寸。这还引出了另一个有趣的问题,textField 包含在主元素中。当主文档的表单提交时,textField 数据不会在请求中发送。如果您遍历 Forms 集合,将找不到 textField。因此,为了使 ComboBox 参与回发,我们必须创建一个隐藏字段并将其附加到主元素的父 form

最后,我想提一下 options 集合。由于 ComboBox 的下拉列表本质上是一个 HTML 表格,并且所有的状态逻辑都封装在 ComboBox 本身中,所以我们实际上不需要创建传统的 HTML Select 元素来进行渲染。尽管 ComboBox 是为在服务器环境中使用而编写的,但在某些情况下,您可能希望从客户端动态添加或删除列表项。因此,我们必须将该集合公开为一个公共属性。我们可以选择创建自己的集合类并引入全新的 API,或者我们可以创建一个内存中的 select 元素,并将其 options 集合映射到公共属性。我选择了后者。由于 select 元素存在于内存中,并未在浏览器中渲染,因此我公开了 repaint() 方法,该方法将显式重绘下拉列表。添加或删除客户端项后,需要调用此方法。

已知问题

  1. 内联对齐 - ComboBox 与相邻文本的对齐不正确。将控件放在表格单元格内以解决此问题。
© . All rights reserved.