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

DropDownCheckList ASP.NET 服务器控件

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.86/5 (31投票s)

2005年10月18日

10分钟阅读

viewsIcon

394064

downloadIcon

9356

CheckBoxList 的子类,它呈现为一个自定义的下拉列表,其行为通过客户端 JavaScript 定义。

引言

对于最终用户而言,DropDownCheckList 类似于一个 HTML <select> 框,单击时会显示带有复选框的可用选项。显示框会列出选中的选项,并带有开发人员定义的列表分隔符,以及在列出的选项超出显示宽度时附加省略号或其他文本的功能。本文介绍了该控件的常用属性和方法,并描述了选择 CheckBoxList 子类化的原因(从中获得的益处)以及因此面临的基本问题。还介绍了通过客户端 JavaScript 解决的挑战,包括在必要时显示省略号,以及使用文档单击处理程序在列表外部单击时自动折叠列表。后者和借用的“shim”技术在开发自定义下拉 Web 控件时尤其有用。

使用控件

DropDownCheckList 的设计包含一个客户端库,旨在提供跨平台兼容性,至少对于主流浏览器的现代版本是如此。它已在 Internet Explorer 6.x、Netscape 7.x 和 8.x 以及 Firefox 1.x 中进行了专门测试。JavaScript 文件 DropDownCheckList.js 包含 DropDownCheckList 的客户端对象定义。要使用该控件,请先将文件 DropDownCheckList.js 复制到以下目录

wwwroot\aspnet_client\UNLV_IAP_WebControls\DropDownCheckList

有多个属性可用于自定义控件的显示。使用 DisplayBoxCssClass 和/或 DisplayBoxCssStyle 属性来指定显示框的 CSS 样式。同样,DisplayTextCssClassDisplayTextCssStyle 指定显示文本的 CSS 样式,而 CheckListCssClassCheckListCssStyle 表示复选框列表框的 CSS 样式。

显示框中下拉图像的渲染取决于 DropImageSrcDropImagePosition 属性。如果 DropImageSrc 为空,或者 DropImagePosition 设置为 NoImage,则下拉框中不会显示图像。如果指定了 DropImageSrc,则图像的位置将是 LeftRightBoth,由 DropImagePosition 指示。

带有各种 CSS 样式和下拉图像的 DropDownCheckList

控件的行为由多个附加属性定义。DropDownMode 属性决定复选框列表的显示方式。如果设置为 Inline,则复选框列表将在周围的 HTML 中展开。如果设置为 OnTopOnTopWithShim,则复选框列表将绝对定位在其他 HTML 内容之上。“shim”选项是为了与 Internet Explorer 兼容而存在的;有关更多详细信息,请参见下面的 使用 Shim 兼容 Internet Explorer

Separator 属性指示用于分隔列表项的字符。DisplayTextWidth 指定显示框中用于列出选定选项的最大像素宽度。当列出的选项超出此宽度时,文本将被截断,并附加 TruncateString 属性。通常,TruncateString 是一个省略号(…),但也可根据需要设置为其他文本或空字符串。如果您希望显示框扩展而不是截断文本,请将 DisplayTextWidth 设置为 -1。

要指示未选中任何选项时要显示的文本,请将 TextWhenNoneChecked 属性设置为所需的字符串。您还可以通过将 DisplayTextList 属性设置为 LabelsValues 来指定是在显示框中列出复选框标签还是值(分别是子 ListItemTextValue 属性)。

DisplayTextList 属性设置为 Values

作为 CheckBoxList 的子类,数据绑定属性(如 DataSourceItems 集合)是继承的。在表单提交时,开发人员可以检查 Items 集合中选中的复选框,就像处理 CheckBoxList 一样。DropDownCheckList 还公开了两个重载的实用方法:SelectedLabelsToString()SelectedValuesToString()。每个方法都会返回选中的项,并将其列为一个单一的连接字符串。这些重载允许开发人员指定列表分隔符和文本分隔符。

以下是一个使用 DropDownCheckList 控件的完整 .aspx 页面的示例,演示了几个属性和两个 Selected... ToString() 方法

