Bug de seleção de DataGridView CheckBox

Sep 02 2020

Nosso aplicativo possui uma lista de itens exibidos em um DataGridView. A primeira coluna é um DataGridViewCheckBoxColumn. Queremos que nosso aplicativo permita que o usuário clique em qualquer lugar da linha como forma de selecionar o CheckBox na primeira coluna.

Descobrimos que, se o usuário clicar diretamente no CheckBox, a seleção / desmarcação funciona bem. O mesmo é verdadeiro se o usuário clicar nos dados das outras colunas.

No entanto, se o usuário clicar em apenas um lado da caixa de seleção, obteremos um comportamento estranho. A CheckBox nessa linha não é selecionada / desmarcada, mas geralmente outra linha é selecionada. Para ter uma ideia mais clara do que está acontecendo, você pode conferir meu breve vídeo sobre o comportamento de bugs .

Tentei definir alguns pontos de interrupção no código, por exemplo, em nosso manipulador SelectionChanged, nosso manipulador CellClick e nosso manipulador CellValueChanged. Acho que esses pontos de interrupção são atingidos no mesmo padrão, independentemente de eu clicar na caixa de seleção, apenas em um lado da caixa de seleção ou nos dados nas outras colunas.

Alguém viu um comportamento assim? Alguma ideia do que pode estar acontecendo? É um bug no código .NET DataGridView ou há algo que devo procurar em nosso código?

Aqui está o código relevante, conforme solicitado (ou você pode baixar um arquivo ZIP com a solução completa ) ...

De Form1.cs:

public Form1()
{
    InitializeComponent();
    dgsControl.SetUp();
}

De Form1.Designer.cs:

private void InitializeComponent()
{
    this.components = new System.ComponentModel.Container();
    this.dgsControl = new DGSelection();
    this.Controls.Add(this.dgsControl);
    // 
    // dgsControl
    // 
    this.dgsControl.Dock = System.Windows.Forms.DockStyle.Fill;
    this.dgsControl.Location = new System.Drawing.Point(3, 3);
    this.dgsControl.Margin = new System.Windows.Forms.Padding(2, 2, 2, 2);
    this.dgsControl.Name = "dgsControl";
    this.dgsControl.Size = new System.Drawing.Size(689, 325);
    this.dgsControl.TabIndex = 0;

    this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
    this.ClientSize = new System.Drawing.Size(800, 450);
    this.Text = "DataGridView Demo";
}

De DGSelection.cs:

public partial class DGSelection : UserControl
{
    #region Member variables

    private class ListData
    {
        public string Option;
        public string Description;
    }
    private static readonly List<ListData> TestData = new List<ListData>
    {
        new ListData { Option = "Option1", Description = "Description1" },
        new ListData { Option = "Option2", Description = "Description2" },
        new ListData { Option = "Option3", Description = "Description3" },
        new ListData { Option = "Option4", Description = "Description4" }
    };

    public event EventHandler OptionsChanged;
    #endregion

    #region Constructor
    public DGSelection()
    {
        InitializeComponent();
        dgvTable.BackgroundColor = Color.DarkGray;
        dgvTable.DefaultCellStyle.BackColor = Color.DarkGray;
        dgvTable.DefaultCellStyle.ForeColor = Color.Black;
        dgvTable.ColumnHeadersDefaultCellStyle.BackColor = Color.DarkGray;
        dgvTable.ColumnHeadersDefaultCellStyle.ForeColor = Color.Black;
        dgvTable.GridColor = Color.DarkGray;
        cbxCheckAll.BackColor = Color.DarkGray;

        // Move label where it belongs (moved elsewhere in Designer for ease of editing).
        lbl_empty.Top = Top + 5;
    }
    #endregion

    #region Public Methods
    public void SetUp()
    {
        dgvTable.Rows.Clear();
        cbxCheckAll.Checked = false;
        bool anyRows = TestData.Any();
        lbl_empty.Visible = !anyRows;
        cbxCheckAll.Visible = anyRows;
        dgvTable.ColumnHeadersVisible = anyRows;

        foreach (ListData ld in TestData)
        {
            dgvTable.Rows.Add(false, ld.Option, ld.Description);
        }
    }
    #endregion

    #region Event Handlers
    private void DGSelection_SelectionChanged(object sender, EventArgs e)
    {
        dgvTable.ClearSelection();
    }

