WinForms, event unable to subscribe from a custom class

76 views Asked by At

I have a custom class CheckboxHeaderCell inherit from DataGridViewColumnHeaderCell to add checkbox into any datagridview. The checkbox added successfully, but on checkbox checked event only custom class event trigger but the event in the form (method HeaderCell_CheckedChanged) where I added this checkbox never triggers.

enter image description here

Class: CheckboxHeaderCell

using System;
using System.Drawing;
using System.Windows.Forms;

namespace test.CustomControls
{
    public class CheckboxHeaderCell : DataGridViewColumnHeaderCell
    {
        public event EventHandler CheckedChanged;

        public CheckBox checkBox;
        public bool IsChecked = false;

        public CheckboxHeaderCell()
        {
            checkBox = new CheckBox();
            checkBox.BackColor = SystemColors.Control;
            checkBox.Margin = new Padding(0);
            checkBox.Padding = new Padding(0);
            checkBox.Size = new Size(14, 14);
            checkBox.UseVisualStyleBackColor = true;
            checkBox.CheckedChanged += CheckBox_CheckedChanged;
            CheckedChanged = new EventHandler(doNothing);
        }

        public void CheckBox_CheckedChanged(object sender, EventArgs e)
        {
            IsChecked = checkBox.Checked;
            CheckedChanged?.Invoke(this, EventArgs.Empty);
            if (DataGridView != null)
            {
                DataGridView.InvalidateCell(this);
            }
        }

        public void doNothing(object sender, EventArgs e)
        {

        }
        //other methods, paint etc
    }
}

I used this class to add custom checkbox into my dgv using below method:

private void AddCheckBoxDGV(DataGridView dgv, string columnName)
    {
        if (dgv.Columns.Contains(columnName))
        {
            CustomControls.CheckboxHeaderCell checkboxHeaderCell = new CustomControls.CheckboxHeaderCell();
            checkboxHeaderCell.CheckedChanged += HeaderCell_CheckedChanged;

            dgv.Columns[columnName].HeaderCell = checkboxHeaderCell;
        }
    }
private void HeaderCell_CheckedChanged(object sender, EventArgs e)
    {
       //do smthing
    }
1

There are 1 answers

5
dr.null On

Your custom HeaderCell does nothing but creates a CheckBox that you will never see or access as UI element. Neither on the HeaderCell nor anywhere else on the grid. So, I don't understand how:

The checkbox added successfully ...

You must add a Control to some Parent.Controls collection and that Parent must be added directly (itself) or indirectly (its parent) to a top-level control. A Form for example. The HeaderCell is not a Control to be used as such.

To proof the concept, add the CheckBox to yourGrid.Controls, you'll see it at the very top left, tick it and you'll get the CheckedChanged event raised.


I understand that you need a custom DataGridViewCheckBoxColumn with a master CheckBox on the HeaderCell to check/uncheck all. Here's an example.

HeaderCell

In the first section, derive a new class from DataGridViewColumnHeaderCell and,

  • Add a property to get/set the CheckState of the master CheckBox. Checked if all the cells are checked, Unchecked if non, and Indeterminate if not all are checked.
  • Override the OnMouseClick method to toggle the state and set the property which,
  • The setter calls a method to set the Value property of the owning column's cells to true or false.
  • Add an event to raise when the current CheckState changes to different value.
public class CheckAllColumnHeaderCell : DataGridViewColumnHeaderCell
{
    enum MouseState : int
    {
        Normal,
        Hot,
        Down
    }
        
    CheckState _checkState = CheckState.Unchecked;
    MouseState _mouseState = MouseState.Normal; // See the mouse methods...
    Rectangle _checkBoxRect; // See the Paint method...
    bool _suppressUpdate = false; // To avoid redundant calls...

    public CheckState CheckState
    {
        get => _checkState;
        set
        {
            if (_checkState != value)
            {
                if (DataGridView.RowCount == 0 ||
                    DataGridView.Rows[0].IsNewRow) return;

                _checkState = value;
                DataGridView?.InvalidateCell(this);
                SetCellsCheckState();
                OnCheckedChanged(new HeaderCellCheckedChangedEventArgs(value));
            }
        }
    }

    private void SetCellsCheckState()
    {
        if (CheckState != CheckState.Indeterminate)
        {
            bool state = CheckState == CheckState.Checked;
            foreach (var row in DataGridView.Rows.Cast<DataGridViewRow>())
            {
                if (!row.IsNewRow) row.Cells[ColumnIndex].Value = state;
            }
        }
    }

    protected override void OnMouseClick(DataGridViewCellMouseEventArgs e)
    {
        base.OnMouseClick(e);

        if (e.Button == MouseButtons.Left && 
            DataGridView.RowCount > 0 && !DataGridView.Rows[0].IsNewRow &&
            _checkBoxRect.Contains(DataGridView.PointToClient(Cursor.Position)))
        {
            _suppressUpdate = true;
            CheckState = CheckState == CheckState.Checked || 
                CheckState == CheckState.Indeterminate
                ? CheckState.Unchecked
                : CheckState.Checked;
            _suppressUpdate = false;
        }
    }

