如何在DataGridView控件中实现级联ComboBox?

3

我需要在几个 DataGridView 中实现级联的 ComboBoxes。作为概念的证明,我已经编写了下面的代码。有三列 (Customer, Country, City) 当选择Country时,City应该自动填充,但它没有工作。

有更好的方法来实现这个功能并修复我的错误吗?

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

}


你的代码不算小,而且你想做的事情也不是很简单。无论如何,请注意你将ValueMember设置为“Id”(因此Value属性返回“Id”),但是你检查的是国家名称,因此城市人口根本没有发生变化。只需将所有ValueMember更改为“Name”,城市人口就应该可以工作了(但我担心这还不够)。 - varocarbas
@varocarbas 谢谢您的时间。平衡放置多少代码非常困难,我很快就把一些东西放在一起,因为我不想问“如何在没有任何代码的情况下完成这个任务!”我知道这并不简单,并且一直在寻找示例,但找不到任何实际可行的示例。我收到了一个错误消息“datagridcomboboxcell.value is not valid”。您知道是否有可以下载并查看其工作方式的示例吗? - user9969
DataGridView的问题在于它是一个相当复杂的控件,有许多事件被系统地调用。组合框类型的单元格接受非常特定的格式。您所提到的错误表明某个时刻缺少正确的格式。这很难追踪。我建议您一步一步地进行操作,并确认每个中间步骤都没有问题。如果您得不到任何帮助,我可以过一段时间写一些小代码。 - varocarbas
DataGridView中的级联/依赖ComboBox列 - Reza Aghaei
2个回答

1
我希望我能轻松地用几行代码为您提供编码解决方案,但我可能需要发布整个Visual Studio项目以演示代码。
这里的想法是,您永远不应该尝试通过控件事件来控制这种情况。相反,您应该旨在使用Windows Forms的数据绑定机制。通过将控件绑定到能够让UI知道其状态何时更改的数据源,您只需修改底层数据,UI将相应更新自身。
您需要设置通常称为ViewModel的内容以保存涉及的各种控件的状态,以及任何业务逻辑(例如根据国家/地区设置城市列表)都应在此ViewModel对象中处理,以响应在其中设置属性。
我邀请您搜索有关数据绑定以及参与其中的各种.NET接口的信息。第一个接口肯定是INotifyPropertyChanged,您的ViewModel将需要实现它以在其状态更改时触发UI中的更改。
合理使用BindingSource组件也将简化您的工作,例如使用所需值填充各个ComboBox。
熟悉Windows Form的数据绑定,您将在处理此类情况时感到少得多的痛苦。

就像我之前说的那样,我希望能用几行代码演示这个,同时也希望我写的内容能给你指明正确的方向。

谢谢!


通用语句的问题在于它们很少有帮助。这是一个复杂的控件,特别挑剔OP的条件(组合框单元格/列)。您能否提供更直接适用的帮助? - varocarbas
1
不。就像我说的,我认为在SO的背景下,他的问题没有正确的解决方案。我可以在github上提供完整的解决方案,但我没有时间。如果你有时间,请随便做。 - Luc Morin
-1 是因为一个疯狂的(懦弱的)白痴现在给我的答案点了负一分(并且可能给你的答案点了赞)。我很少点踩,但是你的答案显然没有解决这里的问题(在我看来),因此它不能成为最受欢迎的答案。 - varocarbas
2
@varocarbas,我不知道你在说什么,但是如果你因为别人给你的回答投了反对票,而去投了我回答的反对票,那么我认为你有一些个人问题需要解决。我可以客气地问一下,从现在开始你能否彻底忽略我和我的回答,我也会对你采取同样的行动呢? - Luc Morin
我忽略你,不要担心。这是友好的沟通(从来没有贬低别人而不说出来)。请稍微关注一下言辞,以便理解他人:我确实说过我给你投了反对票,因为新情况下(你的答案得到最多赞同票)在我看来是错误的;我自己是否得到-1,或者与那个被投反对票的回答无关,都无所谓。 - varocarbas

0
如上面的评论所解释的那样,DataGridViewComboBox相关问题可能会变得棘手(基本上是在已经相当复杂的组件中添加了一个不同的控件);而你的目标确实将这个配置推向了极限。DataGridView是一个旨在简化中等复杂度、与数据有关的问题管理的控件;您可以通过其最显著的特征(例如基于文本框的单元格,在单元格验证后触发事件等)获得最佳性能。因此,包含组合框(或复选框或等效物)单元格是可以的,只要不把它的性能推到极限就行了。为了获得您想要的最佳结果(协调不同的组合框),我建议您不要依赖于一个DataGridView控件(或者至少不要依赖于组合框的协调部分),因为实现起来有问题,最终结果不如可靠,而且总体结构比独立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);)提供了最佳性能。尽管如此,请注意,这段代码在这方面并不完美:它从第二次选择开始工作(每次在第一个选择中选择新项目时自动更新第二个组合框),不确定确切原因,但是,正如所说,我尝试的任何其他替代方案都提供了更糟糕的性能。由于我之前的评论(实际上,这样做甚至不可取)和感觉您必须完成部分工作,因此我在这方面没有太多工作。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接