Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web / ASP.NET

Creating a Multicolumn Combo Box in ASP.NET

4.62/5 (17 votes)
29 Sep 20057 min read 1   6.6K  
Creating a multicolumn combo box in ASP.NET.

Introduction

Previously I came across a requirement to provide a functionality of something similar to a traditional combo box (found in Windows Forms) and a sort of multicolumn combo box in ASP.NET. I searched a lot on the net but found nothing suitable for my requirements. So, I decided to write my own control from scratch and here is what I call a multicolumn combo box. It is a combo box but with multiple columns and a textbox. You can search a particular row by typing the key column value in the textbox.

The snap below shows the working of the multicolumn combo box. The first column is the key column on which we base our search. In this case it is the Item Code. When I type 9 in the textbox it shows all entries having item code starting with digit 9, and when I move my mouse over any item, for example, over an entry having item code 93, then the textbox reflects the current item code. The Search intellisense automatically opens when you type data in the search textbox. You can explicitly open Search intellisense by clicking the button on the right side of the search textbox.

Image 1

Image 2

When the search textbox is empty and you explicitly click the button then all items will be shown as you can see in the snap below. The main objective is that the user can see the data.

Image 3

A Care to be taken! When using

When using the multicolumn combo box, remove z-index of all the controls which lie in the height of the Search intellisense. Care also need to be taken when the DropdownList control lies within the height of the Search intellisense. DropDownList resides always on top irrespective of what z-index you set. That’s how the DropDownList is implemented in Windows. In the sample provided, I simulate the same action. I use two dropdown lists below the multi column combo box and hide them through the ControlsToHide property of the multi column combo box.

HTML
<ControlsToHide>
  <cc1:HiddenControl ControlId="DropDownList1"></cc1:HiddenControl>
  <cc1:HiddenControl ControlId="DropDownList2"></cc1:HiddenControl>
</ControlsToHide>

Thanks to this property, both DropDownList1 and DropDownList2 will be invisible when the multicolumn combo box is shown.

You can set this property using a collection editor which I provided in design mode.

Image 4

You have to do this step only if you have the drop down list within the height. (Text box and other controls are fine. Although, I have provided a generic model, if any control disturbs you, shoot that control out of the game by mentioning its ID!) It’s not a bug but the way the DropdownList control is implemented. I have cured this bug in my control. You can see many sites having this common bug. If you don’t do this step, you will see an output like this:

Image 5

Properties

  • Columns

    The columns to be shown in the multicolumn combo box list. Remember that the key column should be the first column in the multicolumn combo box otherwise the multicolumn combo box will fire a JavaScript error. You can change this behavior by playing with my provided JavaScript source. Moreover, only one key column is allowed. Otherwise an exception of Only one key column is allowed is thrown.

    HTML
    <Columns>
      <cc1:Column HeaderText="Item Code" KeyColumn="True" 
               DataField="PKID" ColumnWidth="10%"></cc1:Column>
      <cc1:Column HeaderText="Item Name" KeyColumn="False" 
               DataField="Name" ColumnWidth="80%"></cc1:Column>
      <cc1:Column HeaderText="UnitCost" KeyColumn="False" 
               DataField="UnitCost" ColumnWidth="10%"></cc1:Column>
    </Columns>

    You can set this property from the Columns property collection editor.

    Image 6

  • AutoPostBack

    This property indicates whether to fire postback when the item in the multicolumn combo box is clicked or not. If this is true it fires an event of RowSelectionChanged which is captured in the aspx page like that:

    In my sample example, I have captured the auto postback event by writing code like this:

    C#
    txtName.Text = args.Cells[1].ToString();//item name column

    You can successively get the values of cells by indexing the Cells attribute because it uses the ArrayList class. For example:

    C#
    args.Cells[0].ToString();//item code
    args.Cells[1].ToString();//item name
    args.Cells[2].ToString();//unit cost
  • ItemMouseOverColor

    The color to use when the mouse moves on the items.

  • GridLinesColor

    The grid lines color of the search grid.

  • DownArrowButtonHeight

    The height of the down arrow button.

  • DownArrowButtonWidth

    The down arrow button width.

  • ValidatorErrorMessage

    The error message to be shown if no data is entered into the search textbox.

  • ValidatorText

    The text of the validator.

  • ValidatorDisplayStyle

    How the validator is rendered on the page.

  • ValidatorEnabled

    If you want to use required field validation on the search text box then enable it by making this true, otherwise false.

  • ValidatorTooltip

    The tool tip to be shown in the validator.

  • ComboBoxListHeight

    The height of the intellisense.

  • ComboBoxListWidth

    The width of the intellisense.

  • TextBoxWidth

    The width of the search data text box.

  • ValidatorCSS

    The CSS class of the required validator.

  • HeaderCSS

    The CSS class of the grid header.

  • ItemsCSS

    The CSS class of grid items.

  • DownArrowButtonCSS

    The CSS class of the down arrow button.

  • TextBoxCSS

    The CSS class of the search data text box.

  • HorizontalScrolling

    Whether to use horizontal scrolling or not.

  • VerticalScrolling

    Whether to use vertical scrolling or not.

  • ControlsToHide

    The control IDs of all those controls which might appear on top of the multicolumn combo box (for example, the DropDownList control). You can specify the IDs of all those controls using this property.

    HTML
    <ControlsToHide>
      <cc1:HiddenControl ControlId="DropDownList1"></cc1:HiddenControl>
      <cc1:HiddenControl ControlId="DropDownList2"></cc1:HiddenControl>
    </ControlsToHide>

