Я боролся с проблемой наличия двух столбцов DataGridViewComboBoxCell в сетке данных. При изменении элемента в столбце 1 в данной строке я хочу, чтобы соответствующая ячейка в столбце 2 изменила элементы выпадающего списка. Ниже я прикрепил очень простой скрипт для такого диалога, см. прикрепленное изображение.
По умолчанию в столбце 2 нет записей. Теперь, когда вы выполните следующую последовательность
col1Changed
и обновит элементы ячейки в столбце 2.Это вызывает следующее исключение:
В DataGridView произошло следующее исключение: System.ArgumentException: значение DataGridViewComboBoxCell недопустимо. Чтобы заменить это диалоговое окно по умолчанию, обработайте событие DataError.
Как я могу избежать этого исключения?
Спасибо
Смотрите мой код для этой проблемы ниже:
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;
}
}
}
Я также получаю исключение с нулевой ссылкой в string selectedItem = ((ComboBox)sender).SelectedItem.ToString();
. Измените его на (string)((ComboBox)sender).SelectedItem;
. И замените X
на Y
здесь .Rows[currentcell.Y]
.
В событии EditingControlShowing
сетки необходимо удалить обработчик события ComboBox.SelectedIndexChanged
, прежде чем добавлять его снова. Или назначьте элемент управления редактированием (ComboBox) полю класса, чтобы удалить обработчик в событии DGV.CellEndEdit
(если оно не равно нулю) и установить для него значение null.
Кроме того, в качестве альтернативы реализуйте событие CellValueChanged
(которое вызывается при изменении выбора CB), если e.ColumnIndex == 0
, получите выбранный Value
, приведите ячейку col2
текущей строки к DataGridViewComboBoxCell
и заполните/привяжите ее Items
/DataSource
соответственно.
Я попытаюсь визуализировать проблему с отображением значений столбца 2 на каждом этапе.
Чтобы решить эту проблему, просто сбросьте значение ячейки во втором столбце, прежде чем изменять элементы поля со списком, установив comboBox.Value = null
. Однако я бы посоветовал вам прочитать о привязке данных. Изменение структур данных вручную может привести к ошибкам.
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];
comboBox.Value = null;
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;
}
}
}
Спасибо, это действительно работает. Обратной стороной является то, что событие col1Changed
инициируется на шаге 3, как только выбирается стрелка раскрывающегося списка и значение в столбце 2 очищается. Это означает, что если вы выберете что-то в ячейке [0, 1], а затем захотите что-то изменить в ячейке [0, 0], ваше значение в ячейке [0, 1] будет сброшено (установлено на ноль), прежде чем вы выберете что-либо в ячейке [0, 1]. ячейка [0, 0]. Я думал, что проблема может быть в привязке, есть ли другой способ добиться того же эффекта с помощью привязки?
Вместо этого привяжитесь к событию CellEndEdit. Это сработает только в том случае, если вы закончите редактирование в ячейке [0,0].
SelectedItem.ToString()
бросает НРЕ. Вместо этого напишите (string)((ComboBox)sender).SelectedItem
. Используйте Y
вместо X
для индекса Rows
: Rows[currentcell.Y]
. Кроме того, Clear();
является общим для всех трех случаев. Переместите его перед if
.
Как можно привязать CellEndEdit
, если событие нельзя передать в ComboBox ComboBox comboBox = e.Control as ComboBox;
? Извините, вместо Y
должен был быть X
.
Событие CellEndEdit
не запускается сразу, когда я меняю выбранный индекс в ячейке [0, 0]
Мое грязное решение проблемы показано ниже для col1Changed
. По сути, я сохраняю текущее значение ячейки во временной переменной, и, если возможно, это значение следует восстановить. Я не на 100% доволен этим, но это лучшее, что я мог придумать.
private void col1Changed(object sender, EventArgs e)
{
var currentcell = dataGridView1.CurrentCellAddress;
if (currentcell.X == col1Idx)
{
// Read the selected item
string selectedItem = (string)((ComboBox)sender).SelectedItem;
// Get the cellfrom column 2
DataGridViewComboBoxCell comboBox = (DataGridViewComboBoxCell)dataGridView1.Rows[currentcell.Y].Cells[col2Idx];
// Store the current value in the cell
string tempValue = (string)dataGridView1.Rows[currentcell.Y].Cells[col2Idx].Value;
comboBox.Value = null;
comboBox.Items.Clear();
if (selectedItem == "1")
{
comboBox.Items.Add("item1");
comboBox.Items.Add("item2");
comboBox.ReadOnly = false;
}
else if (selectedItem == "2")
{
comboBox.Items.Add("item3");
comboBox.Items.Add("item4");
comboBox.ReadOnly = false;
}
else
{
comboBox.ReadOnly = true;
}
if (tempValue != null && comboBox.Items.Contains(tempValue))
{
// Restore the current value in the cell
comboBox.Value = tempValue;
}
else if (comboBox.Items.Count > 0)
{
// Set the cell value to the first item from the list
comboBox.Value = (string)comboBox.Items[0];
}
}
}
Обновлено:
Еще более аккуратное решение — проверить, изменилось ли значение поля со списком, используя следующее: if (selectedItem == (string)dataGridView1.Rows[currentcell.Y].Cells[col1Idx].Value)
private void col1Changed(object sender, EventArgs e)
{
var currentcell = dataGridView1.CurrentCellAddress;
if (currentcell.X == col1Idx)
{
// Read the selected item
string selectedItem = (string)((ComboBox)sender).SelectedItem;
// Get the cell from column 2
DataGridViewComboBoxCell comboBox = (DataGridViewComboBoxCell)dataGridView1.Rows[currentcell.Y].Cells[col2Idx];
// Check if the combobox value has changed
if (selectedItem == (string)dataGridView1.Rows[currentcell.Y].Cells[col1Idx].Value)
return;
comboBox.Value = null;
comboBox.Items.Clear();
if (selectedItem == "1")
{
comboBox.Items.Add("item1");
comboBox.Items.Add("item2");
comboBox.ReadOnly = false;
}
else if (selectedItem == "2")
{
comboBox.Items.Add("item3");
comboBox.Items.Add("item4");
comboBox.ReadOnly = false;
}
else
{
comboBox.ReadOnly = true;
}
if (comboBox.Items.Count > 0)
{
// Set the cell value to the first item from the list
comboBox.Value = (string)comboBox.Items[0];
}
}
}
Мне удалось легко воспроизвести ваше исключение DataError
, и основная причина в том, что при каждом обновлении DataGridView
он пытается синхронизировать отображаемое значение с одним из доступных элементов в поле со списком. Но если вы измените доступные параметры в поле со списком, он больше не сможет найти отображаемое значение и выдаст исключение DataError
.
Насколько я понимаю, вам хотелось бы иметь разные варианты для разных значений индекса, например, вот так:
enum OptionsOne { Select, Bumble, Twinkle, }
enum OptionsTwo { Select, Whisker, Quibble, }
enum OptionsThree { Select, Wobble, Flutter, }
Чтобы решить проблему Refresh
, вы можете поэкспериментировать с использованием привязки для подавления любых изменений значения Option
, если входящее значение не является доступным в данный момент параметром. Вот пример связанного класса записи:
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;
}
Второстепенная проблема заключается в том, что вы можете изменить значение Index
, и теперь выбранное в данный момент значение больше не является допустимым доступным выбором, поэтому другой аспект этой привязки заключается в том, что запись будет сброшена до безопасного «общего» значения Select
, если это произойдет. .
Единственное, что осталось сделать, это убедиться, что доступные варианты выбора отслеживают выбранный элемент. Это можно сделать в обработчике события CurrentCellChanged
, как показано в этой процедуре инициализации:
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>();
}
Еще одна тонкость. В исходном коде изменение выбора CB сделает запись грязной и незафиксированной. Обрабатывая событие CurrentCellDirtyStateChanged
, вы можете сразу же выполнить коммит, если хотите.
Вот код, который я использовал для проверки этого ответа. Клон Конечно, дайте мне знать, если обнаружите какие-либо проблемы с ним.
Это прекрасно, именно то, о чем я просил, огромное спасибо! В своем проекте я использую C# v 7.3, поэтому мне придется заменить некоторые использованные вами хаки, например, ссылочные типы, допускающие значение NULL, но это нормально.
Спасибо за отзыв! Дайте мне знать, если возникнут какие-либо проблемы с переносом на 7.3, и я также хотел сообщить вам о немного более удобном способе сделать это, который пришел мне в голову, поэтому я поместил его в репозиторий как альтернативную ветку с именем bind-when-list-changes
vis-à-vis. вот этот bind-when-cell-selection-changes
. Вопрос, который вы задаете в своем посте, хороший, по моему мнению ▲.
В
dataGridView1_EditingControlShowing
есть переменнаяcolqIdx
без номера. Должно бытьcol1Idx
. (Код не компилируется)