<%@ Page Language="c#" AutoEventWireup="true" %>
<%@ Register TagPrefix="cc1" 
             Namespace="UNLV.IAP.WebControls" 
             Assembly="DropDownCheckList" %>

<script runat="server">
    void Page_Load(object o, EventArgs e)
    {
        lblResults.Text = "";
    }
    
    void btnSubmit_Click(object o, EventArgs e)
    {
        string sLabels = dd.SelectedLabelsToString(", ");
        string sValues = dd.SelectedValuesToString(", ", "'");
        
        lblResults.Text = "Selected Items:  " + sLabels
          + "<br />"
          + "Values: " + sValues;
    }
    
    void btnClear_Click(object o, EventArgs e)
    {
        dd.SelectedValue = null;
    }
    
</script>


<html>
  <head>
    <title>DropDownCheckList Sample</title>
    <style>
      .boxStyle
      {
        border           : 2px solid darkBlue;
        background-color : lightBlue;
        padding          : 8px;
      }
    </style>
  </head>
  
  <body>
    <form runat="server">
    
        <h3>DropDownCheckList Sample</h3>
        <p>Click the drop-down box to select options</p>
        
        <cc1:DropDownCheckList id="dd" runat="server" 
                RepeatColumns       = "2"
                DropImageSrc        = "dropImage.gif"
                DropImagePosition   = "Right"
                DropDownMode        = "OnTopWithShim"
                CheckListCssClass   = "boxStyle"
                CheckListCssStyle   = ""
                DisplayTextCssStyle = "font-family: Tahoma;"
                DisplayTextWidth    = "180"
                DisplayTextList     = "Labels"
                Separator           = ", "
                TruncateString      = "..."
                TextWhenNoneChecked = "--select--"
            >
            <asp:ListItem text="North" value="N" />
            <asp:ListItem text="South" value="S" />
            <asp:ListItem text="East"  value="E" />
            <asp:ListItem text="West"  value="W" />
            <asp:ListItem text="Northeast" value="NE" />
            <asp:ListItem text="Southeast" value="SE" />
            <asp:ListItem text="Northwest" value="NW" />
            <asp:ListItem text="Southwest" value="SW" />
        </cc1:DropDownCheckList>
        
        <p>
            <asp:Button id="btnSubmit" runat="server" text="Submit Choices"
                        onClick="btnSubmit_Click" />

            <asp:Button id="btnClear" runat="server" text="Clear Choices"
                        onClick="btnClear_Click" />
        </p>
        
        <p><asp:Label id="lblResults" runat="server" /></p>
    
    </form>
  </body>

</html>

继承自 CheckBoxList

子类化 CheckBoxList 有积极和消极的后果。积极的一面是,我们的 DropDownCheckList 继承了 CheckBoxList 内置的数据绑定支持、其 Items 集合以及其重复布局属性。

消极的一面是,我们无法访问单个 CheckBox 控件,而这是我们添加 `value` 属性(当 DisplayTextList 设置为 Values 时需要)和客户端 onclick 处理程序所必需的。简单来说,CheckBoxList 不将 CheckBox 控件作为子项,因此我们无法像这样操作单个框。我们可以在客户端库中添加一个 onclick 处理程序,但向渲染的复选框添加 `value` 属性是必须在服务器端管理的。

为了在这一限制下工作,同时又能利用 CheckBoxList 的数据绑定和重复布局渲染,我们在重写的 Render() 方法中执行了一个技巧。在生成显示框所需的 HTML 后,我们渲染一个用于复选框列表的 <div> 标签。如果不需要渲染 `value` 属性,我们只需将基类 CheckBoxListRender() 方法应用于输出流。但如果我们确实需要渲染值,那么我们将在一个自定义的 HtmlTextWriter 上执行 base.Render(),该 HtmlTextWriter 使用 StringWriter 而不是普通的输出流构建。这样,我们就可以检索渲染的列表作为一个普通的 string,并对其进行操作以插入我们自己的 `value` 属性。Render() 中与此相关的代码如下所示

