ASP.NET ComboBox

Published: 16 Feb 2007
By: Muhammad Mosa

The ASP.NET ComboBox is an attempt to try and enhance some of the features of the Normal ASP.NET DropDownList.

Introduction

The ASP.NET ComboBox idea is simple; it is a custom composite control that contains two controls (TextBox, Image). The ComboBox Control inherits directly from System.Web.UI.WebControls.ListControl to inherit all design time capabilities; after all it is a ListControl like all other List Controls such as DropDownList, ListBox, RadioButtonList etc.

ASP.NET ComboBox Control Layout

I've chosen a div to align the ASP.NET ComboBox child controls. The control is rendered as a container div and child controls placed within that container. ASP.NET ComboBox is using the base content rendered by its base class ListControl.

Figure 1. ASP.NET ComboBox Layout

Why did I choose ListControl?

The ListControl by default will render <select> HTML tag element. ASP.NET ComboBox will render its selection options as ListBox control which also inherits from the ListControl, but will allow only one option to be selected. As we are going to simulate a ListBox rendering, a property named Rows is added to ASP.NET ComboBox to limit the number of rows inside the list and it will enable scrolling if the number exceeds the Rows. The ListControl has 90% of my needed functionality implemented and comes with a lightweight viewstate. As for styling, I just need the fore color, background color, Font styles and the Css Class. The ListBox doesn't allow border styles, so even if you try to apply them it will not work.

ASP.NET ComboBox Control JavaScript

Initially the control visibility style is hidden. The visibility is toggled to visible when the TextBox or Image is clicked. In other words, when the first row is clicked. When the ListBox itself is clicked while visible of course the selected item text is displayed on the TextBox and the ListBox is hidden again. The same interaction is done when the Enter or Esc keyboard buttons are pressed. Below are the JavaScript functions used to hide and show ListBox:

Listing 1. JavaScript functions to hide and show list of options
function ScsComboBox_ListBoxHide(lstBxWrapper)
{
lstBxWrapper.style.visibility = "hidden";
lstBxWrapper.style.width = "0px";
}

//txtBxWrapper parameter is supposed to be the
//Container Div of the whole control
//lstBoxWrapper parameter is another div
//contained inside the container div
//"txtBxWrapper", this div is where the list of options "ListBox" is rendered
function ScsComboBox_ListBoxShow(txtBxWrapper, lstBxWrapper)
{
var pos = findPos(txtBxWrapper);
var top = pos[1];
var left = pos[0];

lstBxWrapper.style.left = left + 1;
//Just below the TextBox
lstBxWrapper.style.top = top + txtBxWrapper.offsetHeight;
lstBxWrapper.style.visibility = "visible";
lstBxWrapper.all[0].focus(); //set focus on ListBox
lstBxWrapper.all[0].style.width = txtBxWrapper.offsetWidth;
}

Note: All JavaScript functions are available on ScsComboBoxUtil.js file on the source code attached to the article.

ASP.NET ComboBox Support for AutoPostBack

I didn't try to implement handling for PostBack because it is already implemented in the ListBox or TextBox child controls. So I decided to delegate this to the TextBox control. If you enabled the AutoPostBack Property, and change the selection on the ListBox, the TextBox Text property will set programmatically through the JavaScript. However this will not invoke the onchange client event of the TextBox, so I enforced the onchange client event to fire if the Text is changed on the TextBox.

Listing 2. JavaScript to display selected item text on the TextBox
function ScsComboBox_ListBoxDisplaySelectedText(lstBx,textBx)
{
textChanged = false;
if(lstBx.selectedIndex>=0)
{
oldVal = textBx.value;
textBx.value = lstBx.options[lstBx.selectedIndex].text;
if(oldVal != textBx.value) textChanged = true;
}

ScsComboBox_ListBoxHide(lstBx);
textBx.focus();
if(textChanged) textBx.fireEvent("onchange");
//Fire onchange event of the TextBox, if AutoPostBack is
//enabled this will tell the form to PostBack
}
Listing 3. C# code for AutoPostBack overridden property, delegate AutoPostBack to the TextBox Control
public override bool AutoPostBack
{
get
{
EnsureChildControls();
return _txtBox.AutoPostBack;
}
set
{
EnsureChildControls();
_txtBox.AutoPostBack = value;
}
}

Implementing IPostBackDataHandler

