0

I'm using C# WinForms .NET 4.7.2. I'm encountering what appears to be a bug when using DataGridViewComboBoxColumns in a DataGridView. When I enter data into it with auto-append (where the first-character of data-entry is all that's required), I see the correct drop-down item appear. If I quickly tab to the next cell, sometimes the entered value disappears from the ComboBox cell. This only happens when I enter and leave the column quickly (tabbing along as most data-entry personnel would). This bug forces the data-entry person to go back to the blank column and try again. If they're too fast again, it can stay blank again. I've tested a number of scenarios and events and cannot pin down what's causing this, as setting break points in (e.g.) the CurrentCellDirtyStateChanged event causes the bug to no longer appear. It certainly seems speed-related, as I cannot reproduce the behaviour when I'm tabbing through more slowly.

It would be great if it wasn't a bug and you could tell me what I'm doing wrong to cause this behaviour. Otherwise, if it is a bug, work-around ideas that forces the value to stick would be great.

I'm able to reproduce the bug with the following new project. The bug is a little more pronounced in my real project, so I suppose the bug may get worse with scale. Strange that I haven't encountered this before, as this isn't my first time using ComboBoxColumns in a DataGridView.

How to recreate:

  1. Create a new C# WinForms project in VS (I'm using MS VS Pro 2019 RC V 16)
  2. Add a DataGridView to the form and fully dock it
  3. Code for the form:
using System;
using System.Data;
using System.Windows.Forms;

namespace TestingStuff
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }
        DataTable dtString1;
        DataTable dtString2;
        DataTable dtString3;
        private void Form1_Load(object sender, EventArgs e)
        {
            // create three combobox columns and put them side-by-side:
            // first column:
            DataGridViewComboBoxColumn dgvcbc1 = new DataGridViewComboBoxColumn();
            dgvcbc1.DataPropertyName = "String1";
            dgvcbc1.Name = "String1";

            dtString1 = new DataTable("String1Options");
            dtString1.Columns.Add("String1Long", typeof(string));

            dtString1.Rows.Add("apple");
            dtString1.Rows.Add("bob");
            dtString1.Rows.Add("clobber");
            dtString1.Rows.Add("dilbert");
            dtString1.Rows.Add("ether");

            dgv.Columns.Insert(0, dgvcbc1);

            dgvcbc1.DisplayMember = dtString1.Columns[0].ColumnName;
            dgvcbc1.ValueMember = dtString1.Columns[0].ColumnName;
            dgvcbc1.DataSource = dtString1;

            dgvcbc1.FlatStyle = FlatStyle.Flat;

            // create the second column:
            DataGridViewComboBoxColumn dgvcbc2 = new DataGridViewComboBoxColumn();
            dgvcbc2.DataPropertyName = "String2";
            dgvcbc2.Name = "String2";

            dtString2 = new DataTable("String2Options");
            dtString2.Columns.Add("String2Long", typeof(string));
            
            dtString2.Rows.Add("apple");
            dtString2.Rows.Add("bob");
            dtString2.Rows.Add("clobber");
            dtString2.Rows.Add("dilbert");
            dtString2.Rows.Add("ether");
            
            dgv.Columns.Insert(1, dgvcbc2);

            dgvcbc2.DisplayMember = dtString2.Columns[0].ColumnName;
            dgvcbc2.ValueMember = dtString2.Columns[0].ColumnName;
            dgvcbc2.DataSource = dtString2;

            dgvcbc2.FlatStyle = FlatStyle.Flat;

            // create the third column:
            DataGridViewComboBoxColumn dgvcbc3 = new DataGridViewComboBoxColumn();
            dgvcbc3.DataPropertyName = "String3";
            dgvcbc3.Name = "String3";

            dtString3 = new DataTable("String3Options");
            dtString3.Columns.Add("String3Long", typeof(string));

            dtString3.Rows.Add("apple");
            dtString3.Rows.Add("bob");
            dtString3.Rows.Add("clobber");
            dtString3.Rows.Add("dilbert");
            dtString3.Rows.Add("ether");

            dgv.Columns.Insert(2, dgvcbc3);

            dgvcbc3.DisplayMember = dtString3.Columns[0].ColumnName;
            dgvcbc3.ValueMember = dtString3.Columns[0].ColumnName;
            dgvcbc3.DataSource = dtString3;

            dgvcbc3.FlatStyle = FlatStyle.Flat;

        }
    }
}
  1. Run it
  2. Enter your data quickly (using autocomplete and [Tab] key efficiently)
  3. Notice that your DataGridViewComboBoxColumn values sometimes disappear immediately after leaving the cell

