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.
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.
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.
<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.
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:
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.
<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.
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:
txtName.Text = args.Cells[1].ToString();
You can successively get the values of cells by indexing the Cells
attribute because it uses the ArrayList
class. For example:
args.Cells[0].ToString();
args.Cells[1].ToString();
args.Cells[2].ToString();
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.
<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.
When you click the Post button, you will see that the validation will be in action as shown below:
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.
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.
Controls.Clear()
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
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
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
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:
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:
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.
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
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
.
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.
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
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