Dynamically setting DataGridViewComboBoxCell's DataSource to filtered DataView based off of other cell selection

StackOverflow https://stackoverflow.com/questions/13079070

Question

I have been looking high and low for a way to do the following, but to no avail. I came up with a solution that works, but am wondering if there is a better way of handling it.

The Problem:

I am using a DataGridView that has two DataGridViewComboBoxColumn, col1 and col2.

col1 has had its DataSource set to a DataTable. Based off of the selection from a given cell in col1, I would like to have the respective col2 cell in the same row have its DataSource set to be a filtered DataView of col2's DataSource.

The abbreviated code from my current implementation might better help describe what I am trying to do:

DataGridView dg = new DataGridView();
dg.DataSource = new BindingSource() { DataSource = _myDataTable }; //DataTable with Foreign Keys for ID1 and ID2
dg.CellValueChanged += new DataGridViewCellEventHandler(dg_CellValueChanged);
DataGridViewComboBoxColumn col1 = new DataGridViewComboBoxColumn();
col1.DataPropertyName = "ID1";
col1.DisplayMember = "Display1";
col1.ValueMember = "ID1";
col1.DataSource = dataTable1;
col1.ValueType = typeof(Int32);

DataGridViewComboBoxColumn col2 = new DataGridViewComboBoxColumn();
col2.DataPropertyName = "ID2";
col2.DisplayMember = "Display2";
col2.ValueMember = "ID2";
col2.DataSource = dataTable2;
col2.ValueType = typeof(Int32);

dg.Columns.Add(col1);
dg.Columns.Add(col2);

Then I define the event handler as:

private void dg_CellValueChanged(object sender, DataGridViewCellEventArgs e)
{
    if (e.ColumnIndex == 0 && e.RowIndex > -1)
    {
        var dgv = sender as DataGridView;
        if (dgv == null)
            return;

        int selectedID;
        if (!int.TryParse(dgv[e.ColumnIndex, e.RowIndex].Value.ToString(), out selectedID))
            return;

        var cell = dgv[e.ColumnIndex + 1, e.RowIndex] as DataGridViewComboBoxCell;
        if (cell == null)
            return;

        var col = dgv.Columns[e.ColumnIndex + 1] as DataGridViewComboBoxColumn;
        if(col == null)
            return;

        var dt = col.DataSource as DataTable; // The macro-DataTable containing all possible values
        if(dt == null)
            return;

        DataView dv = new DataView(dt, "ID1 = " + selectedID, "DisplayOrder", DataViewRowState.CurrentRows);
        if(dv.Count == 0)
            return;

        //This is the part that I am wondering if there is a better way of handling
        cell.DataSource = dt; //Set the data source to the macro-DataTable so that when we set the Value to something in the new DataView it will not throw an exception
        cell.DisplayMember = "Display2"; // Have to redefine the Display/Value members
        cell.ValueMember = "ID2";
        cell.Value = dv[0]["ID2"]; // Set the value to the first option in the new DataView to avoid an exception being thrown when setting the dv as the DataSource
        cell.DataSource = dv;
    }
}

The last part is my concern. When dynamically switching the DataSource of cell2, if cell2's current selection doesn't appear in the new DataView, then an exception will be thrown:

System.ArgumentException: DataGridViewComboBoxCell value is not valid.

