根据 DataGridView 中组合框 B 中的选定值筛选组合框 A 中的值

Filter values in combo box A based on selected value in combo box B in DataGridView

我知道这是一个很受欢迎的问题,但我找不到解决问题的办法。 DataGridView dgvTransfer 中有两个组合框 comboFromcomboTo - 以下是它们的填充方式:

    private void PopComboFrom(DataGridViewComboBoxColumn combo, string valMember, int parValue1, int parValue2)
    {
        combo.DataSource = null;
        DataTable dt = Helper.ExecuteDataTable("pp_sp_MachineAndOp",
                                           new SqlParameter("@MachineAndOpID", SqlDbType.Int) { Value = parValue1 },
                                           new SqlParameter("@Seq", SqlDbType.Int) { Value = parValue2 });

        //Add field to be displayed
        dt.Columns.Add("ToShow", typeof(string), "'Seq: ' + Seq  + '    ID: ' + MachineAndOpID + ' (' + OperationID + ') ' + Operation + ' + ' + Machine");

        // bind data table into combo box.
        combo.DisplayMember = "ToShow";
        combo.ValueMember = valMember;
        combo.DataSource = dt;
    }

    private void PopComboTo(DataGridViewComboBoxColumn combo, string valMember, int parValue1, int parValue2, string seqFilter)
    {
        //combo.DataSource = null;
        DataTable dt = Helper.ExecuteDataTable("pp_sp_MachineAndOp",
                                       new SqlParameter("@MachineAndOpID", SqlDbType.Int) { Value = parValue1 },
                                       new SqlParameter("@Seq", SqlDbType.Int) { Value = parValue2 });

        //Add field to be displayed
        dt.Columns.Add("ToShow", typeof(string), "'Seq: ' + Seq  + '    ID: ' + MachineAndOpID + ' (' + OperationID + ') ' + Operation + ' + ' + Machine");

        // bind data table into combo box.
        DataView dv = new DataView(dt);
        dv.RowFilter = "Seq > " + seqFilter;
        combo.DisplayMember = "ToShow";
        combo.ValueMember = valMember;
        combo.DataSource = dv;
    }

comboFrom 更改时,comboTo 的数据源在此事件处理程序中被过滤:

    private void dgvTransfers_EditingControlShowing(object sender, DataGridViewEditingControlShowingEventArgs e)
    {
        if(dgvTransfers.CurrentCell.ColumnIndex == dgvTransfers.Columns[controlFrom].Index && e.Control is ComboBox)
        {
            ComboBox comboBox = e.Control as ComboBox;
            comboBox.SelectedIndexChanged -= ComboFromSelectionChanged;
            comboBox.SelectedIndexChanged += ComboFromSelectionChanged;
        }
    }
    private void ComboFromSelectionChanged(object sender, EventArgs e)
    {
        PopComboTo(cmbMachineAndOpIDTo, "MachineAndOpID", 0, 0, "7");
    }

过滤器适用于 comboTo,但问题是过滤后的 comboTo 中不可见的值也会从 DataGridView 中消失!

未过滤:

过滤:

对于过滤结果为空数据的每一行都会引发此错误:

如何在 GridViewData 中显示值并同时在 comboTo 中过滤它们?

在这个例子中,我们有两个 DataGridViewComboBoxColumn。每列都包含相同的数据源并且显然包含相同的值。目标是这样的,如果用户 select 在特定行的一个组合框中输入一个值,那么,当用户 select 在同一行的另一个组合框时, select 在第一个组合框中输入的值在第二个组合框中不可用。换句话说,网格中同一行上的两个组合框都不会包含相同的值。

我相信有很多方法可以做到这一点。记住这是参考两个 DataGridViewComboBoxColumns 可能会有所帮助。在 DataGridView 的列中处理 ComboBoxes 可能具有挑战性,并且不像常规 ComboBox 那样宽容。将此与每个单元格中的“每个”组合框可能包含“不同的”DataSource 的想法相结合只会使事情更加恶化,并使网格更容易抛出其可怕的 DataError

使用组合框列时的一个大问题是,如果(以某种方式)将组合框单元格的值设置为不在组合框项目列表中的值。一个普通的 ComboBox 只是忽略这个,另一方面,一个 DataGridViewComboBoxCell 会抱怨这个并导致网格抛出它的 DataError。或者更准确地说……代码崩溃问题。因此,您必须小心并确保没有错误设置组合框值。