Data Validation Support

The multicolumn combo box internally uses the RequiredFieldValidator for validating data. You can enable or disable the validation by setting the validator properties. By default, the validation is disabled.

Image 7

When you click the Post button, you will see that the validation will be in action as shown below:

Image 8

How the multicolumn combo box works

The multicolumn combo box is a composite server control which creates a child server control in its CreateChildControls() method. This method notifies the server control to create any child control contained in it. There are two key tasks that you must perform when implementing a composite control:

  • Override the CreateChildControl method to instantiate child controls, initialize them and add them to the parent's controls hierarchy.
  • Implement the INamingContainer interface, which creates a new naming scope under your control.
VB
Public Class MultiColumnComboBox _
       Inherits System.Web.UI.WebControls.WebControl _
       Implements INamingContainer

Now let’s discuss what is a naming scope? This concept is similar to the concept of namespaces in C++ and C#. For e.g., in the .NET Framework, the DataSet class lives in the System.Data namespace. You can’t access it directly without referring the System.Data namespace. The same rule applies to the multicolumn combo box and all composite server controls. For example, when the multicolumn combo box search text box renders on your browser, it generates the client ID like MultiColumnComboBox1:txtData. This ID tells that txtData lives within the scope of MultiColumnComboBox1 which is the ID of the parent server control.

You don’t need to generate this ID because INamingContainer does this for you automatically. When implementing this interface it creates a new naming scope for server controls and generates a unique ID for all child controls. So if you are using two instances of the multicolumn combo box on your aspx page, you will encounter something like:

MultiColumnComboBox1:txtData
MultiColumnComboBox2:txtData

Try removing the INamingContainer interface and observe its effect.

Now comes the CreateChildControl() method. In it I have created all my child controls. The line Controls.Clear() ensures that multiple copies of child controls are not added to the Controls collection of the server control. The rest is very much self explanatory.

VB
Controls.Clear()

''----Data Text box
With txtData
    .ID = "txtData"

    If Not Me.ViewState("TextBoxWidth") Is Nothing Then
        .Width = Me.ViewState("TextBoxWidth")
    End If

    If Not Me.ViewState("TextBoxCSS") Is Nothing Then
        .CssClass = Me.ViewState("TextBoxCSS")
    End If
End With

''---Down Arrow Button
btnDownArrow = New HtmlInputButton

With btnDownArrow

    .ID = "btnDownArrow"

    If Not Me.ViewState("DownArrowButtonWidth") Is Nothing Then
        .Style("Width") = Me.ViewState("DownArrowButtonWidth").ToString()
    End If

    If Not Me.ViewState("DownArrowButtonHeight") Is Nothing Then
        .Style("height") = Me.ViewState("DownArrowButtonHeight").ToString()
    End If

    If Not Me.ViewState("DownArrowButtonCSS") Is Nothing Then
        .Attributes("class") = Me.ViewState("DownArrowButtonCSS")
    End If
End With

''----Required Field Validator
If Not Me.ViewState("ValidatorEnabled") Is Nothing AndAlso _
    Me.ViewState("ValidatorEnabled") = True Then

    rfData = New RequiredFieldValidator

    With rfData
        .ControlToValidate = txtData.ID

        If Me.ViewState("ValidatorErrorMessage") Is Nothing Then
          .ErrorMessage = "Please Enter Data"
        Else
          .ErrorMessage = Me.ViewState("ValidatorErrorMessage")
        End If

        If Me.ViewState("ValidatorText") Is Nothing Then
          .Text = "*"
        Else
          .Text = Me.ViewState("ValidatorText")
        End If

        If Not Me.ViewState("ValidatorDisplayStyle") Is Nothing Then
          .Display = Me.ViewState("ValidatorDisplayStyle")
        Else
          .Display = ValidatorDisplay.None
        End If

        If Not Me.ViewState("ValidatorCSS") Is Nothing Then
          .CssClass = Me.ViewState("ValidatorCSS")
        End If
     End With
End If

''----Data Grid-----
dgSearch = New DataGrid

