Search code examples
c#winformsdatagridview

Swap Image and ToolTip of DataGridViewImageColumn on DoubleClick


I am using a DataGridView to display locks (by user, assigned by the application) in the associated database.
The user is presented with a list of current locks, showing the relevant information, and the last visible column is a DataGridViewImageColumn, with the image fed from the application resources.

There is also a hidden DataGridViewCheckBoxColumn at the end, which is checked when the user double-clicks the icon for a row, and unchecked if the user double-clicks the same icon for that row, i.e. resets their selection.

What I'm trying to do is, based on the value of the checkbox, swap the image and tooltip in the CellContentDoubleClick event, as below, but I'm not seeing the change to the icon or the tooltip.
'ProjectLocked' is the name of the DataGridViewImageColumn and 'DeleteLock' is the name of the DataGridViewCheckBoxColumn.

private void dgvProjectLocks_CellContentDoubleClick(object sender, DataGridViewCellEventArgs e)
{
    // do nothing for the header row
    if (e.RowIndex < 0) return;
    // caste the object to the correct type
    var dgv = (DataGridView)sender;
    // Do nothing if the clicked column is not the ProjectLocked column
    if (dgv.Columns[e.ColumnIndex].Name != "ProjectLocked") return;
    // Get the clicked row
    using (var currentRow = dgv.Rows[e.RowIndex])
    {
        using (var deleteCell = currentRow.Cells["DeleteLock"])
        {
            switch (Convert.ToBoolean(deleteCell.Value))
            {
                case true:
                    currentRow.DefaultCellStyle.BackColor = dgv.DefaultCellStyle.BackColor;
                    currentRow.DefaultCellStyle.ForeColor = dgv.DefaultCellStyle.ForeColor;
                    deleteCell.Value = false;
                    currentRow.Cells["ProjectLocked"].Value = Resources.locked;
                    deleteCell.ToolTipText = Resources.AddProjectLockDeleteToolTip;
                    break;
                case false:
                    currentRow.DefaultCellStyle.BackColor = Color.LightGray;
                    currentRow.DefaultCellStyle.ForeColor = Color.DarkGray;
                    deleteCell.Value = true;
                    currentRow.Cells["ProjectLocked"].Value = Resources.unlocked;
                    deleteCell.ToolTipText = Resources.RemoveProjectLockDeleteToolTip;
                    break;
            }
        }
    }
}

Solution

  • Note: Properties.Resources is a Factory, it returns a new object each time you ask for one. When you assign a new object from the Resource, the object it replaces is left to the GC. In case of Graphics objects, not a good thing. Better cache these objects and use the cached value.
    Here I'm using a Dictionary<bool, Image> as storage facility:

    private Dictionary<bool, Image> DataGridViewLockState = new Dictionary<bool, Image>() {
        [true]  = Resources.locked,
        [false] = Resources.unlocked
    };
    

    About the code presented here:

    • When you assign a reference to a local variable, don't declare it with a using statement. You need the referenced object alive, here, those are your Rows / Cells
      You're setting the ToolTip of a Cell that, as you have mentioned, belongs to a hidden CheckBox Column. I assume you want to set the ToolTip of the Image Column, so you can see it.

    Other:

    • Test both the e.RowIndex and e.ColumIndex for negative values, since both can be negative, at some point (you may have disabled Column sorting, doesn't matter, it's procedural)
    • Better also test whether the Value of a Cell is null or DbNull.Value. Never mind if the data doesn't come out of a database, better safe than sorry

    When you have changed the current state of the affected Cells, call the [DataGridView].EndEdit() method. This commits the changes immediately.
    It also raises the CellValueChanged event immediately, before the current event has completed. Keep this in mind if you have code in the CellValueChanged handler.
    Better call EndEdit() last, to avoid some form of re-entrancy


    private void dgvTest_CellContentDoubleClick(object sender, DataGridViewCellEventArgs e) {
        var dgv = sender as DataGridView;
        if (dgv is null || e.RowIndex < 0 || e.ColumnIndex < 0) return;
        if (dgv.Columns[e.ColumnIndex].Name != "ProjectLocked") return;
    
        var currentRow = dgv.Rows[e.RowIndex];
        var deleteCell = currentRow.Cells["DeleteLock"];
    
        if (deleteCell.Value is null || deleteCell.Value == DBNull.Value) {
            throw new InvalidOperationException("DeleteLock value cannot be null");
        }
    
        // Negate the current value, we want to change state
        bool value = !(bool)deleteCell.Value;
    
        currentRow.Cells["ProjectLocked"].Value = DataGridViewLockState[value];
        currentRow.Cells["ProjectLocked"].ToolTipText = value ? Resources.RowLocked : Resources.RowUnlocked;
        currentRow.DefaultCellStyle.BackColor = value ? dgv.DefaultCellStyle.BackColor : Color.LightGray;
        currentRow.DefaultCellStyle.ForeColor = value ? dgv.DefaultCellStyle.ForeColor : Color.DarkGray;
        deleteCell.Value = value;
        dgv.EndEdit();
    }