鉴于我们不希望一行中的两个组合框具有重复值,有一种情况是两个组合框的值可能相同......当两个值都为“空”时,或者……还没有 selected。换句话说,两个组合框有可能具有相同的“NON”值。这是一个“特殊”情况,我们会寻找它,但是,我想强调的是,简单地使用组合框单元格 null 值来表示这个“empty/non-choice”可能会有问题。所以……为了提供帮助,我建议您添加一个“empty/non-choice”值作为每个组合框中的项目之一。在这一点上可能并不明显;但是,组合框项目列表中的这个简单的“non-choice”项目将在以后简化事情。

首先,我们需要将其分解为两 (2) 个部分。第一部分 (1) 处理用户与组合框的交互。如果我们有一个只有一行的“空”网格,那么,当用户更改其中一个组合框时,另一个组合框会过滤其数据源,反之亦然。第二 (2) 部分处理在网格加载现有数据后要做什么。如果我们简单地加载数据,那么每个组合框都将被正确设置,但是,如果用户单击其中一个组合框,则其他组合框的值将可用于复制。一旦用户更改了组合框的值,第一部分就会启动并过滤另一个组合框。换句话说,我们需要在数据加载到网格后做一些事情来过滤加载的每个组合框单元格。

如果您创建一个新的 winforms 解决方案并将 DataGridView 拖放到窗体上,您应该能够按照以下步骤进行演示并以 step-by-step 方式继续。首先,我们将创建一些简单的组合框数据以显示在组合框项目列表中。然后,在两个组合框中使用该数据。此后,两个组合框将包含相同的数据,用户将能够在一行中重复值。

组合框数据将是一个 DataTable 和两个 properties/fields…一个 int MachineID 和一个 string MachineOPID。数据 table 将有 11 行数据。应该注意的是,在这里我们还想添加我们的“非”选择项......

dt.Rows.Add(0, "-");

应该注意的是,我没有为 MachineOPID 值使用“空”stringnull。我们想要那里的“东西”。在这种情况下,它是一个简单的短划线字符。稍后我们将查找零 (0) 的 MachineID,以了解用户 select 编辑了“non-selected”值。

private DataTable GetComboData() {
  DataTable dt = new DataTable();
  dt.Columns.Add("MachineID", typeof(int));
  dt.Columns.Add("MachineOPID", typeof(string));
  dt.Rows.Add(0, "-");
  for (int i = 1; i < 11; i++) {
    dt.Rows.Add(i, "Mac_" + i);
  }
  return dt;
}

接下来我们需要将组合框列添加到网格中。为此,创建了一个带有四个参数的辅助方法。列名,colName; header 文本,一个数据源,最后是一个 DataPropertyName,将在第二部分 (2) 中使用。

private DataGridViewComboBoxColumn GetComboCol(string colName, string headerText, DataTable data, string dpn) {
  DataGridViewComboBoxColumn col = new DataGridViewComboBoxColumn();
  col.Name = colName;
  col.HeaderText = headerText;
  col.ValueMember = "MachineID";
  col.DisplayMember = "MachineOPID";
  col.DataPropertyName = dpn;
  col.DataSource = data;
  return col;
}

这应该使组合框工作,如下所示。每个组合框都具有相同的值,用户可以 select 任意行的两个组合框具有相同的值。

DataTable ComboData;

public Form1() {
  InitializeComponent();
  dataGridView1.EditMode = DataGridViewEditMode.EditOnEnter;
}

private void Form1_Load(object sender, EventArgs e) {
  ComboData = GetComboData();
  dataGridView1.Columns.Add(GetComboCol("FromMachine", "From Machine", ComboData, "FromMachine"));
  dataGridView1.Columns.Add(GetComboCol("ToMachine", "To Machine", ComboData, "ToMachine"));
  
}

这显然与检查重复的组合框值无关。在这种情况下,我将使用 grids CellValueChanged 事件来“过滤”另一个组合框。例如,如果用户 select 在“FromMachine”列中输入一个值,然后离开该单元格,那么我们将过滤“ToMachine”组合框。如果更改的单元格是“ToMachine”单元格,同样的想法也适用。因此,创建了一个带有三个参数的辅助方法,一个 int 网格行索引; a string 已更改列的列名,最后是我们要过滤的单元格的列名。这样,我们可以使用相同的方法d 用于两个组合框单元格。它可能看起来像……

