DataGridViewComboBoxColumn создание настройка привязка и типичные проблемы

Учимся создавать крутые комбобоксы в DataGridView: привязка данных, настройка отображения и фиксы частых багов. Таблицы станут удобнее!

Бесплатная консультация юриста

+7 (800) 301-07-30

Реклама jurik.ru

Элемент управления DataGridView, это один из самых мощных и гибких инструментов для отображения табличных данных в Windows Forms. Часто возникает необходимость предоставить пользователю выбор значения из заранее определенного списка прямо в ячейке сетки. Для этого используется специализированный тип колонки — DataGridViewComboBoxColumn.

В этой статье мы подробно разберем создание, настройку, привязку данных и решение типичных проблем при работе с выпадающими списками в DataGridView.


Создание и добавление колонки

Колонку с ComboBox можно добавить двумя способами: через дизайнер (в Visual Studio) или программно. Программный подход дает больший контроль и удобен для динамического формирования интерфейса.

Пример программного создания (C#)

private void AddComboBoxColumn
{
 // 1. Создаем экземпляр колонки
 DataGridViewComboBoxColumn comboColumn = new DataGridViewComboBoxColumn;

 // 2. Базовые настройки внешнего вида и поведения
 comboColumn.HeaderText = "Должность"; // Заголовок колонки
 comboColumn.Name = "PositionColumn"; // Имя для обращения в коде
 comboColumn.DropDownWidth = 200; // Ширина выпадающего списка
 comboColumn.Width = 150; // Ширина самой колонки
 comboColumn.MaxDropDownItems = 5; // Макс. элементов без прокрутки
 comboColumn.FlatStyle = FlatStyle.Flat; // Стиль отображения
 comboColumn.DisplayStyle = DataGridViewComboBoxDisplayStyle.DropDownButton; // Кнопка всегда видна

 // 3. Добавляем колонку в грид (например, в начало)
 dataGridView1.Columns.Insert(0, comboColumn);
}

Аналог на VB.NET:

Private Function CreateComboBoxColumn As DataGridViewComboBoxColumn
 Dim column As New DataGridViewComboBoxColumn With {
 .DataPropertyName = "TitleOfCourtesy",
 .HeaderText = "TitleOfCourtesy",
 .DropDownWidth = 160,
 .Width = 90,
 .MaxDropDownItems = 3,
 .FlatStyle = FlatStyle.Flat
 }
 Return column
End Function

Заполнение данными: Два основных подхода

Существует два принципиально разных способа наполнить ComboBox значениями: статический (через коллекцию Items) и динамический (через DataSource).

Статический список (свойство Items)

Подходит для небольших, неизменяемых списков (статусы, типы, перечисления). Значения будут одинаковыми для всех ячеек данной колонки.

private void FillStaticItems(DataGridViewComboBoxColumn col)
{
 // Очистка на случай повторного вызова
 col.Items.Clear;
 
 // Добавление простых строк
 col.Items.Add("Администратор");
 col.Items.Add("Инженер");
 col.Items.Add("Наблюдатель");
 col.Items.Add("Диктор");
 
 // Можно добавлять объекты любого типа, но тогда нужно переопределить ToString
 // или использовать DisplayMember/ValueMember (см. ниже)
}

Динамическая привязка (свойство DataSource), Рекомендуемый способ

Используется, когда данные приходят из базы данных, списка объектов или DataTable. Это позволяет разделить отображаемое значение (DisplayMember) и хранящееся значение (ValueMember).

Ключевые свойства для привязки:

  • DataSource — источник данных (List<T>, DataTable, BindingSource).
  • DisplayMember — имя свойства/колонки, которое видит пользователь.
  • ValueMember — имя свойства/колонки, значение которого записывается в ячейку грида.
  • DataPropertyName — имя свойства/колонки в основном источнике данных DataGridView (DataSource грида), с которой синхронизируется значение ячейки.
private void BindComboBoxColumn(DataGridViewComboBoxColumn comboColumn, DataTable lookupTable)
{
 // 1. Настраиваем источник данных для выпадающего списка
 comboColumn.DataSource = lookupTable; // Таблица справочника (Id, Name)
 comboColumn.DisplayMember = "Name"; // Что показываем пользователю
 comboColumn.ValueMember = "Id"; // Что сохраняем в ячейку (обычно ID)
 
 // 2. ВАЖНО: Привязываем к колонке основного DataSource грида
 // Предположим, у dataGridView1.DataSource есть колонка "RoleId"
 comboColumn.DataPropertyName = "RoleId"; 
}

Принципиально важно: Тип данных колонки ValueMember в справочнике и тип данных колонки DataPropertyName в основной таблице должны строго совпадать (например, оба int или оба string). Несовпадение типов вызовет ошибку System.FormatException: «DataGridViewComboBoxCell value is not valid».


Типичные ошибки и их решения

Ошибка «DataGridViewComboBoxCell value is not valid»

Возникает, когда значение в ячейке основного источника данных (DataPropertyName) не найдено в списке ValueMember комбобокса.

  • Причина 1: Несовпадение типов (int vs string, Guid vs string).
  • Причина 2: В основных данных есть значение (например, NULL или удаленный ID), которого нет в справочнике (DataSource комбобокса).
  • Решение: Добавьте в справочник запись «Не выбрано» / NULL, либо обработайте событие DataError у DataGridView для подавления исключения.
// Обработка ошибок форматирования/привязки
private void dataGridView1_DataError(object sender, DataGridViewDataErrorEventArgs e)
{
 if (e.Exception is FormatException || e.Exception is ArgumentException)
 {
 // Логируем или игнорируем, чтобы не крашить UI
 e.ThrowException = false; 
 // Можно установить значение по умолчанию
 if (dataGridView1.Columns[e.ColumnIndex] is DataGridViewComboBoxColumn)
 {
 dataGridView1.Rows[e.RowIndex].Cells[e.ColumnIndex].Value = DBNull.Value; // или дефолтный ID
 }
 }
}

Значение не отображается (пустая ячейка), но при клике список работает

Проверьте свойство DataPropertyName. Оно должно точно совпадать с именем колонки/свойства в dataGridView1.DataSource. Регистр букв имеет значение.


Продвинутый сценарий: Разные списки для разных строк

По умолчанию DataGridViewComboBoxColumn использует общий DataSource/Items для всех строк. Если требуется, чтобы в каждой строке был свой уникальный набор значений (например, список профилей конкретного сервера), нужно работать на уровне ячеек (DataGridViewComboBoxCell) в событии CellFormatting или при заполнении строк.

Пример: Установка уникального списка для ячейки

private void dataGridView1_CellFormatting(object sender, DataGridViewCellFormattingEventArgs e)
{
 // Предположим, колонка комбобокса имеет индекс 2
 if (e.ColumnIndex == 2 && e.RowIndex >= 0)
 {
 var grid = (DataGridView)sender;
 // Получаем объект данных текущей строки
 var rowData = grid.Rows[e.RowIndex].DataBoundItem as MyRowModel;
 
 if (rowData != null)
 {
 // Получаем ячейку как ComboBoxCell
 var comboCell = grid.Rows[e.RowIndex].Cells[e.ColumnIndex] as DataGridViewComboBoxCell;
  if (comboCell != null)
 { // Заполняем Items ТОЛЬКО для этой ячейки
 comboCell.Items.Clear;
 foreach (var profile in rowData.AvailableProfiles)
 {
 comboCell.Items.Add(profile.Name); // или сам объект Profile
 }
  // Важно: Если используем объекты, а не строки:
 // comboCell.DisplayMember = "Name";
 // comboCell.ValueMember = "Id"; // Не работает для Items так же, как для DataSource. 
 // Для Items значение ячейки должно совпадать с объектом в списке или его ToString.
 }
 }
 }
}

Важное замечание: При использовании Items.Add(object) свойства DisplayMember и ValueMember у ячейки игнорируются. Значение ячейки (cell.Value) должно быть равно одному из объектов в коллекции Items (по ссылке или по значению Equals). Для сложных сценариев с объектами проще использовать DataSource на уровне колонки, а фильтрацию списка делать через BindingSource.Filter или подмену DataSource в CellFormatting (с осторожностью по производительности).


Отслеживание изменений значения

Стандартное событие CellValueChanged срабатывает только после потери фокуса ячейкой (или нажатия Enter). Чтобы реагировать на выбор в выпадающем списке сразу после клика, используйте комбинацию событий:

// 1. Переводим ячейку в режим редактирования при клике (опционально, для UX)
private void dataGridView1_CellClick(object sender, DataGridViewCellEventArgs e)
{
 if (e.ColumnIndex == comboColumnIndex && e.RowIndex >= 0)
 {
 dataGridView1.BeginEdit(true);
 }}

// 2. Ловим момент завершения редактирования (выбора из списка)
private void dataGridView1_CurrentCellDirtyStateChanged(object sender, EventArgs e)
{
 if (dataGridView1.CurrentCell is DataGridViewComboBoxCell && dataGridView1.IsCurrentCellDirty)
 {
 // Зафиксировать изменение сразу же
 dataGridView1.CommitEdit(DataGridViewDataErrorContexts.Commit);
 }
}

// 3. Теперь CellValueChanged сработает сразу после выбора
private void dataGridView1_CellValueChanged(object sender, DataGridViewCellEventArgs e)
{
 if (e.ColumnIndex == comboColumnIndex && e.RowIndex >= 0)
 {
 var newValue = dataGridView1.Rows[e.RowIndex].Cells[e.ColumnIndex].Value;
 // Ваша логика: сохранение в БД, пересчет итогов и т.д.
 Console.WriteLine($"Новое значение: {newValue}");
 }
}

Чек-лист правильной настройки (Best Practices)

  1. Типы данных: Убедитесь, что ValueMember (справочник) == DataPropertyName (основная таблица) по типу (int, long, string, Guid).
  2. DataPropertyName: Обязательно заполните это свойство колонки, иначе выбранное значение не запишется обратно в источник данных грида.
  3. DataSource комбобокса: Установите перед установкой DataSource у самого DataGridView или одновременно, чтобы избежать мигания и ошибок привязки.
  4. NULL значения: Если колонка в БД nullable, добавьте в DataSource комбобокса «пустую» запись (Value = 0 или DBNull.Value, Display = «— Не выбрано —») или обработайте DataError.
  5. Производительность: Не создавайте новый DataSource (новый DataTable/List) для каждой строки в CellFormatting. Используйте общий BindingSource с фильтром или кэшированные списки.
  6. AutoGenerateColumns = false: Явно объявляйте колонки в коде или дизайнере, чтобы контролировать тип колонки (DataGridViewComboBoxColumn вместо TextBoxColumn).

Полный пример: «От справочника к гриду» (C#)

Сценарий: Есть таблица сотрудников (Employees) и справочник отделов (Departments). Нужно выбрать отдел для сотрудника в гриде.

public class Employee
{
 public int Id { get; set; }
 public string Name { get; set; }
 public int? DepartmentId { get; set; } // Nullable!
}
public class Department
{
 public int Id { get; set; }
 public string Name { get; set; }
}

public partial class MainForm : Form
{
 private BindingList _employees;
 private List _departments;

 public MainForm
 {
 InitializeComponent;
 SetupGrid;
 LoadData;
 }
 private void SetupGrid
 { dataGridView1.AutoGenerateColumns = false;
 dataGridView1.DataError += DataGridView1_DataError; // Важно!

 // Колонка имени (Текст)
 dataGridView1.Columns.Add(new DataGridViewTextBoxColumn 
 { 
 DataPropertyName = "Name", 
 HeaderText = "Сотрудник", 
 Width = 200 
 });

 // Колонка отдела (ComboBox)
 var deptColumn = new DataGridViewComboBoxColumn
 {
 DataPropertyName = "DepartmentId", // Связь с Employee.DepartmentId
 HeaderText = "Отдел",
 Name = "DeptColumn",
 Width = 150,
 DisplayMember = "Name", // Показываем Department.Name
 ValueMember = "Id", // Записываем Department.Id
 FlatStyle = FlatStyle.Flat,
 // Настройка для поддержки NULL (пустого выбора)
 DefaultCellStyle = { NullValue = "— Выберите отдел —" } 
 };
 dataGridView1.Columns.Add(deptColumn);
 }

 private void LoadData
 {
 // 1. Загружаем справочник (обычно из БД)
 _departments = new List
 {
 new Department { Id = 1, Name = "Разработка" },
 new Department { Id = 2, Name = "Тестирование" },
 new Department { Id = 3, Name = "Администрация" }
 };

 // 2. Привязываем справочник к колонке комбобокса
 // Делаем это ДО привязки основных данных
 var comboCol = (DataGridViewComboBoxColumn)dataGridView1.Columns["DeptColumn"];
 comboCol.DataSource = _departments;

 // 3. Добавляем "пустой" элемент для NULL, если нужно
 // Вариант А: Добавляем в список (требует каста DataSource к BindingList или List с Insert)
 // Вариант Б: Используем DataTable и добавляем DataRow с Id=0/DBNull.
 // Здесь для простоты оставим как есть, но обработаем NULL в DataError.

 // 4. Загружаем основные данные
 _employees = new BindingList
 {
 new Employee { Id = 1, Name = "Иванов И.И.", DepartmentId = 1 },
 new Employee { Id = 2, Name = "Петров П.П.", DepartmentId = null }, // Без отдела
 new Employee { Id = 3, Name = "Сидоров С.С.", DepartmentId = 3 }
 };
 dataGridView1.DataSource = _employees;
 }

 private void DataGridView1_DataError(object sender, DataGridViewDataErrorEventArgs e)
 {
 // Если DepartmentId = null, а в комбобоксе нет null -> FormatException
 if (e.Exception is FormatException && e.ColumnIndex == dataGridView1.Columns["DeptColumn"].Index)
 {
 e.ThrowException = false; // Подавляем красный крестик
 // Ячейка покажет пусто или NullValue из DefaultCellStyle
 }
 }
}

Работа с DataGridViewComboBoxColumn в WinForms требует понимания механизма привязки данных (Data Binding). Ключевыми моментами являются правильная настройка пары ValueMember / DataPropertyName (для записи значения) и DisplayMember (для чтения), а также соответствие типов данных.

Использование DataSource предпочтительнее коллекции Items для любых динамических данных. Для решения проблемы «разные списки в разных строках» необходимо оперировать на уровне DataGridViewComboBoxCell в событиях отрисовки или форматирования. Не забывайте подписываться на событие DataError — это ваша страховка от падений приложения при несовпадении данных справочника и основной таблицы.