Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Yet Another ASP.NET Combobox - Part 1

0.00/5 (No votes)
23 Nov 2007 1  
Create ASP.NET Combobox using Javascript, CSS and ASP.NET

Screenshot

Screenshot - combox_screenshot2.gif

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:

  • Javascript
  • CSS
  • HTML
  • VB.NET

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:

Screenshot - combox_UI.gif
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)

// CZ_COMBOX here is merely served the namespace

var CZ_COMBOX = {

    // if onclick event target is not menu or imgArw, then hide the menu

    checkTargetVisiblity: function(e){
        var target = (e && e.target) || (event && event.srcElement); 
        var oOption = document.getElementById("cmbOptions");
        var oImgArw = document.getElementById("cmbImgArrow");
        // if(target == oOption || target == oImgArw)

        if(target == oImgArw)
            oOption.style.visibility = 'visible';
        else
            oOption.style.visibility = 'hidden';  
    },

    // trim white space

    trim: function(txt){
        return txt.replace(/^\s+|\s+$/g,"");
    },

    // get text from the target/source element. 

    // Note, we issue kill event bubble because in this case we need event bubbling from the inner elements.

    getTargetElmTextValue: function(e, obj){
        var trg;
        
        if(!e) e = window.event;
        if(e.target) trg = e.target;
        else if(e.srcElement) trg = e.srcElement;
        // if(trg != this) return;

        
        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 "

    'This call is required by the Web Form Designer.

    <System.Diagnostics.DebuggerStepThrough()> Private Sub InitializeComponent()

    End Sub

    'NOTE: The following placeholder declaration is required by the Web Form Designer.

    'Do not delete or move it.

    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.

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