2

I have a DataGridView in a C# WinForms project in which, when the user clicks on certain DGV cells, the cell changes to a DataGridViewComboBoxCell and the ComboBox is populated with some values for the user to select. Here's the form code for the DataGridView_Click event:

private void dgvCategories_Click(Object sender, DataGridViewCellEventArgs e)
{
    if (e.ColumnIndex == 5 && !(dgvCategories.Rows[e.RowIndex].Cells[e.ColumnIndex].GetType().Name == "DataGridViewComboBoxCell"))
    {
        // Bind combobox to dgv and than bind new values datasource to combobox
        DataGridViewComboBoxCell cboNewValueList = new DataGridViewComboBoxCell();

        // Get fields to build New Value query
        List<string> lsNewValuesResult = new List<string>();
        string strCategory = dtCategories.Rows[e.RowIndex][1].ToString();
        string strCompanyName = cboSelectCompany.Text;
        string strQueryGetNewValuesValidationInfo = "SELECT validationdb, validationtable, validationfield, validationfield2, validationvalue2" +
                                                " FROM masterfiles.categories" +
                                                " WHERE category = @category";
                                                //" WHERE category = '" + strCategory + "'";

        // Pass validation info query to db and return list of New Values
        db getListOfNewValues = new db();
        lsNewValuesResult = getListOfNewValues.GetNewValuesList(strQueryGetNewValuesValidationInfo, strCategory, strCompanyName);

        //Populate the combobox with the list of New Values
        foreach (string strListItem in lsNewValuesResult)
        {
            cboNewValueList.Items.Add(strListItem);
        }

        // 
        dgvCategories[e.ColumnIndex, e.RowIndex] = cboNewValueList;

    }
}

Here's the code in the db class that populates the ComboBox (this likely isn't necessary to include for the purposes of this question, but for the sake of completeness, I'm including it, in case is it relevant):

public List<string> GetNewValuesList(string strValidationInfoQuery, string strCategory, string strCompanyName)
{
    List<string> lsValidationInfo = new List<string>();
    List<string> lsNewValuesList = new List<string>();

    using (NpgsqlConnection conn = new NpgsqlConnection(connString))
    using (NpgsqlCommand cmd = new NpgsqlCommand(strValidationInfoQuery, conn))
    {
        cmd.Parameters.AddWithValue("category", strCategory);

        conn.Open();

        using (NpgsqlDataReader reader = cmd.ExecuteReader())
        {
            while (reader.Read())
            {
                int intReaderIndex;
                for (intReaderIndex = 0; intReaderIndex <= reader.FieldCount - 1; intReaderIndex++)
                {

                    // reader indexes 3 & 4 correspond to categories.validationfield2 and validationvalue2, which can be null
                    if (string.IsNullOrEmpty(reader[intReaderIndex].ToString()))
                    {
                        lsValidationInfo.Add("");
                    }
                    else
                    {
                        lsValidationInfo.Add(reader.GetString(intReaderIndex));
                    }
                    //Console.WriteLine("reader index " + intReaderIndex + ": " + reader.GetString(intReaderIndex));
                }
            }
        }
    }

    string strValidationDb = lsValidationInfo[0];
    string strValidationTable = lsValidationInfo[1];
    string strValidationField = lsValidationInfo[2];
    string strValidationField2 = lsValidationInfo[3];
    string strValidationValue2 = lsValidationInfo[4];

    string strQueryGetNewValues = "SELECT DISTINCT " + strValidationField +
                        " FROM " + strValidationDb + "." + strValidationTable +
                        " WHERE company_id = (SELECT id FROM company WHERE name = '" + strCompanyName + "')";

    if (!string.IsNullOrEmpty(strValidationField2) && !string.IsNullOrEmpty(strValidationValue2)) strQueryGetNewValues += " AND " + strValidationField2 + " = '" + strValidationValue2 + "'";

    strQueryGetNewValues += " ORDER BY " + strValidationField;

    using (NpgsqlConnection conn = new NpgsqlConnection(connString))
    using (NpgsqlCommand cmd = new NpgsqlCommand(strQueryGetNewValues, conn))
    {
        conn.Open();

        using (NpgsqlDataReader reader = cmd.ExecuteReader())
        {
            while (reader.Read())
            {
                int intReaderIndex;
                for (intReaderIndex = 0; intReaderIndex <= reader.FieldCount - 1; intReaderIndex++)
                {
                    // reader indexes 3 & 4 correspond to categories.validationfield2 and validationvalue2, which can be null
                    if (string.IsNullOrEmpty(reader[intReaderIndex].ToString()))
                    {
                        lsNewValuesList.Add("");
                    }
                    else
                    {
                        lsNewValuesList.Add(reader.GetString(intReaderIndex));
                    }
                    Console.WriteLine("reader index " + intReaderIndex + ": " + reader.GetString(intReaderIndex));
                }
            }
        }
    }

    return lsNewValuesList;
}

