Как реализовать каскадные ComboBox в элементе управления DataGridView?

Мне нужно реализовать каскадирование ComboBoxes за несколько DataGridView. В качестве доказательства концепции я собрал код ниже. 3 столбца (Клиент, Страна, Город). При выборе Страна Город должен заполнить, но это не работает.

Есть ли лучший способ добиться этого и исправить то, что я делаю неправильно?

public partial class Form1 : Form
{
    private List<Customer> customers;
    private List<Country> countries;
    private List<City> cities;
    private ComboBox cboCountry;
    private ComboBox cboCity;
    public Form1()
    {
        InitializeComponent();
        countries = GetCountries();
        customers = GetCustomers();

        SetupDataGridView();

    }

    private List<Customer> GetCustomers()
    {
        var customerList = new List<Customer>
                          {
                              new Customer {Id=1,Name = "Jo",Surname = "Smith"},
                              new Customer {Id=2,Name = "Mary",Surname = "Glog"},
                              new Customer {Id=3,Name = "Mark",Surname = "Bloggs"}
                          };

        return customerList;
    }

    private List<Country> GetCountries()
    {
        var countryList = new List<Country>
                          {
                              new Country {Id=1,Name = "England"},
                              new Country {Id=2,Name = "Spain"},
                              new Country {Id=3,Name = "Germany"}
                          };

        return countryList;
    }
    private List<City> GetCities(string countryName)
    {
        var cityList = new List<City>();
        if (countryName == "England") cityList.Add(new City { Id = 1, Name = "London" });
        if (countryName == "Spain") cityList.Add(new City { Id = 2, Name = "Madrid" });
        if (countryName == "Germany") cityList.Add(new City { Id = 3, Name = "Berlin" });

        return cityList;
    }

    private void SetupDataGridView()
    {
        dataGridView1.CellLeave += dataGridView1_CellLeave;
        dataGridView1.EditingControlShowing += dataGridView1_EditingControlShowing;

        DataGridViewTextBoxColumn colCustomer = new DataGridViewTextBoxColumn();
        colCustomer.Name = "colCustomer";
        colCustomer.HeaderText = "CustomerName";

        DataGridViewComboBoxColumn colCountry = new DataGridViewComboBoxColumn();
        colCountry.Name = "colCountry";
        colCountry.HeaderText = "Country";


        DataGridViewComboBoxColumn colCity = new DataGridViewComboBoxColumn();
        colCity.Name = "colCity";
        colCity.HeaderText = "City";


        dataGridView1.Columns.Add(colCustomer);
        dataGridView1.Columns.Add(colCountry);
        dataGridView1.Columns.Add(colCity);


        //Databind gridview columns
        ((DataGridViewComboBoxColumn)dataGridView1.Columns["colCountry"]).DisplayMember = "Name";
        ((DataGridViewComboBoxColumn)dataGridView1.Columns["colCountry"]).ValueMember = "Id";
        ((DataGridViewComboBoxColumn)dataGridView1.Columns["colCountry"]).DataSource = countries;

        ((DataGridViewComboBoxColumn)dataGridView1.Columns["colCity"]).DisplayMember = "Name";
        ((DataGridViewComboBoxColumn)dataGridView1.Columns["colCity"]).ValueMember = "Id";
        ((DataGridViewComboBoxColumn)dataGridView1.Columns["colCity"]).DataSource = cities;

        foreach (Customer cust in customers)
        {
            dataGridView1.Rows.Add(cust.Name + " " + cust.Surname);
        }
    }

    private void dataGridView1_EditingControlShowing(object sender, DataGridViewEditingControlShowingEventArgs e)
    {
        //register a event to filter displaying value of items column.
        if (dataGridView1.CurrentRow != null && dataGridView1.CurrentCell.ColumnIndex == 2)
        {
            cboCity = e.Control as ComboBox;
            if (cboCity != null)
            {
                cboCity.DropDown += cboCity_DropDown;
            }
        }

        //Register SelectedValueChanged event and reset item comboBox to default if category changes
        if (dataGridView1.CurrentRow != null && dataGridView1.CurrentCell.ColumnIndex == 1)
        {
            cboCountry = e.Control as ComboBox;
            if (cboCountry != null)
            {
                cboCountry.SelectedValueChanged += cboCountry_SelectedValueChanged;
            }
        }
    }

    void cboCountry_SelectedValueChanged(object sender, EventArgs e)
    {
        //If category value changed then reset item to default.
        dataGridView1.CurrentRow.Cells[2].Value = 0;
    }

    void cboCity_DropDown(object sender, EventArgs e)
    {
        string countryName = dataGridView1.CurrentRow.Cells[1].Value.ToString();
        List<City> cities = new List<City>();

        cities = GetCities(countryName);
        cboCity.DataSource = cities;
        cboCity.DisplayMember = "Name";
        cboCity.ValueMember = "Id";


    }