    public event EventHandler<HeaderCellCheckedChangedEventArgs> CheckedChanged;

    protected virtual void OnCheckedChanged(HeaderCellCheckedChangedEventArgs e)
    {
        CheckedChanged?.Invoke(this, e);
    }

The second section is to update the state of the master CheckBox when some relevant events of the DataGridView occur. For example, when the user tick/untick a cell of the owning column, when adding or removing rows ...

    protected override void OnDataGridViewChanged()
    {
        base.OnDataGridViewChanged();

        if (DataGridView != null)
        {
            DataGridView.CurrentCellDirtyStateChanged -= OnCurrentCellDirtyStateChanged;
            DataGridView.CellValueChanged -= OnDataGridViewOnCellValueChanged;
            DataGridView.RowsAdded -= OnDataGridViewRowsAdded;
            DataGridView.RowsRemoved -= OnDataGridViewRowsRemoved;

            DataGridView.CurrentCellDirtyStateChanged += OnCurrentCellDirtyStateChanged;
            DataGridView.CellValueChanged += OnDataGridViewOnCellValueChanged;
            DataGridView.RowsAdded += OnDataGridViewRowsAdded;
            DataGridView.RowsRemoved += OnDataGridViewRowsRemoved;
        }
    }

    private void OnDataGridViewOnCellValueChanged(object sender,
        DataGridViewCellEventArgs e)
    {
        if (e.RowIndex >= 0 && e.ColumnIndex == ColumnIndex)
        {
            if (DataGridView.NewRowIndex == e.RowIndex)
                DataGridView.NotifyCurrentCellDirty(true);
            else
                UpdateCheckState();
        }
    }

    private void OnDataGridViewRowsAdded(object sender,
        DataGridViewRowsAddedEventArgs e) => UpdateCheckState();

    private void OnDataGridViewRowsRemoved(object sender,
        DataGridViewRowsRemovedEventArgs e) => UpdateCheckState();

    private void OnCurrentCellDirtyStateChanged(object sender, EventArgs e)
    {
        if (DataGridView.CurrentCell?.ColumnIndex == ColumnIndex)
            UpdateCheckState();
    }

    private void UpdateCheckState()
    {            
        if (DataGridView is null) return;
        DataGridView.EndEdit();
        DataGridView.InvalidateCell(this);
        if (_suppressUpdate) return;
        int total = DataGridView.RowCount;
        if (DataGridView.AllowUserToAddRows) total--;
        int chkCount = DataGridView.Rows.Cast<DataGridViewRow>()
            .Aggregate(0, (count, row) =>
            {
                count += (bool)row.Cells[ColumnIndex].EditedFormattedValue ? 1 : 0;
                return count;
            });

        var current = chkCount == 0
            ? CheckState.Unchecked
            : chkCount == total
            ? CheckState.Checked
            : CheckState.Indeterminate;

        if (current != _checkState)
        {
            _checkState = current;                
            OnCheckedChanged(new HeaderCellCheckedChangedEventArgs(_checkState));
        }
    }

The last section here, drawing the master CheckBox. Override the mouse events to update the mouse state which is used in the Paint method to determine which CheckBoxState to draw. I'll use the CheckBoxRenderer to draw the CheckBox in this example. You can try the ControlPaint.DrawCheckBox instead or draw it the way you want.

    protected override void OnMouseMove(DataGridViewCellMouseEventArgs e)
    {
        base.OnMouseMove(e);

        if (Control.MouseButtons != MouseButtons.None) return;

        if (_checkBoxRect.Contains(DataGridView.PointToClient(Cursor.Position)))
        {
            if (_mouseState != MouseState.Hot)
            {
                _mouseState = MouseState.Hot;
                DataGridView.InvalidateCell(this);
            }
        }
        else if (_mouseState != MouseState.Normal)
        {
            _mouseState = MouseState.Normal;
            DataGridView.InvalidateCell(this);
        }
    }

    protected override void OnMouseDown(DataGridViewCellMouseEventArgs e)
    {
        base.OnMouseDown(e);

        if (e.Button == MouseButtons.Left &&
            _checkBoxRect.Contains(DataGridView.PointToClient(Cursor.Position)))
        {
            _mouseState = MouseState.Down;
            DataGridView.InvalidateCell(this);
        }
    }

    protected override void OnMouseUp(DataGridViewCellMouseEventArgs e)
    {
        base.OnMouseUp(e);

        if (e.Button == MouseButtons.Left && _checkBoxRect
            .Contains(DataGridView.PointToClient(Cursor.Position)))
        {
            _mouseState = MouseState.Normal;
            DataGridView.InvalidateCell(this);
        }
    }

    protected override void OnMouseLeave(int rowIndex)
    {
        base.OnMouseLeave(rowIndex);

        if (_mouseState != MouseState.Normal)
        {
            _mouseState = MouseState.Normal;
            DataGridView.InvalidateCell(this);
        }
    }

