
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 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 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.
| You must Sign In to use this message board. |
|
|
 |
|
 |
Hi. Great Control. Thanks for developing it. I use a lot of combo boxes throughout my apps.
I recently downloaded the code from the Code Project. Is there anyway to tell which version of the control that I have?
Thanks again for your work.
Pat
modified on Thursday, November 5, 2009 7:53 PM
|
| Sign In·View Thread·PermaLink | 5.00/5 |
|
|
|
 |
|
 |
I have a DataTable with 2 columns for my MultiColumnComboBox:
ItemID : int : 1,2,3,4,5,6,7,8,9,10,11,12,13,14 ItemName : string
cbxItem.DisplayMember = "ItemID";
When my users enter "1" or "2" or ..."9" in cbxItem is still OK, It automatically selects the item they need
But when they want to select 13 by press 1 then 3
The problem is rised : Item 1 is selecled then item 3 is selected , Not item 13 which my users need .
How can i fix it ?
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
 |
Thanks for sharing your code , an excellent work. I was wondering wether you have any sugesstion on how to add an image/icon to each row? Thanks
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
 |
Sorry. I never investigated what this would take or attempted to try it. So I don't have any insight into a good or easy solution.
If I was going to add this feature I would have to look at how other users had done it and try to merge their code into mine.
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
 |
It seems when I modify a couple different properties, it fires off my SelectedIndexChanged Event. Is this normal? And is there a way to make it not fire off that event?
Ive noticed it when I modify .DataSource and .ValueMember specifically
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
 |
It is possible to programatically bind and unbind from an event. If you did not want an event to fire when you changed a .DataSource or a .ValueMember, you could unbind the event before the change and then bind the event afterward. In other words, toggle the event on/off.
multiColumnComboBox5.SelectedIndexChanged -= new System.EventHandler(this.multiColumnComboBox5_SelectedIndexChanged);
multiColumnComboBox5.DataSource = dataTable5; multiColumnComboBox5.DisplayMember = "Name"; multiColumnComboBox5.ValueMember = "ID"; multiColumnComboBox5.SelectedIndex = -1; multiColumnComboBox5.Text = ""; textBox5A.Text = "";
multiColumnComboBox5.SelectedIndexChanged += new System.EventHandler(this.multiColumnComboBox5_SelectedIndexChanged);
If you look in the Form1.Designer.cs file that is part of the demo you will notice that the line above is automatically created by the properties set in the form designer. You can simply cut-and-paste this line into your code and change the "+=" to a "-=" to undo the binding you set in the designer.
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
 |
even in provided example the numeric keys 5 and 8 does not work. n provided sample, in second combobox on left if you punch the keys 138 from keyboard it doesn't work it only displays 135 even 138 is there in list. same is the case with 5.
Great Job
modified on Sunday, May 10, 2009 8:16 AM
|
| Sign In·View Thread·PermaLink | 2.00/5 |
|
|
|
 |
|
 |
I was not able to reproduce this problem. When I downloaded the example and typed 138, "138" appeared in the combobox, then it auto closed and "Oranges" appeared in the linked textbox. This is the behavior I expected to see based on the way this particular combo was configured. I tried several other numbers and they all seemed to work fine. I tested the other combos and each behaved the way I intended them to in order to illustrate the various configurations possible.
It also worked this way when I ran it from the VS2008 IDE as well.
So I'm not sure why you are seeing different results with your copy. And from my end, it is difficult to fix a problem if I can't replicate it.
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
 |
First of all I really appriciate your work and my intentions are not to hurt your feelings.
Once again I downloaded the demo and tested, it has alternate behaviour with 138. Once it shows 135-"Bananas" and on alternate turn it shows 138-"Oranges". But if I press 135, it always produces the required result and it is always 135-"Bananas". Same is the case when you need to press 345, it produces alternate results 341 and 345 and its repeating. Also the case is same with 127. If you need 127 it produces alternate results 127 and 123, for once it comes 123 and on second turn it becomes 127.
You just need to keep pressing 127 127 127 127 127, and the result will be 127 123 127 123 127. But if you need 123 then its okay all the time.
I am using English United States as input language and US keyboard, But I dont think so that it could be the reason. If this problem is only with me its really surprising for me 
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
 |
i am using your control in a employee lookup form and in this form i can search about an existing name if exist it's ok your control work fine if not i want to be able to write in the combobox and then click add new button to add the non existing name
i wonder if it can be done!
again great work
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
 |
