DataGridView CheckBox 选择错误

DataGridView CheckBox selection bug

我们的应用程序在 DataGridView 中显示了一个项目列表。第一列是 DataGridViewCheckBoxColumn。我们希望我们的应用程序允许用户单击行中的任意位置作为 select 第一列中的 CheckBox 的一种方式。

我们发现如果用户直接点击 CheckBox,selection/deselection 效果很好。如果用户点击其他列中的数据,也是如此。

但是,如果用户只单击复选框的一侧,我们会出现奇怪的行为。该行中的 CheckBox 不是 selected/deselected,但通常另一行是 selected。要更清楚地了解正在发生的事情,您可以查看我的 Short Video of the buggy behavior.

我尝试在代码中设置一些断点,例如,在我们的 SelectionChanged 处理程序、我们的 CellClick 处理程序和我们的 CellValueChanged 处理程序上。我发现这些断点以相同的模式命中,无论我是单击复选框,只是单击复选框的一侧还是其他列中的数据。

有人见过这样的行为吗?任何想法可能会发生什么?是 .NET DataGridView 代码中的错误,还是我应该在代码中查找的内容?

这里是相关代码,按要求(或者你可以下载一个ZIP file with the complete solution)...

来自Form1.cs:

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

来自 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";
}

来自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
}

来自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();
}

我们的代码中是否有什么东西导致了这种行为?还是DataGridView实现的bug?我已经用 .NET Framework v.4.6 和 v.4.8 重现了这个。

(注:转自微软问答论坛,因为我在那里没有得到回复。)

我建议进行这些更改(使用 .Net Framework 4.8 测试):

  1. 不要使用 CheckBox CheckedChanged 事件:它会在尝试更改 Select 时干扰 CellValueChanged 事件全部 复选框状态。请改用 CellClick 事件。
    这也将允许摆脱所有那些添加处理程序/删除处理程序的东西。

  2. 调用RefreshEdit()在单击Cell时立即更新CheckBox Cell的状态:这将立即更新CheckBox值(这是问题 当您在 Cell 区域而不是 CheckBox 内容中单击时,您会看到:控件不会立即更新)。
    有关更多详细信息,请参阅此处的注释:

  3. 删除 CommitEdit(DataGridViewDataErrorContexts.Commit);:如果您需要立即更新值,请改为调用 DataGridView.EndEdit() 方法(也请参阅相关说明)。它在本质上或多或少是相同的东西,但名称本身 - EndEdit - 使其功能更容易理解并且更容易记住。

现在是这样工作的:

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);
}