With dgSearch
      .ID = "dgSearch"
      .BorderWidth = New Unit(1)
      .GridLines = GridLines.Both
      .Width = New Unit("100%")

      If Not Me.ViewState("HeaderCSS") Is Nothing Then
        .HeaderStyle.CssClass = Me.ViewState("HeaderCSS")
      End If

      If Not Me.ViewState("ItemsCSS") Is Nothing Then
        .ItemStyle.CssClass = Me.ViewState("ItemsCSS")
      End If

      If Not Me.ViewState("GridLinesColor") Is Nothing Then
        .BorderColor = Me.ViewState("GridLinesColor")
      Else
        .BorderColor = Color.Black
      End If
End With

The later part of the code is a wild beast for most of the developers. Now let's eat this beast. I programmatically create dynamic template columns for the ASP.NET DataGrid. Normally we set template columns in design mode but in this case we create dynamic template columns for the ASP.NET DataGrid. When programmatically creating, you should implement the ITemplate interface like this:

VB
Friend Class SearchGridTemplateColumn _
     Implements ITemplate

In the instantiation method I add the link button to the control hierarchy. The next thing is to bind the link button with the data source. For this purpose, I do this:

VB
AddHandler lk.DataBinding, AddressOf DataBind

What I am doing here is setting the event handler of the DataBinding event of the LinkButton. This is called when you write dgSearch.DataBind() which in turn calls the DataBinding events of all child controls.

VB
Public Sub InstantiateIn(ByVal container As System.Web.UI.Control) _
                      Implements System.Web.UI.ITemplate.InstantiateIn

    Dim lk As LinkButton

    Select Case templateType
      Case ListItemType.Header
      Case ListItemType.Footer
      Case ListItemType.Item
          lk = New LinkButton
          AddHandler lk.DataBinding, AddressOf DataBind
          lk.ID = ctrlId
          'lkSelect.CommandName = "Select"
          container.Controls.Add(lk)
    End Select
End Sub

Now comes a more nasty part of the code. Take it another way that I am doing dynamic coding for statements like <%# DataBinder.Eval(Container.DataItem,”MyColumn”)%>. Whenever data is displayed (for example, in a DataGrid control), only one version of each row can be displayed. The displayed row is a DataRowView. In the source code below, I am getting the NamingContainer of the LinkButton with the statement CType(lk.NamingContainer, DataGridItem) which is an instance of DataGridItem. I am rendering the LinkButton in each row of the DataGrid and in the rendering process I need to know about every LinkButton. The variable dgi (dgi is present in the source code below) gives me a single item of the DataGrid and with it I refer an individual item of the data source of DataGrid. The sub statement Row.Item(Me.columnName) actually refers to the value of that row which is rendered in each column of the DataGrid.

VB
Protected Sub DataBind(ByVal sender As Object, ByVal e As EventArgs)
    Dim lk As LinkButton
    lk = CType(sender, LinkButton)
    Dim dgi As DataGridItem
    dgi = CType(lk.NamingContainer, DataGridItem)
    lk.Text = CType(CType(dgi.DataItem, _
              DataRowView).Row.Item(Me.columnName), String)
End Sub

Now we come back to the CreateChildControls() method. Whenever adding template columns programmatically in the DataGrid, always create an instance of the TemplateColumn class.

VB
    Dim enmDataSource As IEnumerator = dtSource.GetList().GetEnumerator()
    Dim enmColumns As IEnumerator = Columns.GetEnumerator
    Dim tc As TemplateColumn
    Dim bc As BoundColumn
    Dim col As Column
    Dim keyColCount As Integer = 0

    While enmColumns.MoveNext
        If keyColCount > 1 Then
            Throw New Exception("Only one key column is allowd")
        End If

        col = CType(enmColumns.Current, Column)
        Dim lkColId As String

        If Not col.KeyColumn Then
            'bc = New BoundColumn
            'bc.DataField = col.DataField
            'bc.HeaderText = col.HeaderText
            'bc.HeaderStyle.Wrap = False
            'If col.ColumnWidth.ToString().Length > 0 Then
                '  bc.ItemStyle.Width = col.ColumnWidth
            'End If

            'dgSearch.Columns.Add(bc)
            lkColId = "lk" & col.DataField

            tc = New TemplateColumn
            tc.ItemTemplate = _
               New SearchGridTemplateColumn(ListItemType.Item, _
               col.DataField, lkColId)
            tc.HeaderText = col.HeaderText
            tc.HeaderStyle.Wrap = False

            If col.ColumnWidth.ToString().Length > 0 Then
                tc.ItemStyle.Width = col.ColumnWidth
            End If

            dgSearch.Columns.Add(tc)
        Else
            tc = New TemplateColumn
            tc.ItemTemplate = _
               New SearchGridTemplateColumn(ListItemType.Item, _
               col.DataField)
            tc.HeaderText = col.HeaderText
            tc.HeaderStyle.Wrap = False

            If col.ColumnWidth.ToString().Length > 0 Then
                tc.ItemStyle.Width = col.ColumnWidth
            End If

            dgSearch.Columns.Add(tc)
            keyColCount += 1
        End If
    End While

    dgSearch.AutoGenerateColumns = False
    dgSearch.DataSource = dtSource
    dgSearch.DataBind()
End If

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