Introduction
I've always wondered why Microsoft, or any else for that matter, has never provided a native HTML ComboBox
element. After some poking around on the web, I decided to take matters into my own hands. I wanted a control that would be visually distinctive from the default HTML Select
element and provide features such as auto complete and list validation. This article will discuss in some detail the View Link Behavior written in JavaScript and the C# code that make up the control.
History
Version 1.0 of this web control was based on the element behavior written by Jeremy Bettis at http://www.deadbeef.com/. While Jeremy's behavior worked very well, it didn't include all the features I wanted in a combobox. This update is a total rewrite of that behavior and addresses the following feature requests and bug fixes.
ListItem
Value Retrieval - The combobox value is now mapped to the ListItem
(Option
) value attribute.
- Auto Validation - You can now specify the combobox to validate text entered by the user against the text in the
ListItem
s.
- Text Field
OnChange
Event - The text field's OnChange
event is bubbled up to the combobox control.
- Z-Indexing - The drop down arrow will no longer over lap other HTML elements.
- DataBinding support in Designer Mode.
- Visual distinction from HTML
Select
element.
- Positioning (Bug Fix) - Combobox now displays correctly when printed or absolutely positioned.
- First Item Selection (Bug Fix) - You can now select the first item in the drop down.
Using the ComboBox Control
Note: This control was developed and tested against IE 6.0. However it is likely IE 5.5 compatible, but will not work with older versions or any non-Microsoft browser.
For all the newbie's out there, read and follow the instructions carefully! I will not respond to any inquiries that are answered in this section.
First, we have to copy the runtime files, consisting of the combobox.htc and images directory, to their default virtual directory, /$wwwroot/webctrl_client/progstudios/1_1/. You can specify a different location by adding a new element to the appSettings
collection in the web.config.
<appSettings>
<add key="PROGWEBCONTORLS_COMMONFILEPATH" value="MyVirtualPath" />
</appSettings>
The binary and project files included in the source are based on the .NET Framework 1.1 and Visual Studio 2003. They are not compatible with 1.0 or VS.NET 2002, so you will have to recompile the assembly on your own. To recompile the project in VS.NET 2002, follow the steps outlined below:
- Create a new C# "Web Control Library" project; call it "ProgStudios.WebControls" or whatever you want.
- Right click on the Project and select Properties.
- Make sure that "Assembly Name" and "Default Namespace" properties are set to "ProgStudios.WebControls".
- Drag and drop the files from the ZIP file onto the project.
- Right click the bitmap image, and in the property window, set the "Build Action" property to "Embedded Resource".
- Right click on the Solution and select Properties. Under "Control Properties/Configuration", make sure the configuration is set to "Release" and press OK.
- Right click on the Project and select "Build", or press "ctrl+shift+b" to build the project.
- The output DLL will be located in the bin/release directory.
- Add the control to your tool box.
- Right click on the tab where you want the control placed and select "Add/Remove Items".
- Click "Browse" and locate the DLL.
- Voila!
To add the assembly to your web project, right click on "References" in the Solution Explorer and locate the DLL by clicking "Browse", or add the project under the "Projects" tab. If you are not using Visual Studio, make sure that you copy the DLL to the bin directory. Once you have the control installed, you can drag and drop the control to the web page in Design mode. The following is the generated HTML code.
The first line registers the assembly and associates the "cc1
" tag prefix to the assembly namespace. The "cc1:combobox
" represents the actual control. The child element asp:ListItem
can be swapped with the traditional <option>
tag.
<%@ 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>
You can manually add new items, or like in the demo project, bind it to a DataView
. Like the HtmlSelect
and DropDown
controls, the ComboBox
can be bound to any instance that implements IListSource
, IList
, or IEnumerable
.
<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"/>
The control also exposes a server side event, ServerChange
. You can attach to the event by hooking-up the OnServerChange
event handler. This gets fired if the value is changed on post back. You can also specify the control to automatically post back by setting the AutoPostBack
attribute to 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">
The AutoValidate
property is used to ensure that the text typed by the user matches that contained in the list items collection. If it does not match, the selectedIndex
is set to -1 and the value is set to an empty string. You can alert an error message once the ComboBox
loses focus, by setting the ErrorMessage
property.
Inside the C# Web Control Code
In order to get some free functionality, I decided to inherit from the WebControl
class. This decision was easy since the base class has an AttributesCollection
class member. This is important since all HTML elements support expando properties, which are basically arbitrary attributes that you can tack onto an element and have it be available through client side script. Any attribute not recognized by the server control will be appended to the attribute collection and rendered to the root element on the client.
The rest of the code is pretty straightforward. Besides the exposing of the corresponding server side properties, methods, and events that are available on the web client, I had to implement certain methods to handle all the plumbing. The control must participate in post backs to handle state, therefore, the IPostBackDataHandler.RaisePostDataChangedEvent
and IPostBackDataHandler.LoadPostData
interface methods are implemented to load the posted 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;
}
...
I also had to override the SaveViewState
and LoadViewState
methods in order to manage ListItem
s on post backs.
...
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;
}
...
Writing the control builder class was also a very straightforward task. I inherited from the ControlBuilder
and overrode the GetChildControlType
method to only allow ListItem
and Option
child elements.
...
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))) {
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);
}
throw new Exception(String.Format(
"Invalid child with tagname \"{0}\"", tagName));
}
The ComboBox Link View Behavior
OK, now we get into the most interesting part of the article. This section will, to some degree, de-mystify what my friend refers as the black art of web development, behavior programming. The current version of the ComboBox
employs the ViewLink feature found in element behaviors, which allows you to encapsulate document fragments in your HTML component. A ViewLink element behavior, referred as master element, also maintains its own document tree. So, this means we can now create elements and style sheet properties using HTML, and manipulate these objects through script in our HTC file with out having to worry about name collisions.
For instance, the ComboBox
defines a text input
field with the ID "textField
". If we were using a traditional element behavior, we'd have to define the element by using the document.createElement
method and then use element.appendChild
method to add it to the primary document tree. However, if we use the View Link feature, we can just use HTML to create and define our element. In addition, what happens if we wanted to use more than one ComboBox
on a page or another element behavior that defines a completely different element with the same ID? We'd have a name collision.
<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>
You'll also notice that I define CSS style classes in the HTC file. The className
properties are swapped in the mouseover
, mouseout
, and click
event handlers. So if you want to modify the look and feel, you'll have to do it here.
Figure 1
|
The master element's dimensions are defined by its content fragments. Dynamically created elements, regardless if it's absolutely positioned, affect and are subject to the confines of these dimensions. In the case with the ComboBox
, we create a drop down consisting of DIV
element containing an HTML table representing the ListItems
collection. Each table cell catches window events such as mouseout
, mouseover
, and click
. When added to the master element's document tree, the new drop down's dimensions are considerably larger than the initial size. As you can see in the figure 1, the ComboBox
is inside an HTML table, and when clicked, the drop down expands the height of the table cell. The right edge of the drop down is also clipped, as the scroll bar is not shown.
As you can see, this isn't going to work. We need to append in the newly created table the primary document tree instead. Doing so allows us to absolutely position and not alter the master element's dimensions. This also brings up another interesting point, the textField
is contained in the master element. When the primary document's form is posted, the textField
data is not sent in the request. If you iterate through the Forms collection you will not find textField
. So in order for the ComboBox
to participate in post backs, we have to create a hidden field and append it to the master element's parent form
.
Finally, I'd like to bring up the options
collection. Since the ComboBox
's drop down is essentially an HTML table and all the state logic is encapsulated in the ComboBox
itself, we really don't need to create a traditional HTML Select
element for rendering purposes. Although ComboBox
is written to work in a server environment, there will be cases where you may want to dynamically append or delete list items from the client. Therefore, we must expose the collection as a public property. We have the option of creating our own collection class and introducing a brand new API, or we can just create an in memory select
element and map its options
collection to the public property. I chose the latter. Since the select
element is in memory and not rendered on the browser, I exposed the repaint()
method that will explicitly redraw the drop down. This method needs to be called after you add or delete items from the client.
Known Issues
- Inline alignment - The
ComboBox
does not properly align with adjacent text. Place the control inside a table cell to get around this problem.