Home > Enterprise >  Change row color based on checkbox with limited selection
Change row color based on checkbox with limited selection

Time:09-10

The CurrentCellDirtyStateChanged event code will limit the number of checkboxes to (2). The rows with checked boxes contain editable data that is used to calculate values in the remaining unchecked readonly rows. I would like the datagrid to indicate editable rows by changing the color of the checked rows and have the remaining rows be readonly. The CurrentCellDirtyStateChanged event works for managing the checkboxes, but conflicts with the CellClick event for changing the row color. The boxes do not check and change color consistently. Any help would be greatly appreciated.

using System.Linq;
    private void dgvParameters_CurrentCellDirtyStateChanged(object sender, EventArgs e)
    {
        if (dgvParameters.CurrentCell is DataGridViewCheckBoxCell && dgvParameters.IsCurrentCellDirty && !(bool)dgvParameters.CurrentCell.FormattedValue)
        {
            var count = dgvParameters.Rows.Cast<DataGridViewRow>()
                .SelectMany(r => r.Cells.OfType<DataGridViewCheckBoxCell>()
                .Where(c => (bool)c.FormattedValue))
                .Count();

            if (count == 2) dgvParameters.CancelEdit(); 

        }
    }

    private void dgvParameters_CellClick(object sender, DataGridViewCellEventArgs e)
    {
        int index = dgvParameters.CurrentRow.Index;

        DataGridViewCellStyle CheckedStyle;
        DataGridViewCellStyle UnCheckedStyle;
        CheckedStyle = new DataGridViewCellStyle();
        UnCheckedStyle = new DataGridViewCellStyle();
        CheckedStyle.BackColor = Color.White;
        CheckedStyle.ForeColor = Color.Black;
        UnCheckedStyle.BackColor = Color.LightGray;
        UnCheckedStyle.ForeColor = Color.Black;
        bool isSelect = dgvParameters[5, index].Value as bool? ?? false;

        if (Convert.ToBoolean(row.Cells[5].Value))
        {
            dgvParameters.Rows[index].Cells[1].Style = CheckedStyle;
            dgvParameters.Rows[index].Cells[3].Style = CheckedStyle;
            dgvParameters.Rows[index].Cells[1].ReadOnly = false;
            dgvParameters.Rows[index].Cells[3].ReadOnly = false;

        }
        else
        {
            dgvParameters.Rows[index].Cells[1].Style = UnCheckedStyle;
            dgvParameters.Rows[index].Cells[3].Style = UnCheckedStyle;
            dgvParameters.Rows[index].Cells[1].ReadOnly = true;
            dgvParameters.Rows[index].Cells[3].ReadOnly = true;
        }
    }

CodePudding user response:

Your question says that you would like a row that is not checked should to be ReadOnly. One obvious conflict is that if a row is truly read-only in its entireity, then there would be no way to ever check it to make it editable. My first suggestion would be to make a refreshStyles method that takes into account which cells are DataGridViewCheckBoxCell so that these can always be toggled regardless of the state of the row.

private void refreshStyles()
{
    for (int i = 0; i < DataSource.Count; i  )
    {
        var item = DataSource[i];
        var row = dataGridView.Rows[i];
        // Use System.Linq to get all of the row cells that aren't checkboxes.
        var cells = 
            row.Cells.Cast<DataGridViewCell>()
            .Where(_ => !(_ is DataGridViewCheckBoxCell));
        if(item.IsChecked)
        {
            row.DefaultCellStyle.BackColor = Color.Azure;
            foreach (var cell in cells) cell.ReadOnly = false;
        }
        else
        {
            dataGridView.Rows[i].DefaultCellStyle.BackColor = 
                Color.FromArgb(0xf0, 0xf0, 0xf0);
            foreach (var cell in cells) cell.ReadOnly = true;
        }
    }
}

This is a step towards having the DGV present with a mix of read-only and editable rows.

screenshot


This is all based on having the DataSource property of the DGV set to a binding list of a class we'll call DgvItem that is minimally implemented as shown. The reason for making the IsChecked a binding property is in order to have the DataSource send a notification when its value changes (otherwise it only notifies when items are added or removed).

