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
.
DropDownCheckList
s 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
.
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:
<%@ 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:
protected override void Render(HtmlTextWriter output)
{
. . .
if (_displayTextList == DisplayTextListEnum.Values)
{
StringWriter sw = new StringWriter();
HtmlTextWriter wr = new HtmlTextWriter(sw);
base.Render(wr);
string sHtml = sw.ToString();
wr.Close();
sw.Close();
sHtml = ModifyRenderedCheckboxes(sHtml);
output.Write(sHtml);
}
else
{
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.
protected string ModifyRenderedCheckboxes(string sHtml)
{
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:
function DDCL_DropDownCheckList_DisplayCheckedItems()
{
var sLabel = "";
var sCurrent = "";
var sFull = "";
var sBefore = "";
var sCompText = "";
var bEllipsisAdded = false;
var e = this.divCheckboxes.getElementsByTagName("input");
this.divText.innerHTML = "";
this.divDisplayBox.title = "";
for (var i=0; i<e.length; i++)
{
if (e[i].type == "checkbox" && e[i].checked)
{
if (this.displayList == DDCL_DISPLAYTEXTLIST_LABELS)
sLabel = this.GetLabelForCheckbox(e[i]);
else
sLabel = e[i].value;
if (sCurrent != "")
{
sCurrent += this.separator;
sFull += this.separator;
}
sFull += sLabel;
sCurrent += sLabel;
if (bEllipsisAdded == false)
{
this.divText.innerHTML = "<nobr>" + sCurrent + "</nobr>";
if (this.divText.offsetWidth > this.boundingBoxWidth
&& this.allowExpand == false)
{
while (this.divText.offsetWidth > this.boundingBoxWidth
&& sCurrent.length > 0)
{
sCurrent = sCurrent.substr(0, sCurrent.length - 1);
this.divText.innerHTML = "<nobr>" + sCurrent
+ this.truncateString + "</nobr>";
}
bEllipsisAdded = true;
}
}
}
}
if (this.divText.innerHTML == "")
{
if(this.textWhenNone == "")
this.divText.innerHTML = " ";
else
this.divText.innerHTML = this.textWhenNone;
}
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.
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)
{
return e[i].childNodes[j].nodeValue;
}
}
}
}
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:
function DDCL_DropDownCheckList(...)
{
. . .
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.
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>
.
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()
:
function DDCL_DropDownCheckList_OpenCheckList()
{
if (this.dropDownMode == DDCL_DROPDOWNMODE_INLINE)
{
this.divCheckboxes.style.display = "block";
}
else
{
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 (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
- Joe King, Coalesys, Inc., How to cover an IE windowed control (Select Box, ActiveX Object, etc.) with a DHTML layer[^]
- Peter-Paul Koch[^]