    private void cbxCheckAll_CheckedChanged(object sender, EventArgs e)
    {
        try
        {
            dgvTable.CellValueChanged -= DgvTableCellValueChanged;
            bool checkAll = cbxCheckAll.Checked;
            foreach (DataGridViewRow row in dgvTable.Rows)
                row.Cells[0].Value = checkAll;
        }
        finally
        {
            dgvTable.CellValueChanged += DgvTableCellValueChanged;
        }
        OptionsChanged?.Invoke(this, EventArgs.Empty);
    }

    private void DGSelection_CellClick(object sender, DataGridViewCellEventArgs e)
    {
        if (e.RowIndex < 0)
            return; // Ignore clicks in the header row

        DataGridViewCell checkBoxCell = dgvTable.Rows[e.RowIndex].Cells[0];
        checkBoxCell.Value = !(bool)checkBoxCell.Value;
    }

    private void DgvTableCellValueChanged(object sender, DataGridViewCellEventArgs e)
    {
        try
        {
            cbxCheckAll.CheckedChanged -= cbxCheckAll_CheckedChanged;
            cbxCheckAll.CheckedChanged -= cbxCheckAll_CheckedChanged;
            // Not sure why, but sometimes subscribed twice

            bool checkAll = dgvTable.Rows.Count > 0;
            foreach (DataGridViewRow row in dgvTable.Rows)
                checkAll &= row.Cells[0].Value.Equals(true);
            cbxCheckAll.Checked = checkAll;
        }
        finally
        {
            cbxCheckAll.CheckedChanged += cbxCheckAll_CheckedChanged;
        }
        OptionsChanged?.Invoke(this, EventArgs.Empty);
    }

    private void DGSelection_CurrentCellDirtyStateChanged(object sender, EventArgs e)
    {
        dgvTable.CommitEdit(DataGridViewDataErrorContexts.Commit);
    }
    #endregion
}

De DGSelection.Designer.cs:

