Version 1.3 - Searchable with fixed width / hidden columns. Supports RightToLeft languages.
Some of the users that I write programs for have to fill out forms that consist of selecting many codes from combo boxes. These codes are fixed. The average user can not enter new codes but must select from a predetermined list of values. The people who do the data entry are very familiar with the codes and are keyboard centric. They don't want to use a mouse each time they enter a value. The people who view the data but don't do a lot of data entry are not as familiar with the codes but want to see the more verbose text descriptions that each code is associated with.
The solution that I was trying to create was a multicolumn
ComboBox that was linked to a read-only
TextBox. When a user selected an item from the
ComboBox, it would display both the code and the description in a multicolumn dropdown list. But when the
ComboBox closed, I wanted to display the code in the
ComboBox, and the description in the
TextBox that was linked to it. The code in the
ComboBox would be bound to the database, and the
TextBox would be unbound and for informational purposes only. This way, both sets of users would be able to see the information that was most important to them.
I also wanted to force the user to only select items that were in the
ComboBox list. The user could not enter any text that was not valid. So this control can operate in one of two modes:
AutoComplete is set to
true, the control behaves like a
ComboBoxStyle.DropDown. The user can type in a value, and it will auto complete based on the associated list of values. If the user types a letter that will cause an invalid code to be entered, that keystroke is suppressed.
AutoComplete is set to
false, the control behaves like a
ComboBoxStyle.DropDownList (even though it actually has a
ComboBoxStyle.DropDown visual style). In this mode, the user can hit a letter or number and the control will cycle through all of the list values that start with that letter or number. But the closed
ComboBox never displays the dividing lines between the columns like a
ListBox. It only shows the code value, no matter how wide the
This article and the control I created would not have been possible if not for the work of Nishant Sivakumar and the MultiColumn ComboBox control that he wrote. It was his work that helped me overcome all the tough obstacles that were stopping me from creating what I needed. Once I found his code, I was able to create a solution in about a day. If you like this control and find it useful, thank him first (and most). Instead of duplicating all his documentation here, I would suggest you read about the control on his page if you want a better understanding of how my control works.
I would also like to acknowledge Laurent Muller since I used his forum suggestion as a basis for my auto-complete functionality.
Using the code
To use this code, all you have to do is select the
MultiColumnComboBox from the toolbox along with a
TextBox and put them on a form.
Then, you can set the following properties:
AutoComplete: True makes it behave like an auto completing dropdown. False makes it behave like a read-only dropdownlist.
AutoDropdown: When you press a key, it will automatically drop down the list so you can see the choices. In
true mode, it will automatically close when you've typed in a valid selection.
BackColorEven: The back color of the even items in the list. The default is white.
BackColorOdd: The back color of the odd items in the list. The default is white. I always use a forecolor of black so I did not code an Odd/Even forecolor property.
LinkedColumnIndex: What is the index of the column that you want the
TextBox to display? The majority of my forms have the
ComboBox display column 0 and the
TextBox display column 1.
LinkedTextBox: Select a
TextBox from the dropdown list of controls on the form. When a
TextBox is linked to the
ComboBox, it is automatically set to
false at design time.
Hitting the Escape key clears the
ComboBox and its associated
Version 1.1 changes
Once I started using the control and became a little more familiar with the code Nishant had written, I realized there were a few changes that I wanted to make in order to have it behave the way I needed it to. So I tweaked it a little:
- The Delete key clears the control and the linked
TextBox in the same way the Escape key did in the original version.
- The Backspace key behaves like a left arrow key. It doesn't remove letters from the code since I wanted to enforce only valid selections. It just moves the caret back a space and repositions the list accordingly.
- The original version iterated through every item that was going to be in the dropdown list and dynamically set the column widths. As another member pointed out in the feedback, this was a problem when linking to objects with many public properties as they all showed up in the list. I decided to make the column width a user specified item so that columns could be hidden by setting the value to zero.
- I added an example to the demo that shows one way to programmatically reference a hidden column of a selected combobox item, in case someone wanted to hide a column but use something like an ID or key value.
Adding these features added two more properties:
ColumnWidthDefault: If a column width is not explicitly declared, this will be the width of the column.
ColumnWidths: A delimited string of column widths. To implement this feature, I copied a Microsoft Access idiom of using a semi-colon delimited string to specify the column widths. A blank column is set to the default.
Here are a few examples:
ColumnWidthDefault = 75
Result: Every column will be displayed with a default width of 75.
Bound to an object with six columns:
ColumnWidthDefault = 75
ColumnWidths = 100;;200
Result: Column0 = 100, Column1 = 75 (default), Column2 = 200; the remaining three columns all default to 75.
Bound to an object with three columns:
ColumnWidthDefault = 75
ColumnWidths = 0;50;150
Result: Column0 is hidden with a width of zero, Column1 = 50, Column2 = 150.
Bound to an object with six columns:
ColumnWidthDefault = 0
ColumnWidths = 50;100
Result: Column0 = 50, Column1 = 100, all the rest are hidden because they are the default value of zero.
Version 1.2 changes
Binding the control to an array of objects that has a large number of properties was causing problems. While a dataset will display its columns in the order they are defined, an object with a lot of public properties would display the properties in the dropdown list in a random order. This made it impossible to use column indexes to set column widths since there was no guarantee that a column would always be tied to a particular index.
To solve this problem, I added a new property so that the programmer could define which columns would be displayed by name and which order they would appear.
ColumnName: A delimited string of column names. The order of the names determines the order of the columns. A blank string shows all the columns. An example would look like this:
A few of the dropdown lists in my program have a lot of codes (several hundred). The majority of the time, the users select a few very common choices that are easily memorized. But when a new user is learning the system or an experienced user needs to locate a rarely used item, some kind of search utility can be helpful.
For this reason, I added a
OpenSearchForm event to the class. When a user hits the <F3> key (the standard Windows search key), this event gets fired. The programmer can then create any kind of search form he likes and tie it to this event. I added an example search form to the demo where the user can enter a search word, hit the Enter key, and the string will be located in the search grid. Double-clicking an item in the grid sets the value in the
ComboBox and closes the form. To test this, the user should go to a
ComboBox that lists all the states, hit <F3>, type in the word "North", and hit the Enter key several times to see it loop through states it finds.
To open the search form in my demo program, I added a single routine:
private void multiColumnComboBox_OpenSearchForm(object sender, EventArgs e)
FormSearch newForm = new FormSearch((MultiColumnComboBox)sender);
I then link the
OpenSearchForm event of each
ComboBox on the form to this single event. The search form that is opened then sets the
DataSource of the search form's
DataGridView to the
DataSource of the passed
ComboBox. I use the
ColumnNameCollection and the
ColumnWidthCollection of the passed
ComboBox to determine which columns to display in the grid.
This search routine worked fine for all of my examples except one. If the data source of a
ComboBox was an array of objects that exposed a lot of properties, the dropdown list would look fine if I used the
ColumnNames property to only display the properties I wanted to see:
But if I hit the <F3> key to open the search form, the list would lose its ordering:
Instead of showing Property0, Property1, Property2, it would show the columns in the random order of the underlying array. Even when I hardcoded the positions, it would still ignore these settings in this one instance. The searching and the double-clicking worked fine, just the columns weren't in the order I wanted them to be. Small objects and
DataSets worked like they should.
The only way I was able to solve this was to have the large class implement an interface that only exposed the properties that should appear in the
public interface ICombo012
public class ComplexObject : ICombo012, ICombo19
ComboBox to the smaller interfaces allowed me to use the larger objects without problems. I do not know if this is the best way to solve this problem, but since this is a learning process for me, I'm sure someone will point out a more elegant solution if one is available. My goal is to post each set of improvements even if it has a few problems, in the hopes that I'll eventually be able to refine this into a control that will be useful and reliable.
Version 1.3 changes
I added support for
RightToLeft languages. If the language is
LeftToRight, I draw the strings in the
OnDrawItem event, starting with column zero and ending with the last column, using the default settings. If the language is
RightToLeft, I draw the columns in reverse order from the highest index down to zero. I also use a
StringFormat object to align the strings in a
This update also exposed a small "flaw" in the earlier versions. When setting the
DropDownWidth in the
OnDropDown event, I didn't calculate the width of the vertical scrollbar if it was present. In a
LeftToRight language, this would truncate trailing characters of the last column, and could be "fixed" by making the last column wider.
RightToLeft languages, the presence of the vertical scroll bar would obscure significant characters of the first (rightmost) column. The only way to fix this was to make sure the width of the vertical scrollbar was also added whenever the
Items.Count was greater than the
Version 1.3.1 changes
I received an email from Onno Sloof in the Netherlands suggesting that adding the following line:
protected override void OnSelectedValueChanged(EventArgs e)
OnSelectedValueChanged event would help with databinding. As per his suggestion, I added this line to version 1.3.1.
- January 31, 2008 - Version 1.3.1 published.
- December 26, 2007 - Version 1.3 published.
- August 24, 2007 - Version 1.2 published.
- August 22, 2007 - Version 1.1 published.
- August 14, 2007 - Article first published.