Question

I need to implement cascading ComboBoxes in few DataGridViews. As a proof of concept I have put together the code below. 3 Columns (Customer, Country, City) When selecting Country, City should populate but it doesn't work.

Is there a better way to achieve this and fix what I am doing wrong?

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; }
}

}

Was it helpful?

Solution 2

As explained in the comments above, DataGridViewComboBox related issues might become tricky (you are basically adding a different control inside an-already-pretty-complex one); and what you aim does bring this configuration to its limits. DataGridView is a control expected to ease the management of medium-complexity, data-related issues; you can get the best performance with its most defining features (e.g., textbox-based cells, events triggered after the cell has been validated, etc.). Thus, including comboboxes (or checkboxes or equivalent) cells is OK as far as you don't bring its performance to the limits. To get the best result possible for what you want (coordinating different comboboxes), I suggest you to not rely on a DataGridView control (or, at least, not for the combobox coordination part) as far as the implemention is problematic, the final result not as reliable as it can get and, in any case, the overall structure much more rigid than the one resulting from a DGV-independent approach (i.e., individual ComboBox controls).

In any case, I have felt curious about this implementation (mainly after seeing quite a few problems in my preliminary tests) and decided to write this code to answer your concern.

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]);
    }
}

This code works fine with one limitation: when you change the index of the first combobox, the value is not immediately committed (and thus the second combobox cannot be updated). After doing some tests, I have confirmed that the proposed configuration (i.e., just writing dataGridView1.CommitEdit(DataGridViewDataErrorContexts.Commit); before populating the second-combobox source delivers the best performance). Despite that, note that this code does not work perfectly on this front: it starts working (updating automatically the second combobox every time a new item is selected in the first one) from the second selection onwards; not sure about the exact reason of this but, as said, any other alternative I tried delivers a still worse performance. I haven't worked too much on this front because of my aforementioned comments (actually, doing this is not even recommendable) and because of feeling that you have to do part of the work.....

OTHER TIPS

I wish I could easily give you a coded solution in a few lines, but I would probably have to post an entire Visual Studio project to demonstrate in code.

The idea here is that you should never try to control this kind of scenario by acting through Controls' events. Rather, you should aim to use Windows Forms' data binding mechanism. By binding the controls to a data source that is capable of letting the UI know when its state changes, you only have to modify the underlying data, and the UI will update itself accordingly.

What you need is to setup what is usually known as a ViewModel to hold the state of the various controls involved, and whatever business logic (such as setting the list of cities based on the country) should be taken care of within this ViewModel object in reaction to setting properties on it.

I invite you to search information on data binding as well as on the various .NET interfaces that participate in it. The first one is definitely INotifyPropertyChanged, which your ViewModel will need to implement to trigger changes in the UI when its state changes.

Judicious use of the BindingSource component will also facilitate your job, for example to fill the various ComboBoxes with the desired values.

Get familiar with Windows Form's data binding, and you will have much less pain in handling such scenarios.

Like I said, I wish I could demonstrate this in just a few lines of codes, and I hope that what I wrote will point you in the right direction.

Cheers

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top