To avoid that, I am setting the datasource to the macro-DataTable (containing all of the values that could be shown based off of cell1's selection), changing the selected value of cell2 to be the first result in the DataView and then setting cell2's DataSource to be said DataView. This all ensures that the cell is never going to have an invalid selection and it works as expected.

My question is, is there a better/simpler way of doing this? For my use, this code is only activated when creating new rows, so it isn't being done more than a few times in the given form. However, if there is a better way of doing it or some suggestions on making it better, I would appreciate it!

(first question here, so I am also open to suggestions for posting ... apologies for any missteps)

EDIT (providing table structure - also changed "BoundID" to be "ID1" to avoid confusion):

DataGrid's table structure would be:

MainTableID int

ID1 int --This is a foreign key to col1

ID2 int --This is a foreign key to col2

Column 1's table structure:

ID1 int

Display1 varchar(50)

Column 2's table structure:

ID2 int

Display2 varchar(50)

ID1 int --This is a foreign key to col1

Updated: I have redefined the CellValueChanged event handler per Mohsen's suggestions:

private void dg_CellValueChanged(object sender, DataGridViewCellEventArgs e)
{
    if (e.ColumnIndex == 0 && e.RowIndex > -1)
    {
        var dgv = sender as DataGridView;

        var cell = dgv[e.ColumnIndex + 1, e.RowIndex] as DataGridViewComboBoxCell;
        if (cell == null)
            return;

        DataView dv = new DataView( ((DataTable)((DataGridViewComboBoxColumn)dgv.Columns[e.ColumnIndex + 1]).DataSource), "ID1 = " + dgv.CurrentCell.Value, "DisplayOrder", DataViewRowState.CurrentRows);
        if(dv.Count == 0)
            return;

        cell.DisplayMember = "Display2"; // Have to redefine the Display/Value members
        cell.ValueMember = "ID2";
        cell.DataSource = dv;
    }
}

And I have added in an event handler for the DataError like he suggested:

void dg_DataError(object sender, DataGridViewDataErrorEventArgs e)
{
    if(e.ColumnIndex != 1)
    {
                    //Alert the user for any other DataError's outside of the column I care about
                    MessageBox.Show("The following exception was encountered: " + e.Exception);
    }
}

This seems to work perfectly.

Was it helpful?

Solution

Your code can be simplified to this with no exception throwing:

    if (e.ColumnIndex == 0 && e.RowIndex > -1)
    {
        var dgv = sender as DataGridView;

        var cell = dgv[e.ColumnIndex + 1, e.RowIndex] as DataGridViewComboBoxCell;
        if (cell == null)
            return;

        cell.DataSource = ((DataTable)((DataGridViewComboBoxColumn)dgv.Columns[e.ColumnIndex + 1]).DataSource).Select("BoundID = " + dgv.CurrentCell.Value);                
    }

Updated

When you change an already set item in first combo box since the filtered dataview in the second combobox have different ID1, an exception is thrown with "DataGridViewComboBoxCell value is not valid.". In order to catch this exception register datagridview DataError event with no code in the registered method. Then when you change the already set combbox, the corresponding combobox will be filled with correct items.

OTHER TIPS

This is the quick stop shop for the solution that worked for me, but I still wanted to give Mohsen the credit. Duplicated this in the original question.

I have redefined the CellValueChanged event handler per Mohsen's suggestions:

private void dg_CellValueChanged(object sender, DataGridViewCellEventArgs e)
{
    if (e.ColumnIndex == 0 && e.RowIndex > -1)
    {
        var dgv = sender as DataGridView;

        var cell = dgv[e.ColumnIndex + 1, e.RowIndex] as DataGridViewComboBoxCell;
        if (cell == null)
            return;

        DataView dv = new DataView( ((DataTable)((DataGridViewComboBoxColumn)dgv.Columns[e.ColumnIndex + 1]).DataSource), "ID1 = " + dgv.CurrentCell.Value, "DisplayOrder", DataViewRowState.CurrentRows);
        if(dv.Count == 0)
            return;

        cell.DisplayMember = "Display2"; // Have to redefine the Display/Value members
        cell.ValueMember = "ID2";
        cell.DataSource = dv;
    }
}

And I have added in an event handler for the DataError like he suggested:

void dg_DataError(object sender, DataGridViewDataErrorEventArgs e)
{
    if(e.ColumnIndex != 1)
    {
        //Alert the user for any other DataError's outside of the column I care about
        MessageBox.Show("The following exception was encountered: " + e.Exception);
    }
}

This seems to work perfectly.

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