Screenshot
Figure 1: Combox Demo Screenshot
Download
combox.zip - 20.1 KB
Introduction
This article demonstrates how to implement a ASP.NET ComboBox control, similar to the Windows ComboBox, from scratch. Majority of programming logics actually residents on the client side since the Javascript does most of the data manipulation. This control was built using the Javascript, CSS and ASP.NET. The goal is to create a versatile web control that behaviors like both Textbox and Dropdown with auto-suggest support (Part 2) and the ability to have calendar, tree, grid, or other custom controls as the menu option. Hence, it was named "Combox".
Files in Combox Control
The zip file contains the following files for this control:
- combox.ascx
Combox control designer user front-end, which contains Javascript, CSS, and HTML - combox.ascx.vb
Combox control ASP.NET code behind
- combox_arrowdown.gif
GIF Image for Combox dropdown
- combox_arrowdown_over.gif
GIF Image for Combox dropdown when mouse over the control
The Code
The Combox is made of four main components:
To achieve maximum flexibility, I choose to use HTML block element DIV rather than the ASP.NET ListBox to host the menu options so that you can include elements such as images or any HTML code as menu items.
HTML Code (File: combox.ascx)
<div id="cmbCtrl">
<div id="cmbMain">
<table cellpadding="0" cellspacing="0" border="0">
<tr>
<td>
<div id="cmbTextField">
<asp:textbox id="txtComboxText" name="txtComboxText"
runat="server" style="border:0px" autocomplete="off" />
</div>
</td>
<td><div id="cmbImgArrow"> </div>
</td>
</tr>
</table>
</div>
<div id="cmbOptions" onclick="CZ_COMBOX.getTargetElmTextValue(event, this);">
<asp:placeholder id="phCmbOptions" runat="server">
<asp:Literal id="litCmbOptions" runat="server" />
</asp:placeholder>
</div>
</div>
In a glance, the above HTML reflects the UI below:
Figure 2: Combox UI
Let's walk through the HTML that creates the Combox user interface. It contains two main DIV blocks, cmbMain and cmbOptions.
The cmbMain contain a HTML table with a single row and two columns. The DIV cmbTextField contains the ASP.NET Textbox control txtComboxText. cmImgArrow will contain the GIF image used as a dropdown arrow for cosmetic purpose. The cmbTextField creates the TextBox portion of the Combox, and the cmbOptions hosts the menu options. Notice the autocomplete feature is set to off so that the it won't interferent with our dropdown. Also notice how each TD is enclosed in <DIV> block so that it can be referenced by CSS, which will be explained later in this article.
The DIV cmbOptions hosts the menu options. Whenever users select a item from the dropdown, the onclick event is trigger and is handled by our javascript getTargetElmTextValue(event, this) event handler method.
We will get to the <asp:placeholder> in the later portion of the article. It's really nothing but a placeholder to host menu items.
Javascript Code (File: combox.ascx)
var CZ_COMBOX = {
checkTargetVisiblity: function(e){
var target = (e && e.target) || (event && event.srcElement);
var oOption = document.getElementById("cmbOptions");
var oImgArw = document.getElementById("cmbImgArrow");
if(target == oImgArw)
oOption.style.visibility = 'visible';
else
oOption.style.visibility = 'hidden';
},
trim: function(txt){
return txt.replace(/^\s+|\s+$/g,"");
},
getTargetElmTextValue: function(e, obj){
var trg;
if(!e) e = window.event;
if(e.target) trg = e.target;
else if(e.srcElement) trg = e.srcElement;
document.forms[0].<%= txtComboxText.ClientID %>
Above Javascript is the core of the Combox control. Hopefully, you are somewhat familiar with Javascript. If you are relatively new to Javascript, I would recommend you read "Create Advanced Web Applications With Object-Oriented Techniques", an excellent crash tutorial on OOP in Javascript. It is also covered in my recent blog post.
CZ_COMBOX is the namespace used to prevent name collision when used together with other controls.
checkTargetVisiblity(e) hides the menu options when the mouse click occurs outside the menu.
trim() is similar to the VB Trim function that removes excessive white spaces around the text.
getTargetElmTextValue(e, obj) returns the text from the target/source element when the menu item is clicked. The control ID is retrieved by using Server Side VB.NET <%= txtComboxText.ClientID %>. When EnableAutoPostBack (covered later) is set to True, the function will also submit the form.
getInnerText(elt) return the innerText (IE) or innerHTML (FireFox/Mozilla) from the target source.
isTextNode(node) determines whether a node is a textnode.
Lastly, document.onclick = CZ_COMBOX.checkTargetVisiblity overrides the global onclick event. So whenever there is a mouse click, the function checkTargetVisibility() will be called.
CSS (File: combox.ascx)
DIV#cmbMain TABLE{
border:1px solid lightgrey;
}
DIV#cmbCtrl INPUT{
height:16px;
font-size:8pt;
margin-left:2px;
}
DIV#cmbImgArrow{
width:17px;
height:20px;
background-image:url('images/combox_arrowdown.gif');
}
DIV#cmbImgArrow:hover{
background-image:url('images/combox_arrowdown_over.gif') ;
}
DIV#cmbOptions{
border:1px solid lightgrey;
width:250px;
width:<%= CType(Width, Integer) + 20 %>px;
background:white;
cursor:pointer;
visibility:hidden;
overflow:auto;
position:absolute;
z-index:1000;
font-size:9pt;
}
DIV#cmbOptions A {
display:block;
height:<%= Height %>px; /* default height is nothing */
text-decoration:none;
white-space;nowrap;
color:#000000;
}
DIV#cmbOptions A:hover {
display:block;
background-color:#FFFFC5;
background-color:<%= HighlightColor%>;
cursor:pointer;
}
DIV#cmbOptions A IMG {
border:0;
}
DIV#cmbTextField input{
width:250px; /* default width */
width:<%= Width %>px;
}
.hideDIV{display:none;}
.showDIV{display:block;}
Okay. You probably are thinking those Javascript isn't too bad until you see the Cascading Style Sheets, or CSS, needed for this control. Nothing to be scared of! While CSS can get extremely hairy and confusing, for our little Combox control it isn't actually too bad. Here is how you read it:
Each style starts with "DIV#<ID NAME>". The text immediately after the pound sign is the name of ID. For example, DIV#cmbMain refers to the block element with ID equals to cmbMain, and DIV#cmbOptions A IMG refers to the IMG tag enclosed with hyperlink inside a block element with ID equals to cmbOptions.
So basically, find each corresponding DIV and walk through each line in order to understand what each line does to each specific element. For example, in DIV#cmbOptions you will notice visibility is set to "hidden" and overflow is set to "auto", so you will know initially the menu options are set to hide. However, once the menu items are shown, they will lay on top of other controls on the page. Simple enough? :)
Code Behind (File: combox.ascx.vb)
Public Class combox
Inherits System.Web.UI.UserControl
Protected WithEvents phCmbOptions As System.Web.UI.WebControls.PlaceHolder
Protected WithEvents litCmbOptions As System.Web.UI.WebControls.Literal
Protected CmbOptionsCollection As New System.Collections.Specialized.NameValueCollection
Private _cmbOptionsTemplate As ITemplate = Nothing
Private _enableAutoPostBack As Boolean
Private _highlightColor As String
Private _width As String
Private _height As String
#Region " Web Form Designer Generated Code "
<System.Diagnostics.DebuggerStepThrough()> Private Sub InitializeComponent()
End Sub
Private designerPlaceholderDeclaration As System.Object
#End Region
<TemplateContainer(GetType(ComboxOptionsContainer))> _
Public Property CmbOptionsTemplate() As ITemplate
Get
Return _cmbOptionsTemplate
End Get
Set(ByVal Value As ITemplate)
_cmbOptionsTemplate = Value
End Set
End Property
Private Sub Page_Init(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Init
InitializeComponent()
If Not (CmbOptionsTemplate Is Nothing) Then
Dim container As New ComboxOptionsContainer
CmbOptionsTemplate.InstantiateIn(container)
phCmbOptions.Controls.Add(container)
End If
End Sub
Public Property EnableAutoPostBack() As Boolean
Get
Return _enableAutoPostBack
End Get
Set(ByVal Value As Boolean)
_enableAutoPostBack = Value
End Set
End Property
Public Property HighlightColor() As String
Get
Return _highlightColor
End Get
Set(ByVal Value As String)
_highlightColor = Value
End Set
End Property
Public Property Width() As String
Get
Return _width
End Get
Set(ByVal Value As String)
_width = Value
End Set
End Property
Public Property Height() As String
Get
Return _height
End Get
Set(ByVal Value As String)
_height = Value
End Set
End Property
Public Sub AddOption(ByVal key As String, ByVal text As String)
CmbOptionsCollection.Add(key, text)
End Sub
Public Sub RemoveOption(ByVal key As String)
CmbOptionsCollection.Remove(key)
End Sub
Private Sub Page_PreRender(ByVal sender As Object, ByVal e As System.EventArgs) Handles MyBase.PreRender
Dim strCmbOptions As New System.Text.StringBuilder
For Each key As String In CmbOptionsCollection
strCmbOptions.Append("<div key=""")
strCmbOptions.Append(key)
strCmbOptions.Append(""">")
strCmbOptions.Append(CmbOptionsCollection(key))
strCmbOptions.Append("</div>")
strCmbOptions.Append(vbCrLf)
Next
litCmbOptions.Text = strCmbOptions.ToString
End Sub
Protected Overrides Sub LoadViewState(ByVal savedState As Object)
End Sub
Protected Overrides Function SaveViewState() As Object
End Function
#Region "Container"
Public Class ComboxOptionsContainer
Inherits Control
Implements INamingContainer
Private _OptKey As String
Private _OptText As String
Public Property OptKey() As String
Get
Return _OptKey
End Get
Set(ByVal Value As String)
_OptKey = Value
End Set
End Property
Public Property OptText() As String
Get
Return _OptText
End Get
Set(ByVal Value As String)
_OptText = Value
End Set
End Property
Public Sub New()
_OptKey = ""
_OptText = ""
End Sub
Public Sub New(ByVal newKey As String, ByVal newText As String)
_OptKey = newKey
_OptText = newText
End Sub
End Class
#End Region
End Class
Surprisingly, the easiest part of the whole implementation is the ASP.NET code behind.
Use the Code
<%@ Page Language="vb" AutoEventWireup="false" Codebehind="test.aspx.vb" Inherits="combox.test"%>
<%@ Register TagPrefix="ps" TagName="Combox" Src="combox.ascx" %>
<html>
<head>
<title>Combox Demo</title>
<style>
.flag
{
position:relative;
top:3px;
margin-right:3px;
}
</style>
</head>
<body>
<form id="Form1" method="post" runat="server">
<ps:Combox id="psCombox" EnableAutoPostBack="False" Width="180" HighlightColor="#AABBEB" runat="server">
<CmbOptionsTemplate>
<div key="USA"><img src="flags/usa.gif" class="flag" />United States</div>
<div key="UK"><img src="flags/uk.gif" class="flag" />United Kingdom</div>
<div key="CAN"><img src="flags/canada.gif" class="flag" />Canada</div>
<div key="CHN"><img src="flags/china.gif" class="flag" />China</div>
<div key="FRN"><img src="flags/france.gif" class="flag" />France</div>
<div key="SWD"><img src="flags/sweden.gif" class="flag" />Sweden</div>
<div key="HK"><img src="flags/hongkong.gif" class="flag" />Hong Kong</div>
<div key="POL"><img src="flags/poland.gif" class="flag" />Poland</div>
</CmbOptionsTemplate>
</ps:combox>
</form>
</body>
</html>
What's Next
The main challenge to create a custom user control like this is that it requires rather broad skills not only in ASP.NET code behind but also HTML, CSS, and especially Javascript, as well as attention to UI Design. I'm not a Javascript expert but it serves mainly as a learning experience and to share what I know, and hopefully learn from my own mistakes and from others.
That's it for now. I will add more details when I got the time to this article.