    private void dataGridView1_CellLeave(object sender, DataGridViewCellEventArgs e)
    {
        if (cboCity != null) cboCity.DropDown -= cboCity_DropDown;
        if (cboCountry != null)
        {
            cboCountry.SelectedValueChanged -= cboCountry_SelectedValueChanged;
        }
    }
}

public class Country
{
    public int Id { get; set; }
    public string Name { get; set; }
}

public class City
{
    public int Id { get; set; }
    public string Name { get; set; }
}

public class Customer
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Surname { get; set; }
}

}


person user9969    schedule 07.01.2014    source источник
comment
Ваш код не слишком мал, и то, что вы собираетесь сделать, не так просто. В любом случае обратите внимание, что вы устанавливаете ValueMember в Id (и, таким образом, свойство Value возвращает Id), но затем вы проверяете название страны, и, таким образом, население города вообще не происходит. Просто измените все ValueMember на Name и население города должно заработать (но боюсь, что это еще не все).   -  person varocarbas    schedule 08.01.2014
comment
@varocarbas спасибо за ваше время. Очень сложно сбалансировать, сколько кода нужно добавить, и я быстро кое-что собрал, потому что не хотел спрашивать, как вы это делаете без кода!. Я знаю, что это непросто, и искал образцы, но не смог найти ничего, что действительно работает. Я получаю сообщение об ошибке datagridcomboboxcell.value is not valid. Знаете ли вы какой-либо пример, который я могу загрузить и посмотреть, как это работает?   -  person user9969    schedule 08.01.2014
comment
Проблема с datagridview заключается в том, что это довольно сложный элемент управления с систематическим вызовом множества событий. Ячейки типа Combobox принимают очень специфический формат. Ошибка, о которой вы говорите, указывает на то, что в какой-то момент отсутствует правильный формат. Это довольно сложно отследить. Я рекомендую вам делать все шаг за шагом и подтверждать, что каждый промежуточный шаг в порядке. Я мог бы написать небольшой код через некоторое время, если вы не получите никакой помощи.   -  person varocarbas    schedule 08.01.2014
comment


Ответы (2)


Как объяснялось в комментариях выше, проблемы, связанные с DataGridViewComboBox, могут стать сложными (по сути, вы добавляете другой элемент управления внутри уже довольно сложного элемента); и то, к чему вы стремитесь, доводит эту конфигурацию до ее пределов. DataGridView — это элемент управления, который, как ожидается, упростит управление проблемами средней сложности, связанными с данными; вы можете получить наилучшую производительность с его наиболее определяющими функциями (например, ячейки на основе текстовых полей, события, запускаемые после проверки ячейки и т. д.). Таким образом, включение полей со списком (или флажков или эквивалентных) ячеек допустимо, если вы не доводите его производительность до предела. Чтобы получить наилучший возможный результат для того, что вы хотите (координация различных полей со списком), я предлагаю вам не полагаться на элемент управления DataGridView (или, по крайней мере, не на часть координации со списком), поскольку реализация проблематична, окончательный результат не так надежна, как может быть, и, в любом случае, общая структура гораздо более жесткая, чем та, которая получается в результате независимого от DGV подхода (т. Е. Отдельных ComboBox элементов управления).

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

private void Form1_Load(object sender, EventArgs e)
{
    dataGridView1.EditingControlShowing +=new DataGridViewEditingControlShowingEventHandler(dataGridView1_EditingControlShowing);

    DataGridViewComboBoxColumn curCol1 = new DataGridViewComboBoxColumn();
    List<string> source1 = new List<string>() { "val1", "val2", "val3" };
    curCol1.DataSource = source1;
    DataGridViewComboBoxColumn curCol2 = new DataGridViewComboBoxColumn();

    dataGridView1.Columns.Add(curCol1);
    dataGridView1.Columns.Add(curCol2);

    for (int i = 0; i <= 5; i++)
    {
        dataGridView1.Rows.Add();
        dataGridView1[0, i].Value = source1[0];
        changeSourceCol2((string)dataGridView1[0, i].Value, (DataGridViewComboBoxCell)dataGridView1[1, i]);
    }
}

private void changeSourceCol2(string col1Val, DataGridViewComboBoxCell cellToChange)
{
    if (col1Val != null)
    {
        List<string> source2 = new List<string>() { col1Val + "1", col1Val + "2", col1Val + "3" };
        cellToChange.DataSource = source2;
        cellToChange.Value = source2[0];
    }
}

private void dataGridView1_EditingControlShowing(object sender, DataGridViewEditingControlShowingEventArgs e)
{
    if (dataGridView1.CurrentRow != null)
    {
        ComboBox col1Combo = e.Control as ComboBox;
        if (col1Combo != null)
        {
            if (dataGridView1.CurrentCell.ColumnIndex == 0)
            {
                col1Combo.SelectedIndexChanged += col1Combo_SelectedIndexChanged;
            }
        }
    }
}

