Developer
Developer

Reputation: 8636

Cannot set values from a DataGridViewComboBoxColumn to a bound DataGridView

I am having a DataGridView that initially loads data and shows it.
When an user clicks an Edit Button, I am adding a DataGridViewComboBoxColumn by hiding one of the Columns.

private DataTable BindCombo()
{
    DataTable dt = new DataTable();
    dt.Columns.Add("ProductId", typeof(int));
    dt.Columns.Add("ProductName", typeof(string));
    dt.Rows.Add(1, "Product1");
    dt.Rows.Add(2, "Product2");
    return dt;
}

private void BindGrid()
{
   DataTable dtGrid = new DataTable();
   DataColumn column = new DataColumn("ProductId")
   {
      DataType = System.Type.GetType("System.Int32"),
      AutoIncrement = true,
      AutoIncrementSeed = 1,
      AutoIncrementStep = 1
   };
   dtGrid.Columns.Add(column);
   dtGrid.Columns.Add("ProductName", typeof(string));

   dtGrid.Rows.Add(null, "Product1");
   dtGrid.Rows.Add(null, "Product2");

   dataGridView1.DataSource = dtGrid;
 }

 private void Form1_Load(object sender, EventArgs e)
 {
    BindGrid();
 }

Here is the Button.Click event where I am trying to add a ComboBox Column:

 private void btnEdit_Click(object sender, EventArgs e)
 {
     dataGridView1.AllowUserToAddRows = true;
     dataGridView1.ReadOnly = false;
     dataGridView1.Columns[1].Visible = false;
     DataGridViewComboBoxColumn col1 = new DataGridViewComboBoxColumn
     {
         DisplayStyle = DataGridViewComboBoxDisplayStyle.DropDownButton,
         HeaderText = "Product Name",
         DataSource = BindCombo(),
         ValueMember = "ProductId",
         DisplayMember = "ProductName",
         DataPropertyName = "ProductId"
     };
     dataGridView1.Columns.Add(col1);
     dataGridView1.AllowUserToAddRows = false;
    }

When I click on drop down, nothing happens:

enter image description here

Upvotes: 1

Views: 622

Answers (1)

Jimi
Jimi

Reputation: 32248

The DataTable used as the DataSource of your DataGridView has an auto-increment Column. You cannot use this Column as the ProductId, which can be changed by an User via the ComboBox selector. It will make a mess (unless this was used just just to build a MCVE).
You can use this Column as the Primary Key - setting also Unique = true.

Add instead a int Column that represents the ProductId Key, which links the ProductName Column that is part of the DataTable set as the DataSource of the ComboBox Column.
Since the ValueMember property of the ComboBox is set to the ProductId value and the ComboBox Column is bound to the ProductId Column of the DataGridView DataTable, changing the SelectdItem of the ComboBox will change the value of the ProductId Column of the DataTable used as data source of your DataGridView.

Added to the BindProductsGrid() method:

  • dtGrid.PrimaryKey = new[] { pkColumn };
  • dtGrid.AcceptChanges(); (mandatory)
  • dgvProducts.Columns["ProductId"].ReadOnly = true; and
  • dgvProducts.AllowUserToAddRows = false; (since this seems to be the intention: let the User specify the Product only using the ComboBox selector)

The DataGridViewComboBoxColumn is added after the DataSource of the DataGridView is set. This because this Column is bound to the ProductId Column, as the corresponding Column of the DataGridView's DataTable.
It allows to add to the DataGridView two Columns bound to the same Column of the data source in code, without confusing the Control.