It is important to handle the postback data, since this could not be delegated to any child controls; we need to post the selected value as well as the text. Below is the implementation of the IPostBackDataHandler interface.

Listing 4. IPostBackDataHandler implementation
bool IPostBackDataHandler.LoadPostData(string postDataKey, 
NameValueCollection postCollection)
{
if (!base.IsEnabled) return false;
string[] stringArray = postCollection.GetValues(postDataKey);
this.EnsureDataBound();
if (stringArray != null)
{
this.ValidateEvent(postDataKey, stringArray[0]);
int num1 = this.FindByValueInternal(stringArray[0], false);
if (this.SelectedIndex != num1)
{
// Call this to set the selected index of the selected option. // it is already implemented in Base class ListControl base.SetPostDataSelection(num1);
return true;
}
}
return false;
}

void IPostBackDataHandler.RaisePostDataChangedEvent()
{
this.OnSelectedIndexChanged(EventArgs.Empty);
}

Delegate Properties to child controls

As the ListBox and TextBox have most of the properties needed already implemented, I delegated my needed properties to these controls properties. Some properties are handled by the control itself and not delegated, such as the Rows property.

Listing 5. Property delegation
public override int SelectedIndex
{
get
{
int selectedIndex = base.SelectedIndex;
if ((selectedIndex < 0) && (this.Items.Count > 0))
{
this.Items[0].Selected = true;
selectedIndex = 0;
}
return selectedIndex;
}
set
{
base.SelectedIndex = value;
//Cache selected index to be used later on Data Binding Operation. this.cachedSelectedIndex = value;
}
}

Implementing PerformDataBinding method

I've added an additional feature to the ASP.NET ComboBox control, this feature enables you to insert a default option at the top of the ListItems. This is simple in case of a declarative list of options, but will require additional handling incase of data binding. Simply before binding data from the data source to the control, I add the default item if enabled to the top and all binded options will be added afterwards.

Listing 6. PerformDataBinding Implementation
protected override void PerformDataBinding
(System.Collections.IEnumerable dataSource)
{
if (dataSource != null)
{
bool flag1 = false;
bool flag2 = false;
string dataTextField = this.DataTextField;
string dataValueField = this.DataValueField;
string dataTextFormatString = this.DataTextFormatString;
if (!this.AppendDataBoundItems) this.Items.Clear();
if(EnableDefaultItem)//Insert Default Item this.Items.Insert(0, new ListItem(this.DefaultItemText,
this.DefaultItemValue));

ICollection collDataSource = dataSource as ICollection;
if (collDataSource != null)
this.Items.Capacity = collDataSource.Count + this.Items.Count;
if ((dataTextField.Length != 0) || (dataValueField.Length != 0))
flag1 = true;
if (dataTextFormatString.Length != 0) flag2 = true;
foreach (object obj in dataSource)
{
ListItem item = new ListItem();
if (flag1)
{
if (dataTextField.Length > 0)
{
item.Text = DataBinder.GetPropertyValue(obj, dataTextField,
dataTextFormatString);
}
if (dataValueField.Length > 0)
{
item.Value = DataBinder.GetPropertyValue(obj, dataValueField, null);
}
}
else { if (flag2)
{
item.Text = string.Format(CultureInfo.CurrentCulture,
dataTextFormatString, new object[] { obj });
}
else { item.Text = obj.ToString(); } item.Value = obj.ToString(); } this.Items.Add(item);
}
}

if (this.cachedSelectedValue != null)
{
int index = -1;
index = this.FindByValueInternal(this.cachedSelectedValue, true);
if (index == - 1)
{
throw new ArgumentOutOfRangeException("value",
String.Format(_selectionOutOfRange, new object[] { this.ID,
"SelectedValue" }));
}
this.SelectedIndex = index;
this.cachedSelectedValue = null;
this.cachedSelectedIndex = -1;
}
else if (this.cachedSelectedIndex != -1)
{
this.SelectedIndex = this.cachedSelectedIndex;
this.cachedSelectedIndex = -1;
}
}

Note: This implmenetation is almost similar to the implementation written in the base PerformDataBinding method in ListControl.

Control Style Support

I'm not going through this point right now, but I'll provide another article that describes how to support special styles for your controls. Basically we need to support the style for TextBox, Image and ListBox child controls. So the control will contain three properties, TextBoxStyle, ListBoxStyle and ImageStyle. In the next article I'll talk about these three properties.

