Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web / ASP.NET

DropDownCheckList ASP.NET Server Control

4.86/5 (33 votes)
18 Oct 200510 min read 1   9.4K  
Subclass of CheckBoxList that renders as a custom drop-down with behavior defined through client-side JavaScript.

Image 1

Introduction

For the end user, the DropDownCheckList resembles an HTML <select> box that when clicked presents available options as checkboxes. The display box lists the checked options, with developer-defined list separators and the ability to append an ellipsis or other text when the listed options exceed the display width. This article presents the common properties and methods of the control and describes the choice to subclass CheckBoxList both in terms of the benefits gained from this inheritance and essential issues faced as a result. Challenges that were resolved through client-side JavaScript are also presented, including the display of the ellipsis when necessary, and the employing of document click handlers to automatically collapse a list when a click occurs outside it. The latter and a borrowed shim technique are particularly useful when developing custom drop-down web controls.

Using the Control

The DropDownCheckList was designed with a client-side library intended to provide cross-platform compatibility, at least with the modern versions of major browsers. It has been specifically tested with Internet Explorer 6.x, Netscape 7.x and 8.x, and Firefox 1.x. The JavaScript file DropDownCheckList.js contains the client-side object definition for the DropDownCheckList. To use the control, first copy the file DropDownCheckList.js to the following directory:

wwwroot\aspnet_client\UNLV_IAP_WebControls\DropDownCheckList

Several properties are available to customize the display of the control. Use the DisplayBoxCssClass and/or DisplayBoxCssStyle properties to specify CSS styling for the display box. Likewise, DisplayTextCssClass and DisplayTextCssStyle specify the CSS styling for the display text, and CheckListCssClass and CheckListCssStyle denote CSS styling for the checklist box.

The rendering of a drop-down image in the display box is dependent on the DropImageSrc and DropImagePosition properties. If DropImageSrc is blank, or DropImagePosition is set to NoImage, no image is displayed in the drop-down box. If DropImageSrc is specified, the position of the image will either be Left, Right, or Both, as indicated by DropImagePosition.

Image 2

DropDownCheckLists with various CSS styles and drop images

The control’s behavior is defined through several additional properties. The DropDownMode property determines how the checklist will display. If set to Inline, the checklist will expand within the surrounding HTML. If set to OnTop or OnTopWithShim, the checklist will be absolutely positioned on top of other HTML content. The “shim” option exists to offer compatibility with Internet Explorer; see Using a shim for Internet Explorer below for more details.

The Separator property indicates the character(s) used to separate listed items. DisplayTextWidth specifies the maximum width in pixels available in the display box for listing checked options. When the listed options exceed this width, the text is truncated and the TruncateString property is appended. Typically, TruncateString is an ellipsis (…) but may be set to other text or a blank string if desired. If you prefer to allow the display box to expand rather than truncate the text, set DisplayTextWidth to -1.

To indicate the text to be displayed when no options are checked, set the TextWhenNoneChecked property to the desired string. You may also specify whether checkbox labels or values (the Text and Value properties of a child ListItem respectively) are listed in the display box by setting the DisplayTextList property to either Labels or Values.

Image 3

DisplayTextList property set to Values

As a subclass of CheckBoxList, databinding properties such as DataSource and the Items collection are inherited. Upon form submission, the developer may inspect the Items collection for selected checkboxes, just as one would with a CheckBoxList. The DropDownCheckList also exposes two overloaded utility methods: SelectedLabelsToString() and SelectedValuesToString(). Each returns the selected items, listed as a single concatenated string. The overloads allow the developer to specify list separators and text delimiters.

Here is an example of a complete .aspx page which uses the DropDownCheckList control, demonstrating several properties and the two Selected... ToString() methods:

ASP.NET
<%@ 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>

Inheriting from CheckBoxList

There are positive and negative ramifications to subclassing CheckBoxList. On the plus side, our DropDownCheckList inherits the CheckBoxList’s built-in support for databinding, its Items collection, and its repeat layout properties.

On the minus side, we do not have access to individual CheckBox controls, which we need for adding a value attribute (necessary if DisplayTextList is set to Values), and a client-side onclick handler. Simply put, the CheckBoxList does not incorporate CheckBox controls as children, so we lack the ability to manipulate individual boxes as such. We can add an onclick handler in the client-side library, but adding a value attribute to a rendered checkbox is something that has to be managed server-side.

To work within this constraint, yet still take advantage of the CheckBoxList’s databinding and repeat layout rendering, we perform a trick in our overridden Render() method. After producing the necessary HTML for the display box, we render a <div> tag for the checkbox list. If we don’t need to render value attributes, we simply execute the Render() method from the base CheckBoxList against the output stream. If we do need to render values however, then we execute base.Render() against a custom HtmlTextWriter, constructed with a StringWriter instead of the normal output stream. This way, we can retrieve the rendered list as a regular string, and manipulate it to insert our own value attributes. The relevant code within Render() for this is shown below:

C#
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);
    }

      . . . 

}

The method ModifyRenderedCheckboxes() referenced above uses a regular expression to locate each checkbox, and then inserts the appropriate ListItem.Value property as the value attribute.