The combobox is getting populated, as I can access the items in lsNewValuesResult in the _Click method. The DGV Edit Mode is set to EditOnEnter. I tried EditOnKeystroke, but that didn't cause the combobox to expand on mouse click.

This is what the combobox looks like when the cell is clicked on and the CBO is populated and added to the DGV cell:

enter image description here

That's after I clicked each of the two cells.

[RESOLVED]

See my Answer below.

Unfortunately solving this revealed a new issue.

marky
  • 4,878
  • 17
  • 59
  • 103
  • Add `DataGridViewComboBoxColumn` to the columns collection. – Reza Aghaei Apr 10 '20 at 13:06
  • @RezaAghaei, Sorry, where do I do that? – marky Apr 10 '20 at 14:34
  • @RezaAghaei, I found where to do that, but that won't work in my situation, as doing that would only add a new column, not replace an existing column after the DGV is populated. – marky Apr 14 '20 at 20:40
  • One trick I've used in the past is to create a standard `ComboBox` control on the fly and float it over the `DataGridView`. The `DataGridViewComboBoxCell` was a bit flaky back in the day, not sure if it still is. – SSS Apr 14 '20 at 23:31
  • @marky You need to set `DataPropertyName` property of the column. For example take a look at [this post](https://stackoverflow.com/a/40299238/3110834). – Reza Aghaei Apr 15 '20 at 03:13
  • @marky Does grid `DGV` use `DataSource` property or `Rows` collection to display and edit data? – Iliar Turdushev Apr 15 '20 at 08:26
  • @marky [DataGridViewComboBoxColumn - Have to click cell twice to display combo box](https://stackoverflow.com/a/32947890/3110834) – Reza Aghaei Apr 15 '20 at 10:58
  • @RezaAghaei, I went to the link for the DataPropertyName, but don't see where/how I can apply the information in that question to my situation. Please advise on that. That question you linked regarding the two clicks to open the CBO assumes that the CBO opens after clicking it multiple times - mine doesn't open at all. – marky Apr 15 '20 at 12:49
  • @IliarTurdushev, The DGV is populated from a DataTable. – marky Apr 15 '20 at 12:50
  • @SSS, How would I go about doing that? I'm at the point where I'll try anything! :) – marky Apr 15 '20 at 12:53
  • @marky You should now try everything in a single instance of code. When you mix everything together you cannot say what would be the result. The first problem that I see in the post which makes it confusing for the answerers is the question is not quiet clear. So it will result in a few random answers and result in confusing you. – Reza Aghaei Apr 15 '20 at 13:23
  • Could we suppose you are going to show a list of Products having (Id, Name, Price, CategoryId) in a DataGridView and the CategoryId should come from a list of Categories having (Id, Name) and you are going to show CategoryId as a ComboBox? In fact it's a basic and classic example of DataGridViewComboBoxColumn. – Reza Aghaei Apr 15 '20 at 13:59
  • @marky Hope it doesn't look like I'm nagging , but you don't need the **[RESOLVED] ...** part, the green check-mark ✅ tells everything. – Reza Aghaei Apr 15 '20 at 15:45

5 Answers5

2

If I recognize your problem correctly, in my test app i add a DataGridView whit 6 column, EditMode = EditOnEnter (Others need three time click to open dropdown, As far as I tried) and handle CellStateChanged envent.

private void dgvCategories_CellStateChanged(object sender, DataGridViewCellStateChangedEventArgs e)
{
    if (e.StateChanged == DataGridViewElementStates.Selected)
    {
        DataGridViewCell cell = e.Cell;
        int columnIndex = cell.ColumnIndex;
        int rowIndex = cell.RowIndex;
        //---IF CONDITIONS--
        //columnIndex == 5
        //          Only cells in Columns[5]
        //cell.Selected
        //          Because this event raised two time, first for last selected cell and once again
        //          for currently selected cell and we need only currently selected cell.
        //cell.EditType.Name != "DataGridViewComboBoxEditingControl"
        //          If this cell "CellStateChanged" raised for second time, only other cell types allowed
        //          to edit, otherwise the current cell lost last selected item.
        if (columnIndex == 5 && cell.Selected && cell.EditType.Name != "DataGridViewComboBoxEditingControl")
        {
            DataGridViewComboBoxCell cboNewValueList = new DataGridViewComboBoxCell();

            //Add items to DataGridViewComboBoxCell for test, replace it with yours.
            for (int i = 0; i < 10; i++)
                cboNewValueList.Items.Add($"Item {i}");

            dgvCategories[columnIndex, rowIndex] = cboNewValueList;
        }
    }
}

NOTE: user must click two time in a cell to open drop down menu.

Edit One: As Reza Aghaei suggest for single click in cell:

private void dgvCategories_CellClick(object sender, DataGridViewCellEventArgs e)
{
    DataGridViewComboBoxEditingControl editingControl = dgvCategories.EditingControl as DataGridViewComboBoxEditingControl;
    if (editingControl != null)
        editingControl.DroppedDown = true;
}
  • If you set `EditMode` to `EditOnEnter`, then if you click on dropdown button, one click is enough. If you click on cell content, two clicks is needed. If you would like to make it single click even if you click on cell content, take a look at [this post](https://stackoverflow.com/a/32947890/3110834). – Reza Aghaei Apr 15 '20 at 15:12
  • I am forced to leave edit mode : EditOnKeystrokeOrF2 and unfortunately when I set to this value, when choosing a value it throws an exception.. What I have to do??? – lady Jun 06 '23 at 20:26
2

I'm about to publicly admit that I'm stupid:

For design and functionality reasons that are required for this project, I am manually setting the widths and names of the DGV's columns, and I also need the 2nd through 4th columns ReadOnly = true. Well, I inadvertently set the 5th column - the column that this question is about to ReadOnly = true as well.

Thank you all for your attempts at answering. This just serves to remind us how something so simple can cause a seemingly big issue and is so easy to overlook!

marky
  • 4,878
  • 17
  • 59
  • 103
  • 2
    It shows the importance of [MCVE]. It helps you to reproduce the problem in a minimal complete example in a clean environment. The problem that you cannot reproduce, you cannot solve. Then after you reproduced the problem in a clean environment using a minimal code, most likely you can solve the problem yourself. If you couldn't solve it, then it helps the other users to try to solve the exact same problem that you are facing, not a different problem. – Reza Aghaei Apr 15 '20 at 15:38
  • @RezaAghaei. Very true. I'll remember that! – marky Apr 15 '20 at 15:39
  • 1
    Back to the topic of using combo box for editing the cells, the correct way is using `DataGridViewComboBoxColumn`, not creating `DataGridViewComboBoxCell`. It's the path to go. – Reza Aghaei Apr 15 '20 at 15:41
  • It's the simple things and only mention this from MSDN forums where I tried to help. – Karen Payne Apr 18 '20 at 01:15
  • @Reza Aghaei, I assume that using a `DataGridViewComboBoxColumn` would change the entire column of cells to ComboBoxes. If that's the case, that is not how I need that to work in this use case. I only want the cell that the user clicks on to change to a `ComboBox`. – marky Apr 19 '20 at 12:32
  • you can set [`DisplayStyle`](https://learn.microsoft.com/en-us/dotnet/api/system.windows.forms.datagridviewcomboboxcolumn.displaystyle?view=netframework-4.8) to [`Nothing`](https://learn.microsoft.com/en-us/dotnet/api/system.windows.forms.datagridviewcomboboxdisplaystyle?view=netframework-4.8), then it shows as a combo box, just in edit mode. – Reza Aghaei Apr 19 '20 at 13:48
  • I added a screen capture to the answer, so you can see how it looks like at the run-time. I believe it contains all you need. – Reza Aghaei Apr 19 '20 at 14:09
  • It looks like it handles your concern about showing combox jsut on the editing cell. Did you give it a try or watched the screen capture? – Reza Aghaei Apr 21 '20 at 19:38
0

You might need to turn AutoGenerateColumns off:

Also, it seems to take a three clicks for the dropdown to pop.

    public Form1()
    {
        InitializeComponent();
        dataGridView1.AutoGenerateColumns = false;
        dataGridView1.DataSource = GetDataSource();
        DataGridViewComboBoxColumn dgvcbc = new DataGridViewComboBoxColumn();
        dgvcbc.Items.Add("R0C0");
        dgvcbc.Items.Add("R1C0");
        dgvcbc.Items.Add("R2C0");
        dgvcbc.Items.Add("R3C0");
        dgvcbc.DataPropertyName = "Col0";
        dataGridView1.Columns.Add(dgvcbc);
    }

    DataTable GetDataSource()
    {
        var dtb = new DataTable();
        dtb.Columns.Add("Col0", typeof(string));
        dtb.Columns.Add("Col1", typeof(string));
        dtb.Columns.Add("Col2", typeof(string));
        dtb.Columns.Add("Col3", typeof(string));
        dtb.Columns.Add("Col4", typeof(string));
        dtb.Rows.Add("R0C0", "R0C1", "R0C2", "R0C3", "R0C4");
        dtb.Rows.Add("R1C0", "R1C1", "R1C2", "R1C3", "R1C4");
        dtb.Rows.Add("R2C0", "R2C1", "R2C2", "R2C3", "R2C4");
        dtb.Rows.Add("R3C0", "R3C1", "R3C2", "R3C3", "R3C4");
        return dtb;
    }
SSS
  • 4,807
  • 1
  • 23
  • 44
  • I tried `AutoGenerateColumns` in `frmMain.Designer.cs` and the DGV didn't populate at all, so I tried it in the `dgvCategories_Click` event, turning it off at the beginning and on at the end of the event code and the CBO didn't even show up. Plus, I can click the cell many times but the CBO never opens up - it just gets created on the first click with the top item showing and nothing happens after that - it never opens. – marky Apr 15 '20 at 12:29
  • When you set `AutoGenerateColumns` to `false`, you need to specify which columns will display. That's what the `dataGridView1.Columns.Add(dgvcbc)` line in my example does. This technique is useful if you want to have some hidden columns. – SSS Apr 16 '20 at 03:25
0

You can consider the following facts about DataGridView:

  • If you set AutoGenerateColumns to false, then you need to add columns to Columns collection manually.

  • If you set AutoGenerateColumns to true, when you assign the data to DataSource, the control generates columns automatically for the data source. In this case, the control looks in list of columns of the data source and for each column if there's no column in the Columns collection of the control having the same DataPropertyName as data source's column name, it will add a column to Columns collection.

  • DataPropertyName of the datagridviews' columns determines the bound column of the data source.

  • You usually want to add DataGridViewXXXXColumn to columns collection rather than using a DataGridViewXXXXCell for a cell.

  • If you set EditMode to EditOnEnter, then if you click on dropdown button, one click is enough. If you click on cell content, two clicks is needed.

  • If you would like to make it single click even if you click on cell content, take a look at this post. (Note: I haven't used this is the example, it's a bit annoying.)

  • you can set DisplayStyle to Nothing, then it shows the column as a combo box, just in edit mode.

A basic example on using DataGridViewComboBoxColumn

I suppose you are going to show a list of Products having (Id, Name, Price, CategoryId) in a DataGridView and the CategoryId should come from a list of Categories having (Id, Name) and you are going to show CategoryId as a ComboBox.

enter image description here

In fact it's a basic and classic example of DataGridViewComboBoxColumn:

private void Form1_Load(object sender, EventArgs e) {
    var categories = GetCategories();
    var products = GetProducts();
    var idColumn = new DataGridViewTextBoxColumn() {
      Name = "Id", HeaderText = "Id", DataPropertyName = "Id"
    };
    var nameColumn = new DataGridViewTextBoxColumn() {
      Name = "Name", HeaderText = "Name", DataPropertyName = "Name"
    };
    var priceColumn = new DataGridViewTextBoxColumn() {
      Name = "Price", HeaderText = "Price", DataPropertyName = "Price"
    };
    var categoryIdColumn = new DataGridViewComboBoxColumn() {
      Name = "CategoryId", HeaderText = "Category Id", DataPropertyName = "CategoryId",
      DataSource = categories, DisplayMember = "Name", ValueMember = "Id",
      DisplayStyle = DataGridViewComboBoxDisplayStyle.Nothing
    };
    dataGridView1.Columns.AddRange(idColumn, nameColumn, priceColumn, categoryIdColumn);
    dataGridView1.EditMode = DataGridViewEditMode.EditOnEnter;
    dataGridView1.AutoGenerateColumns = false;
    dataGridView1.DataSource = products;
}
public DataTable GetProducts() {
    var products = new DataTable();
    products.Columns.Add("Id", typeof(int));
    products.Columns.Add("Name", typeof(string));
    products.Columns.Add("Price", typeof(int));
    products.Columns.Add("CategoryId", typeof(int));
    products.Rows.Add(1, "Product 1", 100, 1);
    products.Rows.Add(2, "Product 2", 200, 2);
    return products;
}
public DataTable GetCategories() {
    var categories = new DataTable();
    categories.Columns.Add("Id", typeof(int));
    categories.Columns.Add("Name", typeof(string));
    categories.Rows.Add(1, "Category 1");
    categories.Rows.Add(2, "Category 2");
    return categories;
}

Learn more

To learn more about DataGridView, take a look at DataGridView Control (Windows Forms). It contains links to some documentations and useful How To articles, including:

Reza Aghaei
  • 120,393
  • 18
  • 203
  • 398
0

Are you maybe getting an error which is not being shown for some reason?

If I use your code, DataGridViewComboBoxCell seems to be populated with values, but I get DataGridViewComboBoxCell value is not valid runtime error.

This test code is working fine for me:

private void dgvCategories_CellContentClick(object sender, DataGridViewCellEventArgs e)
{
    DataGridViewComboBoxCell cboNewValueList = new DataGridViewComboBoxCell();

    List<string> lsNewValuesResult = new List<string>();
    lsNewValuesResult.Add("Value1");
    lsNewValuesResult.Add("Value2");
    lsNewValuesResult.Add("Value3");

    foreach (string strListItem in lsNewValuesResult)
    {
        cboNewValueList.Items.Add(strListItem);
    }

    dgvCategories[e.ColumnIndex, e.RowIndex] = cboNewValueList;

    // Added setting of initial value
    cboNewValueList.Value = cboNewValueList.Items[0];   
}

So maybe try setting the initial value for your DataGridViewComboBoxCell after you add it to the DataGridView.

Nemanja Banda
  • 796
  • 2
  • 14
  • 23