private void col1Combo_SelectedIndexChanged(object sender, EventArgs e)
{
    if (dataGridView1.CurrentCell.ColumnIndex == 0)
    {
        dataGridView1.CommitEdit(DataGridViewDataErrorContexts.Commit);
        changeSourceCol2(dataGridView1.CurrentCell.Value.ToString(), (DataGridViewComboBoxCell)dataGridView1[1, dataGridView1.CurrentCell.RowIndex]);
    }
}

Этот код отлично работает с одним ограничением: когда вы меняете индекс первого поля со списком, значение не фиксируется немедленно (и, следовательно, второе поле со списком не может быть обновлено). Проведя несколько тестов, я подтвердил, что предлагаемая конфигурация (т. е. простое написание dataGridView1.CommitEdit(DataGridViewDataErrorContexts.Commit); перед заполнением источника второго поля со списком обеспечивает наилучшую производительность). Несмотря на это, обратите внимание, что этот код не работает идеально на этом фронте: он начинает работать (автоматически обновляя второе поле со списком каждый раз, когда в первом выбирается новый элемент) со второго выбора и далее; не уверен в точной причине этого, но, как уже говорилось, любая другая альтернатива, которую я пробовал, дает еще худшую производительность. Я не слишком много работал на этом фронте из-за моих вышеупомянутых комментариев (на самом деле, делать это даже не рекомендуется) и из-за ощущения, что вы должны сделать часть работы.....

person varocarbas    schedule 08.01.2014

Я хотел бы легко дать вам закодированное решение в нескольких строках, но мне, вероятно, придется опубликовать весь проект Visual Studio, чтобы продемонстрировать его в коде.

Идея здесь в том, что вы никогда не должны пытаться управлять таким сценарием, действуя через события Controls. Скорее, вы должны стремиться использовать механизм привязки данных Windows Forms. Привязывая элементы управления к источнику данных, который может сообщать пользовательскому интерфейсу об изменении его состояния, вам нужно только изменить базовые данные, и пользовательский интерфейс обновится соответствующим образом.

Что вам нужно, так это настроить то, что обычно известно как ViewModel, для хранения состояния различных задействованных элементов управления, и любая бизнес-логика (например, установка списка городов на основе страны) должна быть реализована в этом объекте ViewModel в реакция на установку свойств на нем.

Я предлагаю вам поискать информацию о привязке данных, а также о различных интерфейсах .NET, которые в ней участвуют. Первым из них, безусловно, является INotifyPropertyChanged, который нужно будет реализовать вашей ViewModel, чтобы инициировать изменения в пользовательском интерфейсе при изменении его состояния.

Разумное использование компонента BindingSource также облегчит вашу работу, например, для заполнения различных ComboBox желаемыми значениями.

Познакомьтесь с привязкой данных в Windows Form, и вам будет гораздо легче работать с такими сценариями.

Как я уже сказал, мне бы хотелось продемонстрировать это всего несколькими строками кода, и я надеюсь, что то, что я написал, укажет вам правильное направление.

Ваше здоровье

person Luc Morin    schedule 08.01.2014
comment
Проблема с общим утверждением заключается в том, что они редко бывают полезными. Это сложный элемент управления, особенно требовательный к условиям OP (ячейки / столбцы со списком). Не могли бы вы предоставить более непосредственно применимую помощь? - person varocarbas; 08.01.2014
comment
Нет. Как я уже сказал, я не думаю, что правильное решение его проблемы возможно в контексте SO. Я мог бы предоставить полноценное решение на github, но у меня нет времени. Если да, будь моим гостем. - person Luc Morin; 08.01.2014
comment
-1, потому что сумасшедший (трусливый) идиот сейчас поставил -1 мой ответ (и, предположительно, проголосовал за ваш). Я редко минусую, но ваш ответ явно не решает проблему здесь (ИМХО), и поэтому он не может быть ответом с наибольшим количеством голосов. - person varocarbas; 14.01.2014
comment
@varocarbas Я не знаю, о чем вы говорите, но если вы проголосовали за мой ответ только потому, что кто-то проголосовал за ваш, то я думаю, что у вас есть личные проблемы, которые вам нужно решить. Могу ли я попросить вас с этого момента полностью игнорировать меня и мои ответы, и я буду делать то же самое для вас? - person Luc Morin; 14.01.2014
comment
Я игнорирую тебя, не волнуйся. Это было дружеское общение (никогда -1 без слов). Пожалуйста, обратите немного внимания на слова, чтобы понять людей: я сказал, что заминусовал вас из-за новой ситуации (ваш ответ получил наибольшее количество голосов), по моему мнению, был неправильным; тот факт, что я сам получил -1 или вообще не связан с ответом -1ed, не имеет значения. - person varocarbas; 14.01.2014