C#
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;
}

Challenges Resolved in the Client-Side JavaScript

One of the driving goals of this control was to maintain cross-platform compatibility, at least with the recent versions of Internet Explorer (6.x), Netscape (7.x and 8.x), and Firefox (1.x). This goal impacted several aspects of the client-side script.

(A note about Netscape 6.x: The control as presented does not work with Netscape 6 for one important reason. Netscape 6 does not submit form inputs in a <div> with display set to none – so when the checklist is collapsed, its checkbox selections are not submitted along with other form inputs. Developers who wish to support Netscape 6 may wish to experiment with the visibility attribute as a substitute for the display attribute.)

Appending an ellipsis when the display text is too wide

As there may be more checked options to list than width to display them, it is useful to append an indicator to the truncated text. Typically, an ellipsis (…) is used under such conditions.

Internet Explorer supports a text-overflow[^] style attribute, which, appropriately enough, may be set to ellipsis. Unfortunately, this is not standardized and not supported in Firefox and Netscape. Additionally, only an ellipsis is supported by IE – one cannot customize the appended text when the string is truncated.

To ensure support for Firefox and Netscape, and to allow for the customizable TruncateString property, we use the prototype function DisplayCheckedItems() in the client-side script file, defined as follows:

JavaScript
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 = "";

}

For each checked box, its label or value (depending on the server-side property DisplayTextList) is added to the display text <span> element. This causes the <span>’s offsetWidth property to increase, which is compared against the offsetWidth of the bounding element (the <div> within which the display text <span> is nested). In the constructor, we previously stored the bounding box’s offsetWidth in the boundingBoxWidth property.

This script also references the function GetLabelForCheckbox(), which is used to determine the <label> text that corresponds with the rendered <input type=”checkbox”> for a checked option.

JavaScript
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;
 }

Allowing a click outside the checklist to collapse the list

A click in the display box will expand the checklist. In my first incarnation of this control, a second click in the display box was required to collapse it again. This quickly became an annoyance upon initial testing, as I found myself in the habit of clicking in the white space next to the checklist expecting (subconsciously?) the list to collapse on its own. Perhaps this is just my fixation, but it seemed appropriate with this control to add the functionality of automatically collapsing the list upon a click outside the list.

To accomplish this, the client-side constructor adds a click event handler to the document. It uses either document.attachEvent() (supported in IE) or document.addEventListener() (supported in Mozilla) so as to maintain any existing document click event handlers. The relevant code in the constructor follows:

JavaScript
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);
    }  

    . . .

}

An event added to the document in this way will trigger in a bubbling manner, meaning that the document will be the last object to handle the event. If a click takes place in one of our checkboxes, it will get a shot at the event before the document gets it. This is very useful, as it affords us the ability to indicate that a click has occurred in our checkbox div and should be ignored by the document handler.

The click handler for a checkbox div will set the inCheckboxDiv property to true. This is then tested by our custom document.click handler to determine whether or not to collapse the checklist.

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

Using a shim for Internet Explorer

Custom <div> sections that are absolutely positioned on top of other controls demonstrate one rather unfortunate, but well-documented behavior in Internet Explorer. Any windowed controls, including normal drop-downs and ActiveX controls, will show on top of the <div>, covering the <div>’s content with their own. This happens regardless of the z-index used in the <div>.

Image 4

The <select> boxes show through the <div> in Internet Explorer.

One common workaround to this is to use JavaScript to loop through all <select> objects in the document, setting the display of each to none, or the visibility of each to hidden. I was never happy with that solution as it could make for some rather bizarre display behavior from the point of view of the end user.

Fortunately, Joe King, working for Coalesys, Inc., developed and published a different solution to this problem which works for Internet Explorer versions 5.5 and higher [1]. The technique employs a “shim” in the form of an <iframe> with a z-index one less than the z-index of the <div>. Windowed controls will still show on top of a <div>, but as of version 5.5, an <iframe> positioned absolutely will show on top of windowed controls. The solution Joe presented was to have a shim <iframe> display on top of any windowed controls, with the drop-down <div> layered on top of the <iframe>.

The DropDownCheckList will render such an <iframe> if its DropDownMode is set to OnTopWithShim. The shim is then displayed along with the checklist <div> through the client-side prototype method OpenCheckList():

JavaScript
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";
        }
    }
}

I would also like to acknowledge and thank Peter-Paul Koch [2] for his very useful findPosX() and findPosY() functions referenced above.

Summary

The DropDownCheckList is an ASP.NET control that presents checkboxes as options in a custom drop-down. While several properties are exposed server-side for defining behavior and styling, user interaction with the control is managed through a client-side JavaScript library. This includes the application of an ellipsis or other text, as listed choices become too wide for the display box, and the automatic collapsing of the checklist should the user click outside it. The use of document event handlers to automate collapsing, coupled with a shim technique for Internet Explorer compatibility, provide examples of techniques designed to improve the user experience with custom drop-downs.

Acknowledgements

  1. Joe King, Coalesys, Inc., How to cover an IE windowed control (Select Box, ActiveX Object, etc.) with a DHTML layer[^]
  2. Peter-Paul Koch[^]

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here