protected override void Render(HtmlTextWriter output)
{
  . . . 

    // next, render the contents of the checkboxlist;
    // do we need to render values?
    if (_displayTextList == DisplayTextListEnum.Values)
    {
        // if so, we need to render to a string first
        // so we can include our own values attribute
        StringWriter sw = new StringWriter();
        HtmlTextWriter wr = new HtmlTextWriter(sw);
        base.Render(wr);
        string sHtml = sw.ToString();
        wr.Close();
        sw.Close();

        // now modify the code to include custom attributes
        sHtml = ModifyRenderedCheckboxes(sHtml);

        // and write to the output stream
        output.Write(sHtml);
    }
    else
    {
        // if we're not rendering custom value attributes,
        // just output the checkboxes to the output stream
        base.Render(output);
    }

      . . . 

}

上面引用的 ModifyRenderedCheckboxes() 方法使用正则表达式来定位每个复选框,然后将适当的 ListItem.Value 属性插入为 `value` 属性。

protected string ModifyRenderedCheckboxes(string sHtml)
{

    // use a regular expression to identify in the form:
    //<input id="DropDownCheckList1_0" type="checkbox" 
    string s = string.Format("input id=\"{0}_(?<" + 
               "index>\\d+)\"\\s+type=\"checkbox\"\\s+", 
               this.ClientID);

    Regex r = new Regex(s, RegexOptions.IgnoreCase);

    MatchCollection matches = r.Matches(sHtml);
    foreach (Match m in matches)
    {
        int index = Convert.ToInt32(m.Groups["index"].Value);
        sHtml = Regex.Replace(sHtml, m.Value, m.Value + " value=\"" 
              + this.Items[index].Value + "\" ");
    }

    return sHtml;
}

客户端 JavaScript 中解决的挑战

该控件的驱动目标之一是保持跨平台兼容性,至少与 Internet Explorer (6.x)、Netscape (7.x 和 8.x) 和 Firefox (1.x) 的近期版本兼容。这一目标影响了客户端脚本的几个方面。

