Я уверен, что может быть более чем один способ сделать это. В приведенном ниже примере у меня есть два DataGridViewComboBoxColumns.
Один для списка «Штаты», а другой для списка «Города». Поле со списком города заполняется в зависимости от выбранного штата. Было минимальное тестирование, и я предполагаю, что другое событие (я) также может быть лучшим подходом. Но это может сработать, в моем тесте сработало без ошибок.
Я предполагаю, что у вас может возникнуть одна проблема, когда код «загружает» данные. Вы должны иметь в виду, как вы сказали, что вы не можете «настроить» каждую ячейку поля со списком, пока не узнаете, какое значение имеет «Состояние» (первое поле со списком). И вы не будете знать, какое «состояние» имеет эта строка, пока ПОСЛЕ загрузки данных.
Учитывая это, должно показаться очевидным, что если вы хотите, чтобы поля со списком были установлены правильно при первоначальной загрузке данных… тогда вам лучше убедиться, что в поле со списком «Штат/Город» есть правильные элементы «Штат/Город». Либо так, либо добавляйте строки в сетку по одной, что не очень хорошая идея по многим причинам.
Кроме того, в отличие от обычного поля со списком, DataGridViewComboBoxCell/Column
— это другой монстр, и он печально известен тем, что выбрасывает DataError
, когда в ячейках со списком сетки установлены недопустимые элементы. Сетка выдает DataError
каждый раз, когда код пытается установить значение ячейки в значение, которое НЕ находится в списке элементов поля со списком/столбца. Пример… когда данные загружены. Когда это происходит, если его не поймать… это событие сбоя приложения, и его следует избегать.
Следовательно, при загрузке данных с использованием DataSource
для сетки ОБЯЗАТЕЛЬНО должен быть «полный» список «Города», поскольку мы не знаем, какой «Штат» находится в какой строке. Таким образом, «полный» список городов решит эту проблему при «загрузке» данных, и мы можем что-то сделать «после» загрузки данных для фильтрации вновь добавленных строк.
Чтобы установить индивидуальное поле со списком, мы просто «фильтруем» исходный список со ВСЕМИ «Городами», чтобы он содержал только «Города» из этого выбранного штата. ЕСЛИ мы «ФИЛЬТРИРУЕМ» исходный список, а не создаем новый, то шансы получить DataError
значительно снижаются.
Другими словами, источник данных столбцов со списком содержит полный список… тогда каждая ячейка поля со списком в этом столбце может быть «подмножеством» исходного списка. Сетка не выдаст ошибку, если отображаются только НЕКОТОРЫЕ элементы в списке, ей важно только то, что их НЕТ в списке.
Другой основной вопрос, который вы не описали, — это «как» организованы данные. Если данные хранятся по-другому, то вам может потребоваться внести некоторые коррективы, однако основная идея все равно должна применяться. Итак, давайте посмотрим, как это будет работать в общих чертах, используя шесть (6) шагов.
Общий подход из шести (6) шагов...
- Во-первых, нужно загрузить данные для обоих полей со списком (данные о штатах и данных о городах). В этом примере есть два
DataTables
под названием «Штаты» и «Города». Эти данные предназначены «строго» для источников данных полей со списком… а не для самой сетки.
- Создайте два (2)
DataGridViewComboBoxColumns
под названием «Штаты» и «Города». Используйте таблицу «Штаты» на шаге 1 в качестве источника данных для поля со списком «Штаты» и, очевидно, таблицу «Города» в качестве источника данных для столбца поля со списком «Города». Кроме того, установите в полях со списком необходимые значения свойств, чтобы сопоставить каждый столбец с источником данных сетки.
- Добавьте столбцы из шага 2 в сетку «ДО» установки источника данных сетки.
- Установите источник данных сетки. При правильной настройке столбцы поля со списком должны соответствовать загруженным данным. Однако в поле со списком городов будут отображаться ВСЕ города.
- После загрузки данных нам нужно вызвать метод, который перебирает каждую строку в сетке и «фильтрует» каждый список со списком «Города» по значениям выбранного состояния.
- После этого это все пользовательский интерфейс. Для этого нам нужно подписаться на несколько событий сетки, чтобы поддерживать «отфильтрованное» состояние каждого поля со списком, например, когда пользователь изменяет значение «Состояние» или добавляет новую строку.
Ниже приведен полный пример. Создайте новое решение winforms, поместите DataGridView
в форму и следуйте инструкциям. У нас должно получиться что-то похожее на…
Для начала давайте посмотрим на образцы данных. Будет два DataTables
под названием «Штаты» и «Города».
Схема «Состояния» будет состоять из двух (2) столбцов…
- уникальный
int
ID «StateID»
string
«Имя состояния»
Таблица «Города» будет иметь три (3) столбца…
- уникальный
int
ID «CityID»
int
«StateID и
- a
string
«Название города»
Пример… Состояние данных с использованием CSV… Я вставил это, чтобы вы могли использовать это для тестовых данных в приведенном ниже коде. Код считывает эти данные из простого CSV-файла.
1,Alaska
2,California
3,Texas
4,Colorado
И данные города с использованием CSV
1,1,Anchorage
2,1,Bethel
3,1,Fairbanks
4,1,Soldotna
5,2,Los Angeles
6,2,San Francisco
7,2,San Diego
8,2,Sacramento
9,3,San Antonio
10,3,Austin
11,3,Dallas
12,3,Houston
13,4,Denver
14,4,Colorado Springs
15,4,Aurora
16,3,El Paso
17,4,Boulder
18,2,Pleasanton
19,3,Lubbock
20,1,Ketchikan
Шаг 1
Используя эти данные, код FillDataSet
прочитает каждый файл «Штаты» и «Города» и создаст DataSet
с двумя таблицами с именами States
и Cities.
Ниже приведен код, который прочитает указанные выше файлы и вернет DataSet
с двумя таблицами.
DataSet StateCityDS;
private void FillDataSet() {
DataTable dt1 = new DataTable();
dt1.TableName = "States";
dt1.Columns.Add("StateID", typeof(int));
dt1.Columns.Add("StateName", typeof(string));
DataTable dt2 = new DataTable();
dt2.TableName = "Cities";
dt2.Columns.Add("CityID", typeof(int));
dt2.Columns.Add("StateID", typeof(int));
dt2.Columns.Add("CityName", typeof(string));
string line;
string[] splitArray;
using (StreamReader sr = new StreamReader(@"D:\Test\CSV\StatesDemo_1.txt")) {
while ((line = sr.ReadLine()) != null) {
splitArray = line.Split(',');
if (splitArray.Length >= 2) {
dt1.Rows.Add(splitArray[0], splitArray[1]);
}
}
}
using (StreamReader sr = new StreamReader(@"D:\Test\CSV\CitiesDemo_1.txt")) {
while ((line = sr.ReadLine()) != null) {
splitArray = line.Split(',');
if (splitArray.Length >= 3) {
dt2.Rows.Add(splitArray[0], splitArray[1], splitArray[2]);
}
}
}
StateCityDS = new DataSet();
StateCityDS.Tables.Add(dt1);
StateCityDS.Tables.Add(dt2);
}
Шаг 2 и 3
Теперь у нас есть некоторые тестовые данные. Затем нам нужен метод для добавления двух (2) DataGridViewComboBoxColumns.
. Ниже следует отметить, что DataPropertyName
каждого столбца настроено на соответствие соответствующему столбцу в таблице источника данных «GRIDS». DataSource
каждого столбца устанавливается в одну из таблиц из возвращенного DataSet
выше. Обратите внимание, что DisplayMember
и ValueMember.
мы хотим отображать название штата/города, и мы хотим, чтобы значение было идентификатором штата/города.
private void AddCascadeComboBoxes() {
DataGridViewComboBoxColumn col1 = new DataGridViewComboBoxColumn();
col1.Name = "States";
col1.HeaderText = "State";
col1.DataSource = StateCityDS.Tables["States"];
col1.DisplayMember = "StateName";
col1.ValueMember = "StateID";
col1.DataPropertyName = "States";
dataGridView1.Columns.Add(col1);
col1 = new DataGridViewComboBoxColumn();
col1.Name = "Cities";
col1.HeaderText = "City";
col1.DataSource = StateCityDS.Tables["Cities"];
col1.DisplayMember = "CityName";
col1.ValueMember = "CityID";
col1.DataPropertyName = "Cities";
dataGridView1.Columns.Add(col1);
}
Это должно позаботиться о столбцах поля со списком. Если мы вызовем FillDataSet
, а затем AddCascadeComboBoxes
из события загрузки форм, в сетке должно быть два столбца поля со списком. В первом будут перечислены четыре (4) штата, а во втором поле со списком будут перечислены ВСЕ города для всех штатов. С этой конфигурацией загрузка данных в сетку теперь должна работать успешно. Итак, давайте создадим несколько тестовых данных для сетки и проверим это.
Шаг 4
Ниже GetGridDataSource
имитирует данные, которые будут загружены в саму сетку. В этом примере это просто DataTable
с тремя столбцами под названием «Имя», «Штаты» и «Города». Каждая строка будет иметь разные имена с разными идентификаторами штата и города. Пример: Штат 1 = Аляска, Город 1 = Анкоридж. Этот метод может выглядеть примерно так…
private DataTable GetGridDataSource() {
DataTable dt = new DataTable();
dt.Columns.Add("Name", typeof(string));
dt.Columns.Add("States", typeof(int));
dt.Columns.Add("Cities", typeof(int));
dt.Rows.Add("John", 1, 1);
dt.Rows.Add("Sally", 2, 5);
dt.Rows.Add("Mary", 3, 10);
dt.Rows.Add("Dean", 4, 13);
dt.Rows.Add("Charlie", 2, 8);
dt.Rows.Add("Nancy", 3, 12);
return dt;
}
Если мы установим указанное выше DataTable
как DataSource
в сетке, то каждый столбец поля со списком должен иметь правильное значение «Штат» и «Город», установленное в соответствии с исходным источником данных. Кроме того, вы должны увидеть дополнительный столбец «Имя», как показано на предыдущем рисунке. Если вы видите дополнительный столбец, например штат или город, с номерами, значит DataGridViewComboBoxColumn
настроен неправильно.
Шаг 5
Похоже, это решает проблему «загрузки», однако, когда мы нажимаем на любое из полей со списком «Город», список не фильтруется, и мы по-прежнему видим все города. Итак, нам нужно добавить шаг 5 из общего описания выше. После загрузки данных нам нужен дополнительный шаг для «фильтрации» каждого поля со списком «Город» в каждой «существующей» строке. Это не должно изменить текущее значение ячейки и относительно прямолинейно… что-то вроде…
private void SetCityComboBoxesDataSource() {
foreach (DataGridViewRow row in dataGridView1.Rows) {
if (!row.IsNewRow) {
DataRowView dr = (DataRowView)row.DataBoundItem;
int stateID = (int)dr["States"];
DataView dv = new DataView(StateCityDS.Tables["Cities"]);
dv.RowFilter = string.Format("StateID = {0}", stateID);
DataGridViewComboBoxCell cityCell = (DataGridViewComboBoxCell)(row.Cells["Cities"]);
cityCell.DataSource = dv;
cityCell.DisplayMember = "CityName";
cityCell.ValueMember = "CityID";
}
}
}
Выше код перебирает каждую строку, берет значение из ячейки «Штаты» и «фильтрует» список городов со списком. Нам нужно вызвать этот код только ОДИН РАЗ после загрузки данных сетки.
Если мы запустим этот код как есть, то поля со списком будут отображаться правильно, и если вы нажмете на поле со списком «Город», он должен отобразить «отфильтрованный» список. Однако это подводит нас только к шагу 5 в общем описании. Все это необходимо для «загрузки» данных без ошибок и фильтрации добавленных строк, однако нам все еще нужна часть пользовательского интерфейса. Данные загружены, и теперь нам нужно подписаться на пару событий, чтобы правильно управлять «фильтрацией» полей со списком, когда пользователь взаимодействует с сеткой (UI).
Шаг 6
Бьюсь об заклад, есть несколько способов сделать это, и может быть лучший подход к этому. Однако в этом примере я подключил (подписался) на два (2) события сетки.
Событие grids CellEndEdit
сработает, когда пользователь закончит «редактирование» ячейки и попытается покинуть ячейку. Когда он сработает, мы проверим, находится ли «отредактированная» ячейка в столбце «Состояние». Если это так, то мы отфильтруем ячейку «Города» в этой строке, чтобы отобразить города этого штата.
private void dataGridView1_CellEndEdit(object sender, DataGridViewCellEventArgs e) {
if (e.RowIndex >= 0 && e.ColumnIndex >= 0) {
if (dataGridView1.Columns[e.ColumnIndex].Name == "States") {
if (dataGridView1.Rows[e.RowIndex].Cells["States"].Value != null) {
if (int.TryParse(dataGridView1.Rows[e.RowIndex].Cells["States"].Value.ToString(), out int stateID)) {
DataView dv = new DataView(StateCityDS.Tables["Cities"]);
dv.RowFilter = string.Format("StateID = {0}", stateID);
DataGridViewComboBoxCell cityCell = (DataGridViewComboBoxCell)(dataGridView1.Rows[e.RowIndex].Cells["Cities"]);
cityCell.Value = DBNull.Value;
cityCell.DataSource = dv;
cityCell.DisplayMember = "CityName";
}
}
}
}
}
Событие grids EditingControlShowing
связано с проверкой того, меняет ли пользователь значение «State», которое уже было выбрано. Это установит для ячейки «Города» для этой строки значение «ноль». Это делается для того, чтобы значение поля со списком «Город» не становилось «недействительным», если пользователь меняет значение «Штат», когда оба значения уже установлены. Без этой проверки и установки ячейки в нулевое значение пользователь мог бы выбрать другое состояние, и город остался бы из предыдущего состояния. Это предотвратит несогласованное состояние данных.
private void dataGridView1_EditingControlShowing(object sender, DataGridViewEditingControlShowingEventArgs e) {
int colIndex = dataGridView1.CurrentCell.ColumnIndex;
int rowIndex = dataGridView1.CurrentCell.RowIndex;
if (dataGridView1.Columns[colIndex].Name == "States") {
dataGridView1.Rows[rowIndex].Cells["Cities"].Value = DBNull.Value;
}
}
Подключение сеток DataError
пригодится при отладке.
private void dataGridView1_DataError(object sender, DataGridViewDataErrorEventArgs e) {
MessageBox.Show("Error: R" + e.RowIndex + "C" + e.ColumnIndex + " --> " + e.Exception.Message);
}
Чтобы собрать все это вместе, ниже приведена остальная часть кода. Обратите внимание, что вам нужно будет сохранить два CSV-файла выше в нужном месте, чтобы получить тестовые данные.
DataSet StateCityDS;
DataTable GridDT;
public Form1() {
InitializeComponent();
}
private void Form1_Load(object sender, EventArgs e) {
GridDT = GetGridDataSource();
FillDataSet();
AddCascadeComboBoxes();
dataGridView1.DataSource = GridDT;
SetCityComboBoxesDataSource();
}
Наконец, следует отметить, что если пользователь выбирает поле со списком «Города» из «новой» строки сетки, то будут отображаться ВСЕ города. Предоставлено, что пользователь «может» выбрать любой город, однако, как только пользователь нажмет на поле со списком «Штат», значение выбранного города будет удалено, и фильтр будет применен.
Особое примечание к шагу 1 сверху и установке начальных значений поля со списком. ЕСЛИ вы правильно настроили поля со списком, И при загрузке данных вы получаете сообщение об ошибке данных о том, что элемент не принадлежит к списку элементов со списком... ТОГДА... гарантируется, что данные содержат что-то, чего нет. в выпадающем списке список элементов.
Если это так, то для отладки и выяснения, где находится оскорбительное значение, вам нужно будет просмотреть данные и проверить каждое из значений поля со списком. Если вы найдете тот, которого нет в списке элементов списка со списком, то это один из элементов-нарушителей, который вызывает ошибку.
Проблема, с которой вы столкнулись бы в этом случае, заключается в том, что делать с оскорбительным значением… вы НЕ можете просто игнорировать его. Вам придется либо удалить эту строку из самих данных, либо добавить этот новый элемент в список элементов со списком. Ядовитая пилюля, с которой нужно как-то справиться.
Извините за длинное сообщение, я надеюсь, что это имеет смысл.
person
JohnG
schedule
20.02.2021
DataPropertyName
? Что это за столбец в конструкторе? - person Jimi   schedule 19.02.2021