Элемент управления 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)
- Типы данных: Убедитесь, что
ValueMember(справочник) ==DataPropertyName(основная таблица) по типу (int, long, string, Guid). - DataPropertyName: Обязательно заполните это свойство колонки, иначе выбранное значение не запишется обратно в источник данных грида.
- DataSource комбобокса: Установите перед установкой DataSource у самого DataGridView или одновременно, чтобы избежать мигания и ошибок привязки.
- NULL значения: Если колонка в БД nullable, добавьте в DataSource комбобокса «пустую» запись (Value = 0 или DBNull.Value, Display = «— Не выбрано —») или обработайте DataError.
- Производительность: Не создавайте новый DataSource (новый DataTable/List) для каждой строки в CellFormatting. Используйте общий BindingSource с фильтром или кэшированные списки.
- 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 — это ваша страховка от падений приложения при несовпадении данных справочника и основной таблицы.