Edit: In addition, I've noticed that this bug only occurs on cells that are currently blank - the bug cannot be reproduced when editing a cell from one value to another.

Thanks for any insight.

Sturgus
  • 666
  • 4
  • 18
  • Using the posted code I was unable to re-create _”that your DataGridViewComboBoxColumn values sometimes disappear immediately after leaving the cell”_ … What is the `CurrentCellDirtyStateChanged` event doing when it fires and why did you not post that code? – JohnG Dec 04 '19 at 03:46
  • @JohnG, thank you for taking the time to try it. It does seem difficult to reproduce today. The `CurrentCellDirtyStateChanged` event is unhandled by me and the issue still persists. In my main project, I stuck a line break in that event and couldn't reproduce the issue. I will update my code if I can figure out how to make the bug more evident. – Sturgus Dec 04 '19 at 14:53
  • @JohnG, alright, the revised code makes the bug far more evident. The major change now being that all three columns are `ComboBox`s. – Sturgus Dec 04 '19 at 15:19

1 Answers1

1

I found the bug easy to reproduce with your latest code, manually entering [a] [Tab] [b] [Tab] [c] [Tab] either slow (works) or fast (fails). I also added A-B-C and C-D-E buttons to send key events very rapidly to test in a repeatable way. Below the proposed solution is a debug trace I performed to see what's going on. What I found curious is that the DataGridViewComboBoxEditingControl always gains focus with its Text value equal to "apple". I decided to try setting the first value for the Column.DataSource to String.Empty instead.

You may have seen that I had already found various ways to make a work-around that forces the value to stick. But I'm changing my answer because this looks like a real honest solution.

PROPOSED SOLUTION - ADD THESE THREE LINES TO THE LOAD CODE

protected override void OnLoad(EventArgs e)
{
    base.OnLoad(e);

    // create three combobox columns and put them side-by-side:
    // first column:
    DataGridViewComboBoxColumn dgvcbc1 = new DataGridViewComboBoxColumn();
    dgvcbc1.DataPropertyName = "String1";
    dgvcbc1.Name = "String1";

    dtString1 = new DataTable("String1Options");
    dtString1.Columns.Add("String1Long", typeof(string));

    dtString1.Rows.Add(String.Empty);   // Add this line
    dtString1.Rows.Add("apple");
    dtString1.Rows.Add("bob");
    dtString1.Rows.Add("clobber");
    dtString1.Rows.Add("dilbert");
    dtString1.Rows.Add("ether");

    dgv.Columns.Insert(0, dgvcbc1);

    dgvcbc1.DisplayMember = dtString1.Columns[0].ColumnName;
    dgvcbc1.ValueMember = dtString1.Columns[0].ColumnName;
    dgvcbc1.DataSource = dtString1;

    dgvcbc1.FlatStyle = FlatStyle.Flat;

    // create the second column:
    DataGridViewComboBoxColumn dgvcbc2 = new DataGridViewComboBoxColumn();
    dgvcbc2.DataPropertyName = "String2";
    dgvcbc2.Name = "String2";

    dtString2 = new DataTable("String2Options");
    dtString2.Columns.Add("String2Long", typeof(string));

    dtString2.Rows.Add(String.Empty);   // Add this line
    dtString2.Rows.Add("apple");
    dtString2.Rows.Add("bob");
    dtString2.Rows.Add("clobber");
    dtString2.Rows.Add("dilbert");
    dtString2.Rows.Add("ether");

    dgv.Columns.Insert(1, dgvcbc2);

    dgvcbc2.DisplayMember = dtString2.Columns[0].ColumnName;
    dgvcbc2.ValueMember = dtString2.Columns[0].ColumnName;
    dgvcbc2.DataSource = dtString2;

    dgvcbc2.FlatStyle = FlatStyle.Flat;

    // create the third column:
    DataGridViewComboBoxColumn dgvcbc3 = new DataGridViewComboBoxColumn();
    dgvcbc3.DataPropertyName = "String3";
    dgvcbc3.Name = "String3";

    dtString3 = new DataTable("String3Options");
    dtString3.Columns.Add("String3Long", typeof(string));

    dtString3.Rows.Add(String.Empty);   // Add this line
    dtString3.Rows.Add("apple");
    dtString3.Rows.Add("bob");
    dtString3.Rows.Add("clobber");
    dtString3.Rows.Add("dilbert");
    dtString3.Rows.Add("ether");

    dgv.Columns.Insert(2, dgvcbc3);

    dgvcbc3.DisplayMember = dtString3.Columns[0].ColumnName;
    dgvcbc3.ValueMember = dtString3.Columns[0].ColumnName;
    dgvcbc3.DataSource = dtString3;

    dgvcbc3.FlatStyle = FlatStyle.Flat;
}