private void InitializeComponent()
{
    components = new System.ComponentModel.Container();
    System.Windows.Forms.DataGridViewCellStyle dataGridViewCellStyle1 = new System.Windows.Forms.DataGridViewCellStyle();
    System.Windows.Forms.DataGridViewCellStyle dataGridViewCellStyle2 = new System.Windows.Forms.DataGridViewCellStyle();
    System.Windows.Forms.DataGridViewCellStyle dataGridViewCellStyle3 = new System.Windows.Forms.DataGridViewCellStyle();
    this.dgvTable = new System.Windows.Forms.DataGridView();
    this.colCheckboxes = new System.Windows.Forms.DataGridViewCheckBoxColumn();
    this.colText1 = new System.Windows.Forms.DataGridViewTextBoxColumn();
    this.colText2 = new System.Windows.Forms.DataGridViewTextBoxColumn();
    this.cbxCheckAll = new System.Windows.Forms.CheckBox();
    this.lbl_empty = new System.Windows.Forms.Label();
    ((System.ComponentModel.ISupportInitialize)(this.dgvTable)).BeginInit();
    this.SuspendLayout();
    // 
    // dgvTable
    // 
    this.dgvTable.AllowUserToAddRows = false;
    this.dgvTable.AllowUserToDeleteRows = false;
    this.dgvTable.AllowUserToResizeColumns = false;
    this.dgvTable.AllowUserToResizeRows = false;
    this.dgvTable.AutoSizeRowsMode = System.Windows.Forms.DataGridViewAutoSizeRowsMode.AllCells;
    this.dgvTable.BackgroundColor = System.Drawing.SystemColors.Window;
    this.dgvTable.BorderStyle = System.Windows.Forms.BorderStyle.None;
    this.dgvTable.CellBorderStyle = System.Windows.Forms.DataGridViewCellBorderStyle.None;
    this.dgvTable.ColumnHeadersBorderStyle = System.Windows.Forms.DataGridViewHeaderBorderStyle.Single;
    dataGridViewCellStyle1.Alignment = System.Windows.Forms.DataGridViewContentAlignment.MiddleLeft;
    dataGridViewCellStyle1.BackColor = System.Drawing.SystemColors.ControlDark;
    dataGridViewCellStyle1.Font = new System.Drawing.Font("Microsoft Sans Serif", 8.25F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(0)));
    dataGridViewCellStyle1.ForeColor = System.Drawing.SystemColors.WindowText;
    dataGridViewCellStyle1.Padding = new System.Windows.Forms.Padding(3);
    dataGridViewCellStyle1.SelectionBackColor = System.Drawing.SystemColors.Highlight;
    dataGridViewCellStyle1.SelectionForeColor = System.Drawing.SystemColors.HighlightText;
    dataGridViewCellStyle1.WrapMode = System.Windows.Forms.DataGridViewTriState.True;
    this.dgvTable.ColumnHeadersDefaultCellStyle = dataGridViewCellStyle1;
    this.dgvTable.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize;
    this.dgvTable.Columns.AddRange(new System.Windows.Forms.DataGridViewColumn[] {
        this.colCheckboxes,
        this.colText1,
        this.colText2 });
    this.dgvTable.Dock = System.Windows.Forms.DockStyle.Fill;
    this.dgvTable.EditMode = System.Windows.Forms.DataGridViewEditMode.EditOnEnter;
    this.dgvTable.EnableHeadersVisualStyles = false;
    this.dgvTable.Location = new System.Drawing.Point(0, 0);
    this.dgvTable.Margin = new System.Windows.Forms.Padding(2);
    this.dgvTable.MultiSelect = false;
    this.dgvTable.Name = "dgvTable";
    this.dgvTable.RowHeadersVisible = false;
    this.dgvTable.RowTemplate.Height = 24;
    this.dgvTable.ScrollBars = System.Windows.Forms.ScrollBars.Vertical;
    this.dgvTable.SelectionMode = System.Windows.Forms.DataGridViewSelectionMode.CellSelect;
    this.dgvTable.Size = new System.Drawing.Size(484, 318);
    this.dgvTable.TabIndex = 0;
    this.dgvTable.CellClick += new System.Windows.Forms.DataGridViewCellEventHandler(this.DGSelection_CellClick);
    this.dgvTable.CellValueChanged += new System.Windows.Forms.DataGridViewCellEventHandler(this.DgvTableCellValueChanged);
    this.dgvTable.CurrentCellDirtyStateChanged += new System.EventHandler(this.DGSelection_CurrentCellDirtyStateChanged);
    this.dgvTable.SelectionChanged += new System.EventHandler(this.DGSelection_SelectionChanged);
    // 
    // colCheckboxes
    // 
    this.colCheckboxes.AutoSizeMode = System.Windows.Forms.DataGridViewAutoSizeColumnMode.None;
    this.colCheckboxes.Frozen = true;
    this.colCheckboxes.HeaderText = "";
    this.colCheckboxes.Name = "colCheckboxes";
    this.colCheckboxes.Resizable = System.Windows.Forms.DataGridViewTriState.False;
    this.colCheckboxes.Width = 30;
    // 
    // colText1
    // 
    this.colText1.AutoSizeMode = System.Windows.Forms.DataGridViewAutoSizeColumnMode.AllCells;
    dataGridViewCellStyle2.Padding = new System.Windows.Forms.Padding(5, 0, 0, 0);
    this.colText1.DefaultCellStyle = dataGridViewCellStyle2;
    this.colText1.HeaderText = "Option";
    this.colText1.Name = "colText1";
    this.colText1.ReadOnly = true;
    this.colText1.Resizable = System.Windows.Forms.DataGridViewTriState.False;
    this.colText1.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.NotSortable;
    this.colText1.Width = 57;
    // 
    // colText2
    // 
    this.colText2.AutoSizeMode = System.Windows.Forms.DataGridViewAutoSizeColumnMode.Fill;
    dataGridViewCellStyle3.Padding = new System.Windows.Forms.Padding(5, 0, 0, 0);
    this.colText2.DefaultCellStyle = dataGridViewCellStyle3;
    this.colText2.HeaderText = "Description";
    this.colText2.Name = "colText2";
    this.colText2.ReadOnly = true;
    this.colText2.Resizable = System.Windows.Forms.DataGridViewTriState.False;
    this.colText2.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.NotSortable;
    // 
    // cbxCheckAll
    // 
    this.cbxCheckAll.AutoSize = true;
    this.cbxCheckAll.BackColor = System.Drawing.SystemColors.ControlDark;
    this.cbxCheckAll.Location = new System.Drawing.Point(8, 5);
    this.cbxCheckAll.Margin = new System.Windows.Forms.Padding(2);
    this.cbxCheckAll.Name = "cbxCheckAll";
    this.cbxCheckAll.Size = new System.Drawing.Size(15, 14);
    this.cbxCheckAll.TabIndex = 1;
    this.cbxCheckAll.UseVisualStyleBackColor = false;
    this.cbxCheckAll.CheckedChanged += new System.EventHandler(this.cbxCheckAll_CheckedChanged);
    // 
    // lbl_empty
    // 
    this.lbl_empty.Anchor = ((System.Windows.Forms.AnchorStyles)
        (((System.Windows.Forms.AnchorStyles.Top |
           System.Windows.Forms.AnchorStyles.Left) |
           System.Windows.Forms.AnchorStyles.Right)));
    this.lbl_empty.BackColor = System.Drawing.Color.Transparent;
    this.lbl_empty.Location = new System.Drawing.Point(3, 25);
    this.lbl_empty.Name = "lbl_empty";
    this.lbl_empty.Size = new System.Drawing.Size(478, 44);
    this.lbl_empty.TabIndex = 2;
    this.lbl_empty.Text = "No data defined for the list";
    this.lbl_empty.Visible = false;
    // 
    // DGSelection
    // 
    this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
    this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
    this.Controls.Add(this.lbl_empty);
    this.Controls.Add(this.cbxCheckAll);
    this.Controls.Add(this.dgvTable);
    this.Margin = new System.Windows.Forms.Padding(2);
    this.Name = "DGSelectionControl";
    this.Size = new System.Drawing.Size(484, 318);
    ((System.ComponentModel.ISupportInitialize)(this.dgvTable)).EndInit();
    this.ResumeLayout(false);
    this.PerformLayout();
}