Web Resources

The ASP.NET ComboBox control contains some JavaScript code, this code is wrapped in a ".js" file. The file is added as an embedded resource in the control assembly and the assembly Meta data, this file is marked as Web Resource file.

[assembly: WebResource("SCS.WebControls.Scripts.ScsComboBoxUtil.js", 
"text/javascript")]

It worth mentioning also that the Image used in the control is stored as Web Resource too. If you do not provide the ImageUrl property, the control will render its default embedded image. This is commonly used in all third party controls to provide consistent look and feel for their controls.

To read a tip about how to register a file as web resource and render it with your control, please read my blog entry: System.Web.UI.WebResourceAttribute Issue

Control Rendering

I have applied control rendering by overriding the AddAttributesToRender and Render methods. I've also made a helper method to prepare the child controls for rendering. You may refer to the attached download file to see how these methods are implemented.

Listing 7. Render Methods
private void PrepareChildControlsForRender()
{
_img.Enabled = this.Enabled;
_txtBox.Enabled = this.Enabled;

//Apply ReadOnly _txtBox.ReadOnly = this.ReadOnly;
string jsNormal1 =
String.Format(_jsNormal1, this.TextBoxStyle.CssClass, _txtBox.ClientID);
string jsHover =
String.Format(_jsHover, TextBoxHoverCssClass, _txtBox.ClientID);
string jsActive =
String.Format(_jsActive, TextBoxActiveCssClass, _txtBox.ClientID);
string jsNormal2 =
String.Format(_jsNormal2, this.TextBoxStyle.CssClass, _txtBox.ClientID);

_txtBox.Attributes["onkeydown"] = "if(event.keyCode != 9)return false;";
_txtBox.Attributes["onmouseover"] = jsHover;
_txtBox.Attributes["onmouseout"] = jsNormal1;
_txtBox.Attributes["onfocus"] = jsActive;
_txtBox.Attributes["onblur"] = jsNormal2;

//Apply Styles if (_txtBxStyle != null) _txtBox.ApplyStyle(_txtBxStyle);

_txtBox.Width = this.Width;

if (_imgStyle != null) _img.ApplyStyle(_imgStyle);

_img.ImageUrl = this.ImageUrl;
_img.ImageAlign = ImageAlign.AbsMiddle;

if (_lstBxStyle != null) this.ApplyStyle(_lstBxStyle);
if (this.SelectedValue != String.Empty) this.Text = this.SelectedItem.Text;
}

protected override void Render(HtmlTextWriter writer)
{
if (this.DesignMode)
{
EnsureChildControls();
}

PrepareChildControlsForRender();
//Render TextBox & Dropdown Image writer.AddAttribute(HtmlTextWriterAttribute.Id, this.ClientID);
writer.AddAttribute(HtmlTextWriterAttribute.Nowrap, "nowrap");
writer.AddStyleAttribute(HtmlTextWriterStyle.Width, "auto");
writer.AddStyleAttribute(HtmlTextWriterStyle.Display, "inline");
//Render onclick event handler for ListBox drop down //if the control is Enabled or not ReadOnly if (this.Enabled && !this.ReadOnly)
writer.AddAttribute(HtmlTextWriterAttribute.Onclick,
ListBoxShowClientFunction);

writer.RenderBeginTag(HtmlTextWriterTag.Div);
_txtBox.RenderControl(writer);
_img.RenderControl(writer);

//Render ListBox Wrapper writer.AddAttribute(HtmlTextWriterAttribute.Id, ListBoxWrapperClientID); writer.AddStyleAttribute(HtmlTextWriterStyle.Position, "absolute");
writer.AddStyleAttribute(HtmlTextWriterStyle.Visibility, "hidden");
writer.AddStyleAttribute(HtmlTextWriterStyle.Top, "-100px");
writer.AddStyleAttribute(HtmlTextWriterStyle.Left, "-100px");
writer.RenderBeginTag(HtmlTextWriterTag.Div);

//Render ListBox AddAttributesToRender(writer); writer.RenderBeginTag(this.TagName);
base.RenderContents(writer);
writer.RenderEndTag();

writer.RenderEndTag();
writer.RenderEndTag();
}

Composite Control Tips

