How to fix speed-based data-entry bug for DataGridViewComboBoxColumns?

186 Views Asked by At

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.

1

There are 1 best solutions below

3
IV. On

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.