My theory as to why this would be an effective fix: The empty entry guarantees that any non-empty match in the auto-append list will register as a 'change'. See if you can repro. I get 0 failures on this version with either manual entry or button clicks.

graphic showing no blanks

DEBUGGING AND TESTING INFORMATION FOLLOWS

I placed Debug.WriteLine() on various events to see what's going on.

NORMAL (slow)

onCurrentCellChanged [0, 0]
onCellBeginEdit
CBEdit got focus with text='apple'
Key down 'A'
onCurrentCellDirtyStateChanged True True
Key down 'Tab'
onCurrentCellDirtyStateChanged False True
CBEdit losing focus with text='apple'
onCellEndEdit
onCurrentCellChanged [1, 0]
onCellBeginEdit
CBEdit got focus with text='apple'
Key down 'B'
onCurrentCellDirtyStateChanged True True
Key down 'Tab'
onCurrentCellDirtyStateChanged False True
CBEdit losing focus with text='bob'
onCellEndEdit
onCurrentCellChanged [2, 0]
onCellBeginEdit
CBEdit got focus with text='apple'
Key down 'C'
onCurrentCellDirtyStateChanged True True
Key down 'Tab'
onCurrentCellDirtyStateChanged False True
CBEdit losing focus with text='clobber'
onCellEndEdit
onCurrentCellChanged [0, 1]

PATHOLOGICAL (fast)

onCurrentCellChanged [0, 1]
onCellBeginEdit
CBEdit got focus with text='apple'
Key down 'A'
onCurrentCellDirtyStateChanged True True
Key down 'Tab'
onCurrentCellDirtyStateChanged False True
CBEdit losing focus with text='apple'
onCellEndEdit
onCurrentCellChanged [1, 1]
onCellBeginEdit
CBEdit got focus with text='bob'
Key down 'B'
// Missing event (Insufficient time for list search?)
Key down 'Tab'
CBEdit losing focus with text='bob'
onCellEndEdit
onCurrentCellChanged [2, 1]
onCellBeginEdit
CBEdit got focus with text='clobber'
Key down 'C'
// Missing event (Insufficient time for list search?)
Key down 'Tab'
CBEdit losing focus with text='clobber'
onCellEndEdit
onCurrentCellChanged [0, 2]

AUTOMATED TESTING UPDATE (from the "having too much fun" department)

It bothered me that there was so much variable with this manual testing interval. If we're ever to get to the bottom this, there has to be a way to send the keystrokes rapidly in an automated way.

private void buttonABC_Click(object sender, EventArgs e)
{
    SendKeyPlusTab("abc");
}

private void buttonCDE_Click(object sender, EventArgs e)
{
    SendKeyPlusTab("cde");
}

public void SendKeyPlusTab(string keys)
{
    var nRowsB4 = dgv.Rows.Count;
    if (!dgv.Focused)
    {
        dgv.Focus();
        Task.Delay(100).Wait();
    }
    // Send consecutive keys as fast as
    // possible with a tab after each.
    foreach (var key in keys)
    {
        SendKeys.SendWait($"{key}\t");
    }
}

Thanks for the brain teaser regardless of outcomes it's been a blast. Cheers.

IVSoftware
  • 5,732
  • 2
  • 12
  • 23
  • Here's the [GitHub repo](https://github.com/IVSoftware/speed_based_data_entry_mre_2.git) for the .NET Framework 4.7 project that I have been using to test. – IVSoftware Jun 12 '22 at 12:53
  • Thanks for your efforts. Adding the string.Empty does fit as a work-around. Though in my actual solution, I have some numeric columns where this doesn't work, so I have to convert back and forth between numbers and strings. Extra work for me, but worth it for those using it! – Sturgus Jun 13 '22 at 18:17
  • For sure! BTW I hid my first answer but if you want to want to see the various workarounds I did by handling the `DataGridView.EditingControlShowing` event (and grabbing the `DataGridViewComboBoxEditingControl` as the column goes into edit mode) here's [my first attempt](https://github.com/IVSoftware/speed_based_data_entry_mre) that still has all the debugging hooks in. For that matter since you've been so generous you know I'd throw in the first answer for free if you want it :) – IVSoftware Jun 13 '22 at 18:55