Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Custom DataGridViewCell not triggering DataSource change events

I'm having a struggle with the WinForms DataGridView in conjunction with data binding. I assign a DataView wrapping a DataSet to the DataGridView.DataSource, which works perfectly well so far. The problem starts when implementing a custom DataGridViewCell. My goal is to provide a ComboBoxCell for selecting Enum values that is always fully interactive and doesn't require the user to enter edit mode explicitly.

How it looks like in action

here's the binding setup:

  • DataSet S contains exactly one DataTable, T
  • DataView V wraps said table
  • DataGridView.DataSource is set to V
  • Some parts of the application subscribe to T.RowChanged event. This is the crucial part.

As far as functionality goes, my custom cell behaves exactly as intended. However, it doesn't cause firing the DataTable.RowChanged event unless the whole DataGridView loses focus... but all other non-custom cells do. I still get a CellValueChanged event, and the DataSet has the new value.. but there is neither DataTable.RowChanged nor DataGridView.DataBindingComplete, and the row doesn't get invalidated automatically like it usually does.

I'm obviously doing something wrong. I am probably missing a notifier event or implemented something wrong, but after two days of searching, stepping and disassembling .Net code I'm still completely stuck.

Here are the most important sections (not the full source code) of the class definition:

public class DataGridViewEnumCell : DataGridViewCell, IDataGridViewEditingCell
{
    private Type    enumType            = null;
    private Enum    enumValue           = default(Enum);
    private bool    enumValueChanged    = false;


    public virtual object EditingCellFormattedValue
    {
        get { return this.GetEditingCellFormattedValue(DataGridViewDataErrorContexts.Formatting); }
        set { this.enumValue = (Enum)Utility.SafeCast(value, this.enumType); }
    }
    public virtual bool EditingCellValueChanged
    {
        get { return this.enumValueChanged; }
        set { this.enumValueChanged = value; }
    }
    public override Type EditType
    {
        get { return null; }
    }
    public override Type FormattedValueType
    {
        get { return this.enumType; }
    }
    public override Type ValueType
    {
        get
        {
            if (this.OwningColumn != null && this.OwningColumn.ValueType != null)
            {
                return this.OwningColumn.ValueType;
            }
            else
            {
                return this.enumType;
            }
        }
        set
        {
            base.ValueType = value;
        }
    }
    // The kind of Enum that is edited in this cell.
    public Type EnumValueType
    {
        get { return this.enumType; }
        set { this.enumType = value; }
    }


    public virtual object GetEditingCellFormattedValue(DataGridViewDataErrorContexts context)
    {
        if (context.HasFlag(DataGridViewDataErrorContexts.ClipboardContent))
        {
            return Convert.ToString(this.enumValue);
        }
        else
        {
            return this.enumValue ?? this.enumType.GetDefaultValue();
        }
    }
    public override object ParseFormattedValue(object formattedValue, DataGridViewCellStyle cellStyle, TypeConverter formattedValueTypeConverter, TypeConverter valueTypeConverter)
    {
        // Cast the Enum value to the original cell value type
        object cellVal;
        Utility.SafeCast(formattedValue, this.ValueType, out cellVal);
        return cellVal;
    }
    protected override object GetFormattedValue(object value, int rowIndex, ref DataGridViewCellStyle cellStyle, TypeConverter valueTypeConverter, TypeConverter formattedValueTypeConverter, DataGridViewDataErrorContexts context)
    {
        if (this.DataGridView == null || value == null)
        {
            return this.enumType.GetDefaultValue();
        }

        // Cast the cell value to the appropriate Enum value type
        object enumVal;
        Utility.SafeCast(value, this.enumType, out enumVal);

        // Let the base implementation apply additional formatting
        return base.GetFormattedValue(enumVal, rowIndex, ref cellStyle, valueTypeConverter, formattedValueTypeConverter, context);
    }
    private Enum GetCurrentValue()
    {
        object unknownVal = (this.enumValueChanged ? this.enumValue : this.Value);
        object enumVal;
        Utility.SafeCast(unknownVal, this.enumType, out enumVal);

        return (Enum)enumVal;
    }

    public virtual void PrepareEditingCellForEdit(bool selectAll)
    {
        this.enumValue = this.GetCurrentValue();
    }

    protected override void OnClick(DataGridViewCellEventArgs e)
    {
        base.OnClick(e);
        if (this.DataGridView.CurrentCell == this && (DateTime.Now - this.mouseClosed).TotalMilliseconds > 200)
        {
            // Due to some reason I don't understand sometimes EditMode is already active.
            // Don't do it twice in these cases.
            if (!this.IsInEditMode)
            {
                // Begin editing
                this.DataGridView.BeginEdit(true);
            }
            this.ShowDropDown();
        }
    }

    public void HideDropDown()
    {
        // ... snip ...

        // Revert value to original state, if not accepted explicitly
        // It will also run into this code after the new selection 
        // has been accepted (see below)
        if (this.DataGridView != null)
        {
            this.enumValue = this.GetCurrentValue();
            this.enumValueChanged = false;
            this.DataGridView.EndEdit();
        }
    }

    // Called when a value has been selected. All calue changes run through this method!
    private void dropdown_AcceptSelection(object sender, EventArgs e)
    {
        Enum selectedEnum = (Enum)this.dropdown.SelectedItem;
        if (!this.enumValue.Equals(selectedEnum))
        {
            this.enumValue = selectedEnum;
            this.enumValueChanged = true;
            this.DataGridView.NotifyCurrentCellDirty(true);
            this.DataGridView.CommitEdit(DataGridViewDataErrorContexts.Commit);
        }
    }
}

Again, the DataSource is properly firing events when the DataGridView loses focus, or when editing any other cell in the DataGridView, but I somehow can't get it to update it after editing my custom cell.

How can I achieve this?

like image 561
Adam Avatar asked Nov 10 '22 12:11

Adam


1 Answers

I finally was able to solve this.

As it turns out, the overall issue didn't have anything to do with my custom IDataGridViewEditingCell. Not receiving a RowChanged event was simply because the DataGridView row wasn't validated until leaving the currently selected row - which I didn't notice, because there was only one row in my tests, so I had to focus a different Control to achieve this.

Not validating the current row until unselecting / defocusing it seems to be expected and normal behavior in the DataGridView. It wasn't what I wanted though, so I derived my own DataGridView, and did the following:

protected override void OnCellEndEdit(DataGridViewCellEventArgs e)
{
    base.OnCellEndEdit(e);
    // Force validation after each cell edit, making sure that 
    // all row changes are validated in the DataSource immediately.
    this.OnValidating(new System.ComponentModel.CancelEventArgs());
}

So far, it seems to work flawlessly, but I might just be lucky. Any approval from a more experienced DataGridView developer would be highly appreciated, so.. feel free to comment!

like image 51
Adam Avatar answered Nov 15 '22 07:11

Adam