Thursday, June 11, 2009

Tweaking a DataGridView ComboBoxColumn to allow editing.

Let me first say this. I love the DataGridView and I think it's an awesome and very versatile control. It's highly customizable, and you can create a damn near excel-like application around it. Having said that though, anyone who's ever used a DataGridView knows just how "quirky" the stupid thing can be. Things that look like they should be simple, require clever hacks to implement.

Recently, I was creating an in-house app here at work, in which I needed a DataGridView with a ComboBoxColumn but with one wrinkle. Out of the box, the ComboBoxColumn doesn't support editing. Meaning, it acts like a ComboBox that has the DropDownStyle set to DropDownList (which doesn't allow the user to enter new values). We, however, did need to have the ability for the user to enter new values. I thought that this would be a simple property that I'd be able to set on the ComboBoxColumn. Yea, well there is no such property, so queue clever hack!

The first thing you need to do is hook into the DataGridView's EditingControlShowing event. This event fires when the actual ComboBox is "dropped down". The interesting thing is that the EventArgs has a property on it "Control" that can be cast to a standard WinForms ComboBox.

To demonstrate, I've created a simple Windows App that has a button on it and a DataGridView. It also has a method that just returns a list of strings that contains all the days of the week (needed something just for testing). When the button is clicked, the DataGridView gets populated with a ComboBoxColumn with all the days. Here's the entire code:



using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Windows.Forms;

namespace DataGridViewComboBoxTesting
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
this.dataGridView1.EditingControlShowing += HandleEditShowing;
}

private void HandleEditShowing(object sender, DataGridViewEditingControlShowingEventArgs e)
{
var cbo = e.Control as ComboBox;
if (cbo == null)
{
return;
}

cbo.DropDownStyle = ComboBoxStyle.DropDown;
cbo.Validating -= HandleComboBoxValidating;
cbo.Validating += HandleComboBoxValidating;
}

private void HandleComboBoxValidating(object sender, CancelEventArgs e)
{
var combo = sender as DataGridViewComboBoxEditingControl;
if (combo == null)
{
return;
}
if (!combo.Items.Contains(combo.Text)) //check if item is already in drop down, if not, add it to all
{
var comboColumn = this.dataGridView1.Columns[this.dataGridView1.CurrentCell.ColumnIndex]
as DataGridViewComboBoxColumn;
combo.Items.Add(combo.Text);
comboColumn.Items.Add(combo.Text);
this.dataGridView1.CurrentCell.Value = combo.Text;
}
}

private void button1_Click(object sender, EventArgs e)
{
var cboColumn = new DataGridViewComboBoxColumn
{
Name = "ComboBox",
HeaderText = "Combo Box Column"
};

foreach (var day in this.GetListOfStrings())
{
cboColumn.Items.Add(day);
}

this.dataGridView1.Columns.Add(cboColumn);
}

public IEnumerable<string> GetListOfStrings()
{
foreach (var day in Enum.GetValues(typeof(DayOfWeek)))
{
yield return day.ToString();
}
}
}
}


As you can see, in the HandleEditShowing method, we can get access the the underlying ComboBox itself, and set the DropDownStyle property right there. We then do a little more hackery so that when the user enters a new value, we can add it to all other ComboBox's in that column. We do that by hooking into the ComboBox's Validating event (first we unhook it in case we're already hooked into it, so that our event handler doesn't get called more than once).


private void HandleEditShowing(object sender, DataGridViewEditingControlShowingEventArgs e)
{
var cbo = e.Control as ComboBox;
if (cbo == null)
{
return;
}

cbo.DropDownStyle = ComboBoxStyle.DropDown;
cbo.Validating -= HandleComboBoxValidating;
cbo.Validating += HandleComboBoxValidating;
}


The event handler then looks like this:



private void HandleComboBoxValidating(object sender, CancelEventArgs e)
{
var combo = sender as DataGridViewComboBoxEditingControl;
if (combo == null)
{
return;
}
if (!combo.Items.Contains(combo.Text)) //check if item is already in drop down, if not, add it to all
{
var comboColumn = this.dataGridView1.Columns[this.dataGridView1.CurrentCell.ColumnIndex] as DataGridViewComboBoxColumn;
combo.Items.Add(combo.Text);
comboColumn.Items.Add(combo.Text);
this.dataGridView1.CurrentCell.Value = combo.Text;
}
}


Simple. We just check if it's already in the list, if it's not then add it to the combo box as well as add it to all other combo boxes.

Here's a link to the source code. In the code I also created a class called DataGridViewComboBoxColumnEx which basically inherits from DataGridViewComboBoxColumn and encapsulates all this hackery nicely. You just use it as you normally would but it allows editing.

Source code: http://www.box.net/shared/73bmzmv1r6

6 comments:

Andy Edward Gunawan said...

Hi,

First of all, nice post!

but I just want to ask you, is there any way we can detect when the dropdown is cliked??

I have a datagridview with a comboboxcolumn.

I've managed to get the comboboxcell to allow editing, but I also need to have a form to show when the user click the dropdown.

Is there any way to achieve this as to isolate the event when the user click the dropdown?

A.Friedman said...

In the method HandleEditShowing I cast the e.Control to a regular ComboBox. At that point you can hook into any event that a ComboBox has. So If I'm understanding your question correctly, you want to know when the user clicks and the drop down is shown right? You can do this:

cbo.DropDown -= cbo_DropDown;
cbo.DropDown += cbo_DropDown;

private void cbo_DropDown(object sender, EventArgs e)
{
Console.WriteLine("Drop down");
}

Andy Edward Gunawan said...

Excellent solution!! Thank you

to further the issue,

I have been able to put the value back from the new form (which was shown using the combobox drop down handler) to the original datagridviewcomboboxcell.

which is like this:

comboboxcell.Items.Clear()
comboboxcell.Items.Add(value.ToString)
comboboxcell.Value = comboboxcell.Items(0)

but when I want to edit the value without clicking the dropdown triangle (like edit the text directly), the dropdown handler fired again (which was to show a new form for my case).

I know the reason for this issue is that because I already have an item in that particular comboboxcell, so when i double click to edit it directly, the dropdown event was fired.

So is there anyway to do a direct edit after there's an item in the combobox cell?

A.Friedman said...

Email me the code to a.friedman07 at gmail.com and I'll take a look. I'm not quite sure I understand what it is you're trying to accomplish.

michal said...

Hi, i have a problem with this code. First thanks a lot, nice soution. But when i start typing a word, which is in the list already, there is an autocomplete think, which completes your word .. when i confirm it, there comes an error, or leave an blank cell, nothink in there.
Next think, when i type a new word to each cell in each column, theres not coming an new row. How to solve these problems?

Daniel said...

That works wonderfully.

There is one bug which I am sure I will be able to fix when it comes to my own program. But when you type in the new name and hit enter and it excepts it it does not add the next line like it would when you just hit the drop down and select one that is already in the list.

Thanks for this