I've been struggling with a problem of having two DataGridViewComboBoxCell columns in a datagrid. Upon changing an item in column 1 in given row, I want the corresponding cell in column 2 to change its droplist items. I attached very simple script below for such a dialog, see the attached image.
By default column 2 has no entries. Now when you do the following sequence
col1Changed
event and updates cell items in column 2This triggers the following exception:
The following exception occurred in the DataGridView: System.ArgumentException: DataGridViewComboBoxCell value is not valid. To replace this default dialog please handle the DataError event.
How can I avoid this exception?
Thanks
See my code for this problem below:
void initaliseGrid()
{
string[] itemsCol1 = new string[] { "1", "2", "3" };
DataGridViewComboBoxColumn col1 = new DataGridViewComboBoxColumn();
col1.DropDownWidth = 200;
col1.Width = 200;
col1.MaxDropDownItems = 3;
col1.HeaderText = "col 1";
col1.ReadOnly = false;
dataGridView1.Columns.Insert(0, col1);
col1.Items.AddRange(itemsCol1);
DataGridViewComboBoxColumn col2 = new DataGridViewComboBoxColumn();
col2.DropDownWidth = 200;
col2.Width = 200;
col2.MaxDropDownItems = 3;
col2.HeaderText = "col 2";
col2.ReadOnly = true;
dataGridView1.Columns.Insert(1, col2);
}
int col1Idx = 0;
int col2Idx = 1;
private void dataGridView1_EditingControlShowing(object sender, DataGridViewEditingControlShowingEventArgs e)
{
// Check if the curent cell is combobox
if (dataGridView1.CurrentCell.ColumnIndex == colqIdx && e.Control is ComboBox)
{
ComboBox comboBox = e.Control as ComboBox;
// Add an event when combobox index is changed
comboBox.SelectedIndexChanged += col1Changed;
}
}
private void col1Changed(object sender, EventArgs e)
{
var currentcell = dataGridView1.CurrentCellAddress;
if (currentcell.X == col1Idx)
{
// Read the selected item
string selectedItem = ((ComboBox)sender).SelectedItem.ToString();
// Get the cellfrom column 2
DataGridViewComboBoxCell comboBox = (DataGridViewComboBoxCell)dataGridView1.Rows[currentcell.X].Cells[col2Idx];
if (selectedItem == "1")
{
comboBox.Items.Clear();
comboBox.Items.Add("item1");
comboBox.Items.Add("item2");
comboBox.ReadOnly = false;
}
else if (selectedItem == "2")
{
comboBox.Items.Clear();
comboBox.Items.Add("item3");
comboBox.Items.Add("item4");
comboBox.ReadOnly = false;
}
else
{
comboBox.Items.Clear();
comboBox.ReadOnly = true;
}
}
}
I was able to easily reproduce your DataError
exception and the root cause is that whenever the DataGridView
refreshes, it tries to synchronize the displayed value with one of the available items in the combo box. But if you change the available options in the combo box, it can no longer find the displayed value and throws the DataError
exception.
As I understand it, You'd like to have different options for different index values, something like this for example:
enum OptionsOne { Select, Bumble, Twinkle, }
enum OptionsTwo { Select, Whisker, Quibble, }
enum OptionsThree { Select, Wobble, Flutter, }
To handle the Refresh
issue, you can experiment with using binding to suppress any change to the Option
value if the incoming value isn't a currently available option. Here's an example for the bound record class:
class Record : INotifyPropertyChanged
{
static int _recordCount = 1;
public string Description { get; set; } = $"Record {_recordCount++}";
// By initially setting index to '1' and Available options
// to `OptionsOne` every record is born in sync with itself.
public int Index
{
get => _index;
set
{
if (!Equals(_index, value))
{
_index = value;
switch (Index)
{
case 1: AvailableOptions = Enum.GetNames(typeof(OptionsOne)); break;
case 2: AvailableOptions = Enum.GetNames(typeof(OptionsTwo)); break;
case 3: AvailableOptions = Enum.GetNames(typeof(OptionsThree)); break;
}
if (Option is string currentOption && !AvailableOptions.Contains(currentOption))
{
Option = AvailableOptions[0];
OnPropertyChanged(nameof(Option));
}
OnPropertyChanged();
}
}
}
int _index = 1;
[Browsable(false)]
public string[] AvailableOptions { get; set; } = Enum.GetNames(typeof(OptionsOne));
public string? Option
{
get => _option;
set
{
if (!Equals(_option, value))
{
if (Option is string currentOption && AvailableOptions.Contains(value))
{
_option = value;
OnPropertyChanged();
}
}
}
}
string? _option = $"{OptionsOne.Select}";
public virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null) =>
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
public event PropertyChangedEventHandler? PropertyChanged;
}
A secondary issue is that you might change the Index
value, and now the currently selected value is no longer a valid available choice, so another aspect of this binding is that the record will reset to the safe "common" value of Select
if that occurs.
The only thing that remains to be done is making sure the available selections track the selected item. This can be done in a handler for the CurrentCellChanged
event as shown in this initialization routine:
public partial class MainForm : Form
{
public MainForm() => InitializeComponent();
protected override void OnLoad(EventArgs e)
{
int replaceIndex;
DataGridViewComboBoxColumn cbCol;
base.OnLoad(e);
dataGridView.DataSource = Records;
dataGridView.Columns[nameof(Record.Description)].AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill;
// Index column
cbCol = new DataGridViewComboBoxColumn
{
Name = nameof(Record.Index),
DataSource = new int[] {1,2,3},
DataPropertyName = nameof(Record.Index),
};
replaceIndex = dataGridView.Columns[nameof(Record.Index)].Index;
dataGridView.Columns.RemoveAt(replaceIndex);
dataGridView.Columns.Insert(replaceIndex, cbCol);
// Option column
cbCol = new DataGridViewComboBoxColumn
{
Name = nameof(Record.Option),
DataSource = Enum.GetNames<OptionsOne>(),
DataPropertyName = nameof(Record.Option),
};
replaceIndex = dataGridView.Columns[nameof(Record.Option)].Index;
dataGridView.Columns.RemoveAt(replaceIndex);
dataGridView.Columns.Insert(replaceIndex, cbCol);
dataGridView.CurrentCellChanged += (sender, e) =>
{
var cbCol = ((DataGridViewComboBoxColumn)dataGridView.Columns[nameof(Record.Option)]);
if (dataGridView.CurrentCell is DataGridViewComboBoxCell cbCell && cbCell.ColumnIndex == cbCol.Index)
{
var record = Records[dataGridView.CurrentCell.RowIndex];
cbCell.DataSource = record.AvailableOptions;
}
};
dataGridView.DataError += (sender, e) =>
{
Debug.Fail("We don't expect this to happen anymore!");
};
// Consider 'not' allowing the record to be dirty after a CB select.
dataGridView.CurrentCellDirtyStateChanged += (sender, e) =>
{
if(dataGridView.CurrentCell is DataGridViewComboBoxCell)
{
if (dataGridView.IsCurrentCellDirty)
{
BeginInvoke(()=> dataGridView.EndEdit(DataGridViewDataErrorContexts.Commit));
}
}
};
Records.Add(new Record());
Records.Add(new Record());
Records.Add(new Record());
}
BindingList<Record> Records { get; } = new BindingList<Record>();
}
One more subtlety. In your original code, changing a CB selection is going to make the record dirty and uncommitted. By handling the CurrentCellDirtyStateChanged
event you can go ahead and commit immediately if desired.