Version 1.3 - Searchable with fixed width / hidden columns. Supports RightToLeft languages.
Introduction
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
= true
. When 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
= false
. When 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 ComboBox
is.
Background
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 AutoComplete
= 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 ReadOnly
= true
and TabStop
= false
at design time.
Hitting the Escape key clears the ComboBox
and its associated TextBox
.
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
ColumnWidths =
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:
ProductCode;ProductCategory;ProductDescription
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);
newForm.ShowDialog();
}
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 DataSet
s 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 ComboBox
:
public interface ICombo012
{
string Property0
{
get;
}
string Property1
{
get;
}
string Property2
{
get;
}
}
public class ComplexObject : ICombo012, ICombo19
{
}
Binding the 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 RightToLeft
style.
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.
But with 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 MaxDropDownItems
.
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)
{
base.OnSelectedValueChanged(e);
.
.
.
}
to the OnSelectedValueChanged
event would help with databinding. As per his suggestion, I added this line to version 1.3.1.
History
- 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.
I wrote my first program on a Tandy computer using a 1963 black & white Zenith TV for a monitor.
I wrote my second program in Fortran using a card punch machine.
I've been hooked ever since...