(关于 Netscape 6.x 的说明:由于一个重要原因,本文提供的控件无法在 Netscape 6 中运行。Netscape 6 不会提交设置为 displaynone<div> 中的表单输入——因此当复选框列表折叠时,其复选框选择不会与其他表单输入一起提交。希望支持 Netscape 6 的开发人员可能希望尝试使用 visibility 属性来代替 display 属性。

当显示文本过宽时附加省略号

由于要列出的选定选项可能比显示的宽度更多,因此在截断文本后附加一个指示符很有用。通常,在这种情况下会使用省略号(…)。

Internet Explorer 支持 text-overflow[^] 样式属性,顾名思义,它可以设置为 ellipsis。不幸的是,这不是标准化的,并且在 Firefox 和 Netscape 中不受支持。此外,IE 只支持省略号——当字符串被截断时,您无法自定义附加的文本。

为确保对 Firefox 和 Netscape 的支持,并允许自定义 TruncateString 属性,我们在客户端脚本文件中使用原型函数 DisplayCheckedItems(),其定义如下

function DDCL_DropDownCheckList_DisplayCheckedItems()
{
    var sLabel = "";
    var sCurrent = "";
    var sFull = "";
    var sBefore = "";
    var sCompText = "";
    var bEllipsisAdded = false;

    // get all checkboxes in the checklist
    var e = this.divCheckboxes.getElementsByTagName("input");
    // clear the display text
    this.divText.innerHTML = "";
    // clear the title (tooltip) attribute
    this.divDisplayBox.title = "";

    // loop through all checkboxes in the checklist to see 
    // which ones are checked;
    for (var i=0; i<e.length; i++)
    {
      if (e[i].type == "checkbox" && e[i].checked)
      {
        // if the checkbox is checked, get its associated label text
        if (this.displayList == DDCL_DISPLAYTEXTLIST_LABELS)
            // get the label for the checkbox
            sLabel = this.GetLabelForCheckbox(e[i]);
        else
            // get the value for the checkbox
            sLabel = e[i].value;
        
        // add the list separator if necessary
        if (sCurrent != "")
        {
            sCurrent += this.separator;
            sFull += this.separator;
        }

        sFull += sLabel;
        sCurrent += sLabel;

        if (bEllipsisAdded == false)
        {
            // add this one to the text box, then test for the
            // width against the display box
            this.divText.innerHTML = "<nobr>" + sCurrent + "</nobr>";
            if (this.divText.offsetWidth > this.boundingBoxWidth 
                && this.allowExpand == false)
            {
              // too big; shrink by what we can and add the ellipsis 
              // (or other trunacte string)
              while (this.divText.offsetWidth > this.boundingBoxWidth 
                      && sCurrent.length > 0)
              {
                sCurrent = sCurrent.substr(0, sCurrent.length - 1);
                this.divText.innerHTML = "<nobr>" + sCurrent 
                  + this.truncateString + "</nobr>";
              }
              
              // and indicate the ellipsis (or other truncate text) 
              // has been added
              bEllipsisAdded = true;
            }
        }
      }
    }

    // finally, if there are no contents, display the textWhenNone message
    if (this.divText.innerHTML == "")
    {
        if(this.textWhenNone == "")
            this.divText.innerHTML = "&nbsp;";
        else
           this.divText.innerHTML = this.textWhenNone;
    }

    // if we added the ellipsis, set the title attribute to the full string
    // (which will display as a tooltip in most browsers)
    if (bEllipsisAdded)
        this.divDisplayBox.title = sFull;
    else
        this.divDisplayBox.title = "";

}

对于每个选中的框,其标签或值(取决于服务器端属性 DisplayTextList)都会添加到显示文本 <span> 元素中。这会导致 <span>offsetWidth 属性增加,该属性与包含显示文本 <span> 的嵌套元素(<div>)的 offsetWidth 进行比较。在构造函数中,我们之前已将边界框的 offsetWidth 存储在 boundingBoxWidth 属性中。

此脚本还引用了 GetLabelForCheckbox() 函数,该函数用于确定与已渲染的 <input type=”checkbox”> 对应的 <label> 文本,以用于选中的选项。

function DDCL_DropDownCheckList_GetLabelForCheckbox(elem)
 {
   var e = this.divCheckboxes.getElementsByTagName("label");
   for (var i=0; i<e.length; i++)
   {
     if (e[i].htmlFor == elem.id)
     {
        for (var j=0; j<e[i].childNodes.length; j++)
        {
            if (e[i].childNodes[j].nodeType == 3) //text type
            {
                return e[i].childNodes[j].nodeValue;
            }
        }
     }
   }

   // still here?  no <label> for this checkbox then
   return null;
 }

允许单击列表框外部以折叠列表

单击显示框会展开复选框列表。在我创建该控件的第一个版本中,需要再次单击显示框才能将其折叠。这在初步测试时很快就成了令人烦恼的事情,因为我习惯于单击复选框列表旁边的空白区域,期望(下意识地?)列表能自行折叠。也许这只是我的固执,但对于该控件来说,在列表外部单击时自动折叠列表的功能似乎很合适。

为了实现这一点,客户端构造函数向文档添加了一个 click 事件处理程序。它使用 document.attachEvent()(在 IE 中支持)或 document.addEventListener()(在 Mozilla 中支持),以维护任何现有的文档 click 事件处理程序。构造函数中的相关代码如下

function DDCL_DropDownCheckList(...)
{
    . . .

    // if the browser supports bubbling events, install a default click
    // handler for the document too, that will close the checkboxes div
    // if there is a click outside it
    if (document.attachEvent)
    {
        document.attachEvent('onclick'
           , function() { eval("DDCL_HandleDocumentClick('" + id + "');") }
           );
    }
    else if (document.addEventListener)
    {
        document.addEventListener('click'
           , function() { eval("DDCL_HandleDocumentClick('" + id + "');") }
           , false);
    }  

    . . .

}

以这种方式添加到 document 的事件会以冒泡方式触发,这意味着 document 是最后一个处理该事件的对象。如果单击了我们的复选框之一,它将在文档获取事件之前获得处理该事件的机会。这非常有用,因为它使我们能够指示已在复选框 div 中发生了单击,并且 document 处理程序应忽略该单击。

复选框 divclick 处理程序会将 inCheckboxDiv 属性设置为 true。然后,我们的自定义 document.click 处理程序会测试此属性,以确定是否应折叠复选框列表。

function DDCL_HandleDocumentClick(id)
{
   var obj = DDCL_GetObject(id);
   if (obj)
   {
    if (obj.inCheckboxDiv == true)
        obj.inCheckboxDiv = false;
    else
        obj.CloseCheckList(); 
   }
}

使用 Shim 兼容 Internet Explorer

绝对定位在其他控件之上的自定义 <div> 部分会展示 Internet Explorer 中一种相当不幸但记录在案的行为。任何窗口控件,包括普通下拉列表和 ActiveX 控件,都会显示在 <div> 之上,并用它们自己的内容覆盖 <div> 的内容。这发生在不考虑 <div> 中使用的 z-index

Internet Explorer 中的 <select> 框会透过 <div> 显示.

一种常见的解决方法是使用 JavaScript 循环遍历文档中的所有 <select> 对象,将每个对象的 display 设置为 none,或将每个对象的 visibility 设置为 hidden。我对此解决方案从未满意,因为它可能导致最终用户出现一些相当奇怪的显示行为。

幸运的是,Joe King,为 Coalesys, Inc. 工作,开发并发布了另一种解决方案来解决此问题,该解决方案适用于 Internet Explorer 5.5 及更高版本 [1]。该技术使用一个“shim”,形式是一个 <iframe>,其 z-index<div>z-index 小一。窗口控件仍会显示在 <div> 之上,但从 5.5 版开始,绝对定位的 <iframe> 会显示在窗口控件之上。Joe 提出的解决方案是让一个 shim <iframe> 显示在任何窗口控件之上,并将下拉列表 <div> 叠加在该 <iframe> 之上。

如果 DropDownCheckListDropDownMode 设置为 OnTopWithShim,它将渲染这样一个 <iframe>。然后,通过客户端原型方法 OpenCheckList(),shim 将与下拉列表 <div> 一起显示

function DDCL_DropDownCheckList_OpenCheckList()
{
    // open the checkboxlist; first, position it below the displaybox

    // determine the position based on the dropDownMode
    if (this.dropDownMode == DDCL_DROPDOWNMODE_INLINE)
    {
        // inline mode; we're already setup as we need to be
        this.divCheckboxes.style.display = "block";
    }
    else
    {
        // on top modes; position the box
        this.divCheckboxes.style.left = DDCL_findPosX(this.divDisplayBox);
        this.divCheckboxes.style.top  = DDCL_findPosY(this.divDisplayBox) 
          + this.divDisplayBox.offsetHeight;

        this.divCheckboxes.style.display = "block";

        // if we want the shim, apply that now
        if (this.dropDownMode == DDCL_DROPDOWNMODE_ONTOPWITHSHIM)
        {
            this.shim.style.width   = this.divCheckboxes.offsetWidth;
            this.shim.style.height  = this.divCheckboxes.offsetHeight;
            this.shim.style.top     = this.divCheckboxes.style.top;
            this.shim.style.left    = this.divCheckboxes.style.left;
            this.shim.style.zIndex  = this.divCheckboxes.style.zIndex - 1;
            this.shim.style.display = "block";
        }
    }
}

我还想感谢 Peter-Paul Koch [2],感谢他提供的非常实用的 findPosX()findPosY() 函数。

摘要

DropDownCheckList 是一个 ASP.NET 控件,它将复选框作为选项呈现在自定义下拉列表中。虽然服务器端公开了多个用于定义行为和样式的属性,但用户与控件的交互通过客户端 JavaScript 库进行管理。这包括在列表选项的宽度超出显示框时应用省略号或其他文本,以及用户单击列表框外部时自动折叠复选框列表。使用文档事件处理程序来自动折叠,并结合 Internet Explorer 兼容性的 shim 技术,提供了旨在改善自定义下拉列表用户体验的技巧示例。

致谢

  1. Joe King, Coalesys, Inc., 如何用 DHTML 层覆盖 IE 窗口控件(Select Box、ActiveX Object 等)[^]
  2. Peter-Paul Koch[^]
© . All rights reserved.