Thanks for the compliment. I'm glad you like the control.
I derived this control from a ComboBox. I then overrode the KeyPress event so the new control would behave exactly the way I wanted it to. You can modify this routine to change the way the control behaves when a key is struck.
For example, if you moved the last line of the routine that reads e.Handled = true; to the top of the routine and then replaced the line e.KeyChar = (char)0; with e.Handled = false; you would allow items that were not in the list to be typed into the box.
You would have to do additional things like remove the code that suppresses the Backspace key if you wanted to allow that functionality. You simply have to modify this one routine to change the way it behaves when the user strikes a key.
The other thing to realize is that I wrote a single KeyPress event as part of my control that applies to ALL copies of the control that are placed on the form. If you want to you can remove this routine from my control and have an individual KeyPress event as part of the form for each separate control. Some could act like my original control. Some could act like a normal ComboBox.
I hope this helps.
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
|
 |
|
 |
Here's what I tried... ValueMember to the customerID field of my datasource and DisplayMember to customerName.
But customerName is not unique, hence the need for a multi-column combobox. But no matter which one is chosen, the first with that name is selected.
I've tried workarounds, like setting DisplayMember to a concatenated query value Name + ID and it seems to work. But in the end for this program, DisplayMember needs to be customerName.
Any suggestions?
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
 |
I was unsure from your post what you meant by saying "customerName is not unique" and "the first with that name is selected".
1) Do you mean the values of customerName are not unique? For example you have the name "John Doe" multiple times in the recordset?
2) Or do you mean the query that loads values into the combobox is a join that has two columns with the same name of "customerName" and the combobox always displays the first column with that name?
If it is the first condition, I tried it with my demo program and a combobox with multiple identical items displayed the correct ID number for each item selected. So it appears to work for me.
If it is the second condition, then giving the duplicate column name an alias (using the "AS" keyword) so it has a unique name would solve that problem:
SELECT a.customerID, a.customerName, b.customerID AS customerID2, b.customerName AS customerName2 FROM tableName1 a INNER JOIN tableName2 b ON a.relatedField = b.relatedField
You could bind to customerName if you wanted to bind to the first field and customerName2 if you wanted to bind to the second.
Since I was uncertain of the nature of your exact problem, I hope one of these answers is helpful. If I misunderstood the problem entirely, then this non-answer may help you formulate a different description of your problem so I can help you solve it.
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
 |
I'been used the control, bound to a datatable. Let's suppose I have the following table with the following data
DataTable [fruits]
fruitsid fruitsname fruitsdescription 1 apple red apple 2 pineapple yellow pineapple 3 etc etc desc.
Then I bound the Combobox with the previous table where Displaymember = fruitsname ValueMember = fruitsid SelectedValue = fruitsid
But when I run the app, for some obscure reason when you try to select an item or write text to find items, it just select the first and never changes, either if you write or select or do whathever the combobox remains with just one item selected.
This is a bug or this is the normal behavior of the control?
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
 |
DisplayMember sets the column that will be displayed visually.
ValueMember sets the column that will be bound to the database field.
SelectedValue either sets or gets the current value of ValueMember.
So for example if you had the following code in your program:
multiColumnComboBox1.DataSource = dataTable1; multiColumnComboBox1.Displaymember = fruitsname; multiColumnComboBox1.ValueMember = fruitsid; multiColumnComboBox1.SelectedValue = 2;
The combobox would be set to fruitsid = 2, would display "pineapple" and store "2" in the database when the record was saved.
The problem with your original settings above is that you have SelectedValue = fruitsid. fruitsid is a columnname or a description of a property. It is not a variable that can be dereferenced to a literal value like 1, 2, 3, etc.
Another way to write the code that may make it more clear is like this:
dataTable1.Columns.Add("fruitsid ", typeof(String)); dataTable1.Columns.Add("fruitsname ", typeof(String)); dataTable1.Columns.Add("fruitsdescription", typeof(String));
multiColumnComboBox1.DataSource = dataTable1; multiColumnComboBox1.Displaymember = fruitsname; multiColumnComboBox1.ValueMember = fruitsid; multiColumnComboBox1.SelectedValue = dataTable1[1]["fruitsid"];
You want SelectedValue to be equal to the value represented by the fruitsid field in the current record in a related dataset.
|
| Sign In·View Thread·PermaLink | 5.00/5 |
|
|
|
 |
 | Headers  Massimo Colurcio | 16:17 18 Dec '08 |