private void SetComboData(int rowIndex, string curColName, string targetColName) {
  if (dataGridView1.Rows[rowIndex].Cells[curColName].Value != null) {
    string selectedValue = dataGridView1.Rows[rowIndex].Cells[curColName].Value.ToString();
    DataGridViewComboBoxCell cell = (DataGridViewComboBoxCell)dataGridView1.Rows[rowIndex].Cells[targetColName];
    DataView dv = new DataView(ComboData);
    // we do not want to filter out the "empty" value - i.e. BOTH From and To can be empty
    if (selectedValue != "0") {
      dv.RowFilter = "MachineID <> '" + selectedValue + "'";
    }
    cell.DataSource = dv;
  }
}

首先简单检查一下单元格是否不是 null。然后,获取该单元格值,因为这将是我们要从其他组合框中过滤掉的项目。抓住另一个组合框单元格并将其转换为 DataGridViewComboBoxCell 以便我们可以设置其数据源。创建一个新的 DataView,设置行过滤器,除非 selected 项是我们不想过滤掉的“non-selected”项。最后设置单元格数据源。

此辅助方法应将网格 CellValueChanged 事件中的代码简化为类似...

private void dataGridView1_CellValueChanged(object sender, DataGridViewCellEventArgs e) {
  if (dataGridView1.Columns[e.ColumnIndex].Name == "FromMachine") {
    SetComboData(e.RowIndex, "FromMachine", "ToMachine");
  }
  else {
    if (dataGridView1.Columns[e.ColumnIndex].Name == "ToMachine") {
      SetComboData(e.RowIndex, "ToMachine", "FromMachine");
    }
  }
}


// updated constructor to subscribe to the grids `CellValueChanged` event
public Form1() {
  InitializeComponent();
  dataGridView1.EditMode = DataGridViewEditMode.EditOnEnter;
  dataGridView1.CellValueChanged += new DataGridViewCellEventHandler(dataGridView1_CellValueChanged);
}

此更改应如下所示起作用。同一行上的两个组合框不能设置为相同的值。如果任一组合框发生更改并且组合框都可以设置为相同的“non-selected”值,这应该会更新。

第一部分用户与组合框的交互到此结束。第二部分是当数据加载到网格中时要做什么。 IMO,如果您在数据绑定的网格中使用组合框列……“检查”绑定到组合框列的组合框数据是明智的。任何不良数据都会导致网格抛出其代码崩溃 DataError。即使采用简单的方法,在将数据绑定到网格之前检查网格数据也只是一种 CYA 方法。

所以……问题永远是……

”what do you do if a row in the data has offending data in the combo boxes… i.e.…. a non-existent value (out of bounds)… or in this case, duplicate FromMachine and ToMachine values?”

不幸的是,这是我们不能忽视的事情。您要么必须删除有问题的数据,要么更改它。这两个想法都是坏主意,但你有什么选择呢?在此示例中,如果两个值相同,那么我们要做的就是将“ToMachine”“更改”为我们的“non-selected”值。如果该值越界,我们将其更改为 non-selected 值。显然,这个“变化”可能是日志文件中一些需要的信息。然而,在这个例子中......我们只想改变它,记录它并继续。

为了对此进行测试,下面是一些我们可以用来绑定到网格的测试数据。它有三 (3) 行具有重复值。此外,它还有三行错误数据,因此 MachineID 超出范围 (12, -1)。它可能看起来像……

private DataTable GetGridData() {
  DataTable dt = new DataTable();
  dt.Columns.Add("FromMachine", typeof(int));
  dt.Columns.Add("ToMachine", typeof(int));
  dt.Rows.Add(1, 1);
  dt.Rows.Add(4, 1);
  dt.Rows.Add(5, 5);
  dt.Rows.Add(0, 9);
  dt.Rows.Add(4, 0);
  dt.Rows.Add(0, 0);
  dt.Rows.Add(12, 0);
  dt.Rows.Add(0, -1);
  dt.Rows.Add(-1, 12);
  return dt;
}