Existe algo em nosso código que está causando esse comportamento? Ou é um bug na implementação do DataGridView? Eu reproduzi isso com o .NET Framework v.4.6 e v.4.8.

(Observação: postado novamente do Fórum de perguntas e respostas da Microsoft, já que não recebi respostas lá.)

Respostas

2 Jimi Sep 02 2020 at 01:01

Eu sugiro estas mudanças (testadas com .Net Framework 4.8):

  1. Não use o CheckedChangedevento CheckBox : ele irá interferir com o CellValueChangedevento quando tentar alterar o estado Selecionar Todos CheckBox. Em Clickvez disso, use o evento.
    Isso também permitirá que você se livre de todas as coisas de adicionar / remover manipulador.

  2. Chame RefreshEdit () para atualizar o estado da célula do CheckBox assim que a célula for clicada: isso atualizará o valor do CheckBox imediatamente (este é o problema que você está vendo ao clicar dentro da área da célula em vez do conteúdo do CheckBox: o controle não é atualizado imediatamente).
    Para obter mais detalhes, consulte as notas aqui:
    Verifique programaticamente uma DataGridView CheckBox que acabou de ser desmarcada

  3. Remova isso CommitEdit(DataGridViewDataErrorContexts.Commit);: se você precisar atualizar um valor imediatamente, chame o método DataGridView.EndEdit () (consulte também essas notas sobre isso).

É assim que funciona agora:

private void cbxCheckAll_Click(object sender, EventArgs e)
{
    if (dgvTable.Rows.Count == 0) return;
    try {
        bool checkAll = cbxCheckAll.Checked;
        foreach (DataGridViewRow row in dgvTable.Rows) {
            row.Cells[0].Value = checkAll;
        }
    }
    finally {
        OptionsChanged?.Invoke(this, EventArgs.Empty);
    }
}

private void DGSelection_CellClick(object sender, DataGridViewCellEventArgs e)
{
    if (e.RowIndex < 0) return;

    bool currentValue = (bool)dgvTable[0, e.RowIndex].Value;
    dgvTable[0, e.RowIndex].Value = !currentValue;
}

private void DgvTableCellValueChanged(object sender, DataGridViewCellEventArgs e)
{
    if (!dgvTable.IsHandleCreated) return;
    cbxCheckAll.Checked = dgvTable.Rows.OfType<DataGridViewRow>().All(r => (bool)r.Cells[0].Value == true);
    dgvTable.BeginInvoke(new Action(() => dgvTable.RefreshEdit()));
    OptionsChanged?.Invoke(this, EventArgs.Empty);
}