Be sure to create you child control in the CreateChildControls method, and to call EnsureChildControls method before accessing your child controls in any property as shown in the above code, except in the Render method, as in that time all child controls would be created and ready.

Listing 8. Common Implementation for CreateChildControls
protected override void CreateChildControls()
{
Controls.Clear();// Clear previously added controls before add new controls /* ........ Controls creation and initialization */ //Add child controls to Controls collection this.Controls.Add(ctrl1);
this.Controls.Add(ctrl2);
this.Controls.Add(ctrl3);
this.Controls.Add(ctrl4);
}

Implement the INamingContainer interface

Any control that implements this interface creates a new namespace in which all child control ID attributes are guaranteed to be unique within the entire page. It is interesting to know that this is a marker interface that has no methods or properties to implement.

Design Time and Code Editor support Tips

You'll find that some of the inherited properties are not used, or could be replaced with others. It is better to hide such properties from design time and from code editor. To do this, mark these properties with the following attributes.

Listing 9. Design Time and Code Editor Support
//Hide from Designer Property Grid
[Browsable(false)] 
// Hide from VS.NET Code Editor IntelliSense [EditorBrowsable(EditorBrowsableState.Never)] // Not Serialized in Designer Source code "HTML view" [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public override System.Drawing.Color BackColor
{
get{return base.BackColor;}
set{base.BackColor = value;}
}

ASP.NET ComboBox What might not be good

The generated HTML of these controls is not 100% supported with Opera or other browsers, I've tested it with Opera, it is working but sometimes the behavior is not as expected. Also the positioned list box under the text box, sometimes is not 100% accurate. You may find some other issues as it has not tested thorouhly as it should be. I've added it to a production project and it didn't so far present in issues.

Conclusion:

The ASP.NET ComboBox is a simple control that is built to satisfy specific needs. It doesn't include many of the advanced features available in third party controls. It is based on the simple idea to hide the ListBox under the TextBox, and to show it only on demand. It can be enhanced to support auto suggest e.g. you type the first characters of the word and the ListBox automatically selects the first match. It also doesn't support multi column drop downs, which could be another enhancement. For bugs or enhancement, kindly contact me at mohammed_moses@yahoo.co.uk

Downloads

ASP.NET ComboBox Version 1.51
ASP.NET ComboBox Version 1.5
ASP.NET ComboBox Version 1.0

History

  • Version 1.5:
    • Drop down list is only displayed when the drop down arrow is clicked
    • Text box is enabled to enter free text by setting ScsComboBox.EnableFreeText Property
    • Support for key board up and down
    • Support for auto complete by searching the drop down list for match (type text and press tab key)
      Bugs Fixed:
    • Javascript error when placed inside GridView
    • Drop down Image not displayed when placed inside GridView
  • version 1.0:
    • Initial Release

About Muhammad Mosa

Sorry, no bio is available

View complete profile

Top Articles in this category

JavaScript with ASP.NET 2.0 Pages - Part 1
ASP.NET 2.0 has made quite a few enhancements over ASP.NET 1.x in terms of handling common client-side tasks. It has also created new classes, properties and method of working with JavaScript code. This article explores the enhancements and the various ways of injecting JavaScript programmatically into ASP.NET 2.0 pages.

JavaScript with ASP.NET 2.0 Pages - Part 2
ASP.NET provides a number of ways of working with client-side script. This article explores the usage and drawbacks of ASP.NET script callbacks, and briefly presents a bird's view of ASP.NET AJAX.

Upload multiple files using the HtmlInputFile control
In this article, Haissam Abdul Malak will explain how to upload multiple files using several file upload controls. This article will demonstrates how to create a webform with three HtmlInputFile controls which will allow the user to upload three files at a time.

Using WebParts in ASP.Net 2.0
This article describes various aspects of using webparts in asp.net 2.0.

An Architectural View of the ASP.NET MVC Framework
Dino Esposito introduces the ASP.NET MVC framework.

Top
 
 
 

Discussion


Subject Author Date
placeholder Right to left issues Ariel R 6/3/2007 11:39 AM
Firefox compatibility problem Ariel R 6/4/2007 3:19 AM
placeholder FireFox issue Muhammad Mosa 6/4/2007 7:52 AM
RE: FireFox issue Ravindra Thakur 12/24/2007 11:16 AM
placeholder Firefox compatibility problem Ariel R 6/4/2007 9:40 AM