如果我们将此 table 绑定到网格,我们将获得网格 DataError。此外,重复的值将显示在网格中。重复值是明显和常量 DataError 的次要问题,因为数据中的值超出范围……即 12 和 -1 值。这就是为什么我们需要在将数据绑定到网格之前检查网格组合框数据值的原因。不要以为你可以简单地捕捉到 DataError 和 ignore/swallow 错误……你必须做点什么。

为此,我们将创建一个简单的方法,它获取原始网格数据 table 并循环遍历 table 的行。我们将检查越界值,因为我们正在这样做,所以我们可以检查“重复”组合框值并采取相应措施。在这两种情况下,如果该值超出范围,我们将该值设置为“non-selected”值 0。如果同一行上的任意两个值相等,则同样适用。

进行这些更改后,我们“应该”能够松一口气,因为我们知道不会因为组合框值超出范围而引发网格数据错误。此外,这将修复任何具有相同组合框值的行。这个修复方法可能看起来像……

private void FixDuplicateComboData(DataTable originalData) {
  int curRowIndex = 0;
  int fromValue;
  int toValue;
  foreach (DataRow row in originalData.Rows) {
    fromValue = (int)row["FromMachine"];
    toValue = (int)row["ToMachine"];
    if (fromValue < 0 || fromValue > 10) {
      Debug.WriteLine("Row: " + curRowIndex + " From: " + row["FromMachine"] + " value is out of range - setting to no-choice");
      row["FromMachine"] = 0;
    }
    if (toValue < 0 || toValue > 10) {
      Debug.WriteLine("Row: " + curRowIndex + " To: " + row["ToMachine"] + " value is out of range - setting to no-choice");
      row["ToMachine"] = 0;
    }
    if (fromValue == toValue) {
      Debug.WriteLine("Row: " + curRowIndex + " From: " + row["FromMachine"] + " and To: " + row["ToMachine"] + " - Machines are the same... Setting to no-choice");
      row["ToMachine"] = 0;
    }
    curRowIndex++;
  }
}

这应该可以消除数据错误并且重复项已得到修复,因此在加载数据后网格中不会显示重复项。但是,仍然存在一个小问题。问题是当数据加载到网格中时,每个组合框单元格都没有被过滤。因此,在加载数据后,如果我们单击具有两个值的组合框行,您将在组合框项目列表中看到“其他”组合框值 IS。此外,如果我们 select 那个项目......我们的数据错误朋友将开始抱怨。

因此,我们还有最后一步要完成。将好的数据加载到网格后,我们需要遍历网格中的所有行,并将每个组合框单元格的数据源设置为正确设置的过滤器。幸运的是,我们之前的 SetComboData 方法应该使这个相对简单......就像......

private void SetComboDataAfterDataLoad() {
  foreach (DataGridViewRow row in dataGridView1.Rows) {
    SetComboData(row.Index, "FromMachine", "ToMachine");
    SetComboData(row.Index, "ToMachine", "FromMachine");
  }
}

最终产品应该像下面显示的那样工作……

DataTable ComboData;
DataTable GridTable;

public Form1() {
  InitializeComponent();
  dataGridView1.EditMode = DataGridViewEditMode.EditOnEnter;
  dataGridView1.CellValueChanged += new DataGridViewCellEventHandler(dataGridView1_CellValueChanged);
  dataGridView1.DataError += new DataGridViewDataErrorEventHandler(dataGridView1_DataError);
}


private void Form1_Load(object sender, EventArgs e) {
  ComboData = GetComboData();
  dataGridView1.Columns.Add(GetComboCol("FromMachine", "From Machine", ComboData, "FromMachine"));
  dataGridView1.Columns.Add(GetComboCol("ToMachine", "To Machine", ComboData, "ToMachine"));
  GridTable = GetGridData();
  FixDuplicateComboData(GridTable);
  dataGridView1.DataSource = GridTable;
  SetComboDataAfterDataLoad();
}


private void dataGridView1_DataError(object sender, DataGridViewDataErrorEventArgs e) {
  MessageBox.Show("Data Error: " + e.Exception.Message);
  Debug.WriteLine("Data Error: " + e.Exception.Message);
}

这应该完成了这个例子。我希望它有意义。