    protected override void Paint(Graphics graphics,
        Rectangle clipBounds,
        Rectangle cellBounds,
        int rowIndex,
        DataGridViewElementStates dataGridViewElementState,
        object value,
        object formattedValue,
        string errorText,
        DataGridViewCellStyle cellStyle,
        DataGridViewAdvancedBorderStyle advancedBorderStyle,
        DataGridViewPaintParts paintParts)
    {
        // You don't want to draw the HeaderText. Do you?
        // If yes, comment this and don't center the 
        // check box horizontally.
        paintParts &= ~DataGridViewPaintParts.ContentForeground;

        base.Paint(graphics,
            clipBounds,
            cellBounds,
            rowIndex,
            dataGridViewElementState,
            value,
            formattedValue,
            errorText,
            cellStyle,
            advancedBorderStyle,
            paintParts);

        bool enabled = DataGridView.Enabled;
        CheckBoxState state = CheckBoxState.UncheckedNormal;

        switch (CheckState)
        {
            case CheckState.Checked:
                state = !enabled ? CheckBoxState.CheckedDisabled
                    : _mouseState == MouseState.Down
                    ? CheckBoxState.CheckedPressed
                    : _mouseState == MouseState.Hot
                    ? CheckBoxState.CheckedHot
                    : CheckBoxState.CheckedNormal;
                break;
            case CheckState.Indeterminate:
                state = !enabled ? CheckBoxState.MixedDisabled
                    : _mouseState == MouseState.Down
                    ? CheckBoxState.MixedPressed
                    : _mouseState == MouseState.Hot
                    ? CheckBoxState.MixedHot
                    : CheckBoxState.MixedNormal;
                break;
            case CheckState.Unchecked:
                state = !enabled ? CheckBoxState.UncheckedDisabled
                    : _mouseState == MouseState.Down
                    ? CheckBoxState.UncheckedPressed
                    : _mouseState == MouseState.Hot
                    ? CheckBoxState.UncheckedHot
                    : CheckBoxState.UncheckedNormal;
                break;
        }

        Size sz = CheckBoxRenderer.GetGlyphSize(graphics, state);
        Point pt = new Point(
                cellBounds.X + (cellBounds.Width - sz.Width) / 2,
                cellBounds.Y + (cellBounds.Height - sz.Height) / 2);
        _checkBoxRect = new Rectangle(pt, sz);

        CheckBoxRenderer.DrawCheckBox(graphics, pt, state);
    }
}

For the HeaderCell custom event, the following custom event arguments class is used to pass the new state.

public class HeaderCellCheckedChangedEventArgs : EventArgs
{
    public HeaderCellCheckedChangedEventArgs(CheckState state)
    {
        CheckState = state;
    }

    public CheckState CheckState { get; }
}

For some thoughts, See:

Column & Cell

Derive a new class from DataGridViewCheckBoxCell. Unless you want to override the Paint method to draw the CheckBox yourself. You don't need to do anything here. It's mainly used to set the CellTemplate of the column class.

public class CheckAlCheckBoxlCell : DataGridViewCheckBoxCell
{
    public CheckAlCheckBoxlCell() { }
}

... and derive another from DataGridViewCheckBoxColumn.

public class CheckAllCheckBoxColumn : DataGridViewCheckBoxColumn
{
    private readonly CheckAllColumnHeaderCell _headerCell;

    public CheckAllCheckBoxColumn()
    {
        CellTemplate = new CheckAlCheckBoxlCell();
        _headerCell = new CheckAllColumnHeaderCell();
        HeaderCell = _headerCell;
    }

    public override DataGridViewCell CellTemplate
    {
        get => base.CellTemplate;
        set
        {
            if (value != null &&
                !value.GetType().IsAssignableFrom(typeof(CheckAlCheckBoxlCell)))
                throw new InvalidCastException("Must be a CheckAlCheckBoxlCell.");

            base.CellTemplate = value;
        }
    }

    [Browsable(false)]
    public CheckState CheckState
    {
        get => _headerCell.CheckState;
        set
        {
            switch (value)
            {
                case CheckState.Checked:
                case CheckState.Indeterminate:
                    _headerCell.CheckState = CheckState.Checked;
                    break;
                default:
                    _headerCell.CheckState = CheckState.Unchecked;
                    break;
            }
        }
    }
}

Implementation

Compile and add the custom column by the designer. You'll find in the grid's columns designer a new item is listed and named CheckAllCheckBoxColumn. You need to subscribe to the CheckedChanged event by code though. Assuming the column name is checkAllColumn:

// In your Form...
protected override void OnLoad(EventArgs e)
{
    base.OnLoad(e);
    (checkAllColumn.HeaderCell as CheckAllColumnHeaderCell)
        .CheckedChanged += OnColumnCheckAllChanged;
            
}

private void OnColumnCheckAllChanged(object sender, HeaderCellCheckedChangedEventArgs e)
{
    Console.WriteLine(e.CheckState);
}

Or, do it all by code:

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

    var checkAllColumn = new CheckAllCheckBoxColumn();
    dataGridView1.Columns.Add(checkAllColumn);
    (checkAllColumn.HeaderCell as CheckAllColumnHeaderCell)
        .CheckedChanged += OnColumnCheckAllChanged;
            
}

SO78242105