private void BindProductsGrid()
{
    var dtGrid = new DataTable();
    var pkColumn = new DataColumn("ID") {
        DataType = typeof(int),
        AutoIncrement = true,
        AutoIncrementSeed = 1,
        AutoIncrementStep = 1,
        Unique = true
    };

    var productColumn = new DataColumn("ProductId") {
        DataType = typeof(int),
        Caption = "Product Id"
    };

    dtGrid.Columns.Add(pkColumn);
    dtGrid.Columns.Add(productColumn);

    dtGrid.Rows.Add(null, 1);
    dtGrid.Rows.Add(null, 2);
    dtGrid.Rows.Add(null, 3);
    dtGrid.Rows.Add(null, 4);

    dtGrid.PrimaryKey = new[] { pkColumn };
    dtGrid.AcceptChanges();

    dgvProducts.DataSource = dtGrid;

    dgvProducts.Columns["ID"].Visible = false;
    dgvProducts.Columns["ProductId"].ReadOnly = true;

    var productName = new DataGridViewComboBoxColumn {
        DisplayStyle = DataGridViewComboBoxDisplayStyle.Nothing,
        Name = "ProductName",
        HeaderText = "Product Name",
        ValueMember = "ProductId",
        DisplayMember = "ProductName",
        DataSource = BindCombo(),
        DataPropertyName = "ProductId",
        DisplayIndex = 2
    };
    dgvProducts.Columns.Add(productName);
    dgvProducts.AllowUserToAddRows = false;
}

The DataTable used as data source of the ComboBox has the same two Columns, just AcceptChanges() (optional) is added to the previous code:

private DataTable BindCombo()
{
    var dt = new DataTable();
    dt.Columns.Add("ProductId", typeof(int));
    dt.Columns.Add("ProductName", typeof(string));

    dt.Rows.Add(1, "Product1");
    dt.Rows.Add(2, "Product2");
    dt.Rows.Add(3, "Product3");
    dt.Rows.Add(4, "Product4");
    dt.AcceptChanges();
    return dt;
}

Now, some adjustments to make the Product selection more responsive:
(➨ note that the DataGridView is named dgvProducts)

  • The EditingControlShowing handler subscribes to the ComboBox Cell SelectedIndexChanged event

  • The ComboBox SelectedIndexChanged handler invokes asynchronously Validate(), to show the ComboBox selection immediately (no need to select another Cell to see it applied)

    BeginInvoke(new Action(() => Validate()));
    
  • The DataGridView CellContentClick handler changes the style of the ComboBox to DataGridViewComboBoxDisplayStyle.ComboBox

  • CellLeave handler restores the ComboBox style to DataGridViewComboBoxDisplayStyle.Nothing, so it looks like a TextBox.

▶ If you instead want to just hide/show the ProductName column, you can do without the CellContentClick and CellLeave handlers and keep the initial ComboBox style.

private void dgvComboBox_SelectedIndexChanged(object sender, EventArgs e)
{
    (sender as ComboBox).SelectedIndexChanged -= dgvComboBox_SelectedIndexChanged;
    BeginInvoke(new Action(() => Validate()));
}

private void dgvProducts_EditingControlShowing(object sender, DataGridViewEditingControlShowingEventArgs e)
{
    if (e.Control is ComboBox cbo) {
        cbo.SelectedIndexChanged += dgvComboBox_SelectedIndexChanged;
    }
}

private void dgvProducts_CellContentClick(object sender, DataGridViewCellEventArgs e)
{
    if (e.RowIndex > 0 && e.ColumnIndex == 2) {
        if (dgvProducts[2, e.RowIndex] is DataGridViewComboBoxCell cbox) {
            cbox.DisplayStyle = DataGridViewComboBoxDisplayStyle.ComboBox;
        }
    }
}

private void dgvProducts_CellLeave(object sender, DataGridViewCellEventArgs e)
{
    if (e.RowIndex < 0) return;
    if (dgvProducts.Columns[e.ColumnIndex].Name == "ProductName") {
        if (dgvProducts["ProductName", e.RowIndex] is DataGridViewComboBoxCell cbox) {
            cbox.DisplayStyle = DataGridViewComboBoxDisplayStyle.Nothing;
        }
    }
}

This how it works using the code shown here:

DataGridView ComboBoxColumn Selector

Upvotes: 1

Related Questions