class DgvItem : INotifyPropertyChanged
{
    bool _IsChecked = false;
    public bool IsChecked
    {
        get => _IsChecked;
        set
        {
            if (!Equals(_IsChecked, value))
            {
                _IsChecked = value;
                OnPropertyChanged();
            }
        }
    }
    public string Description { get; set; }
    public event PropertyChangedEventHandler PropertyChanged;
    protected virtual void OnPropertyChanged(
        [CallerMemberName] string propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}

Initialize

The only thing left is to glue it all together in the override of the Load event in MainForm.

protected override void onl oad(EventArgs e)
{
    base.OnLoad(e);
    dataGridView.AllowUserToAddRows = false;

    // Set the DataSource of DGV to a binding list of 'DgvItem' instances.
    dataGridView.DataSource = DataSource;
    for (int i = 1; i <= 5; i  )
    {
        DataSource.Add(new DgvItem { Description = $"Item {i}" });
    }

    // Do a little column formatting
    dataGridView.Columns[nameof(DgvItem.IsChecked)].Width = 50;
    dataGridView.Columns[nameof(DgvItem.IsChecked)].HeaderText = string.Empty;
    dataGridView.Columns[nameof(DgvItem.Description)].AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill;

    // Ensure the row isn't stuck in edit mode when a checkbox changes
    dataGridView.CurrentCellDirtyStateChanged  = (sender, e) =>
    {
        if(dataGridView.CurrentCell is DataGridViewCheckBoxCell)
        {
            dataGridView.EndEdit();
        }
    };

    // Get notified when a checkbox changes
    DataSource.ListChanged  = (sender, e) =>
    {
        switch (e.ListChangedType)
        {
            case ListChangedType.ItemChanged:
                // Make changes to row styles when that happens.
                refreshStyles();
                break;
        }
    };

    // Init the styles
    refreshStyles();
}

I hope this gives you a few ideas towards achieving the outcome you want.

CodePudding user response:

You can continue with the CurrentCellDirtyStateChanged approach and handle the CellValueChanged event to set the properties of the same row's cells in question according to the new value of the DataGridViewCheckBoxCell.

First of all, make sure that you set the default values of the target cells either by the designer or code. I presume the initial value of the check box cells is false.

public YourForm()
{
    InitializeComponent();

    foreach (int i in new[] { 1, 3 })
    {
        dgvParameters.Columns[i].DefaultCellStyle.BackColor = Color.LightGray;
        dgvParameters.Columns[i].DefaultCellStyle.ForeColor = Color.Gray;
        dgvParameters.Columns[i].ReadOnly = true;
    }
}

Secondly, modify the CurrentCellDirtyStateChanged event as follows...

private void dgvParameters_CurrentCellDirtyStateChanged(object sender, EventArgs e)
{
    if (dgvParameters.CurrentCell is DataGridViewCheckBoxCell &&
        dgvParameters.IsCurrentCellDirty)
    {
        if (!(bool)dgvParameters.CurrentCell.FormattedValue)
        {
            var count = dgvParameters.Rows.Cast<DataGridViewRow>()
                .SelectMany(r => r.Cells.OfType<DataGridViewCheckBoxCell>()
                .Where(c => (bool)c.FormattedValue))
                .Count();

            if (count == 2) dgvParameters.CancelEdit();
        }

        dgvParameters.CommitEdit(DataGridViewDataErrorContexts.Commit);
    }
}

Note the dgvParameters.CommitEdit(DataGridViewDataErrorContexts.Commit); line is there to apply and see the result as you toggle the value. Comment it if you need to keep the dirty state of the current row and commit the change on cell leave.

Lastly, handle the CellValueChanged event to update the target cells if the e.ColumnIndex is the index of the check box cell...

private void dgvParameters_CellValueChanged(object sender, DataGridViewCellEventArgs e)
{
    if (e.RowIndex >= 0 && e.ColumnIndex == 5)
    {
        var cell5 = dgvParameters.Rows[e.RowIndex].Cells[e.ColumnIndex];
        bool isReadOnly = !(bool)cell5.FormattedValue;
        Color backColor, foreColor;

        if (isReadOnly)
        {
            backColor = Color.LightGray;
            foreColor = Color.Gray;
        }
        else
        {
            backColor = Color.White;
            foreColor = Color.Black;
        }

        foreach (int i in new[] { 1, 3 })
        {
            dgvParameters.Rows[e.RowIndex].Cells[i].Style.BackColor = backColor;
            dgvParameters.Rows[e.RowIndex].Cells[i].Style.ForeColor = foreColor;
            dgvParameters.Rows[e.RowIndex].Cells[i].ReadOnly = isReadOnly;
        }
    }
}

This way, you are just handling one row and you don't need to think about the rest.

  • Related