|
 |
Is there anyway to show columns' headers (and maybe let the columns be resizable)?
P.S.: great job, you got my 5
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
 |
When I saw this question, my immediate thought was that column headers should not be that difficult to implement. But after researching it for a while, it appears that this is either not as trivial as I first thought or I've been Googling in the wrong place. I'll keep working on this and see if I can find a solution.
In the programs I write, I always know the values that are going to be displayed in the ComboBox. So fixed-width columns work fine for me. If I was creating a commercial program where the user would get to load values into a table to be displayed, then I would probably want to make my columns size dynamically based on the length of the values in the table.
If you look at the article above, there is a link to the project that I originally based this control on. The author of that control, Nishant Sivakumar, had his columns auto size using the following code:
protected override void OnMeasureItem(MeasureItemEventArgs e) { base.OnMeasureItem(e);
if (DesignMode) return;
for (int colIndex = 0; colIndex < columnNames.Length; colIndex++) { string item = Convert.ToString(FilterItemOnProperty(Items[e.Index], columnNames[colIndex])); SizeF sizeF = e.Graphics.MeasureString(item, Font); columnWidths[colIndex] = Math.Max(columnWidths[colIndex], sizeF.Width); }
float totWidth = CalculateTotalWidth();
e.ItemWidth = (int)totWidth; } The advantage of this code is that it sets your sizing for you. The disadvantage is that it eliminates the hidden columns with a width of zero that mine has because it dynamically sizes every column. I'm sure there are ways that you could combine both methods if you needed to. For example columns with a positive width would be fixed, columns with a zero width would be hidden and columns with a width of -1 would use the equation functions listed above to resize dynamically.
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
|
 |
|
 |
When I want to scroll down by either clicking on the scroll bar or usng the mouse wheel the item move in the wrong direction, can this be solved?
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
 |
I was unable to recreate this problem on my machine.
When I scroll down by clicking the scrollbar arrow, the items move down. Same for the up direction.
I borrowed a USB mouse with a scrollwheel and attached it to my laptop. Again the combobox behaved as expected.
This project has been out here for 10 months and viewed 43k times. Since this is the first time anyone has mentioned this problem, I suspect that this anomaly might be specific to your machine.
Unfortunately this insight does nothing to solve your problem. But if I can't recreate it, it is very difficult to fix it. The class I wrote derived from a standard ComboBox and has no code that should affect scrolling. The only change I made was adjusting the width of the dropdown list if the scrollbar was present.
If you have a simple demo project that displays this behavior, you can email it to me at My First Initial + My Last Name at littlerock.org and I'll be glad to take a look at it and tell you if the problem continues on my machine.
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
 |
Hi, I have created a DataGridViewMultiColumnComboBoxColumn control by using this code with a bit of modification. You can get the working code here: http://cid-6c18e772c1d9e850.skydrive.live.com/browse.aspx/MyDownload
It works for the fisrt time dropdown-click of the combobox. However, for the subsequnce click, the dropdownlist rectangle doesn't fully fill.
I trace the code and notice that in OnDrawItem event,
line 368 : e.Graphics.FillRectangle(brushBackColor, e.Bounds);
the e.Bounds has a different value compared with the first time entry.
Any idea how to refresh or repaint the dropdownlist rectangle for the subsequence click? If you 'lostfocus' the combobox, and click on the combobox again, the dropdownlist rectangle will be fully displayed.
Thanks.
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
 |
Hey michaels,
Sorry I didn't respond to this earlier. I never got an automatic notification from CodeProject that someone had posted a question.
Since this combobox is imbedded into a grid, the solution requires an additional step.
You currently have your column widths set to 55;300 for a total width of the 355 for the dropdown.
You need to go into the properties of Column1 and set DropDownWidth = 355.
So with the grid in addition to setting the widths of the individual columns, you need to set the total for the dropdown as well.
|
| Sign In·View Thread·PermaLink | 2.00/5 |
|
|
|
 |
|
|
 |
|
 |
Hi Darryl
Sorry to be pain, but I have been unable to find the code that Micheals posted. Could you possibly let me have it as would love to use it in a DGV as well?
Thanks in advance
Neil
|
| Sign In·View Thread·PermaLink | 2.00/5 |
|
|
|
 |
|
|