2
votes

I'm buidling an application like this:

enter image description here

Within each row in the table:

  • User can check or uncheck value in the Option 1 and Option 2 column.
    After that, the value of checkbox in All option column with be updated to:
     - "Checked": if both Option 1 and Option 2 is checked
     - "Unchecked": if both Option 1 and Option 2 is unchecked
     - "Indeterminate": other cases

  • User can check or uncheck value in the All option column.
    After that, both value of Option 1 and Option 2 will be updated based on current value of All option.
    In this case, user cannot change the value of All option column to Indeterminate (only toggle between Checked and Unchecked).

I'm implementing the above application using DataGridView in Winforms. Following is my code:

public partial class Form1 : Form
    {
        DataTable dataTable;

        public Form1()
        {
            InitializeComponent();

            dataTable = new DataTable();
            dataTable.Columns.Add("Item");
            dataTable.Columns.Add("All option", typeof(CheckState));
            dataTable.Columns.Add("Option 1", typeof(bool));
            dataTable.Columns.Add("Option 2", typeof(bool));
            dataTable.Rows.Add("Item 1", CheckState.Unchecked, false, false);
            dataTable.Rows.Add("Item 2", CheckState.Unchecked, false, false);
            dataGrid.DataSource = dataTable;

        }

        private void dataGrid_CurrentCellDirtyStateChanged(object sender, EventArgs e)
        {
            if (dataGrid.CurrentCell is DataGridViewCheckBoxCell)
            {
                dataGrid.CommitEdit(DataGridViewDataErrorContexts.Commit);
            }
        }

        bool isUnderUpdateAll = false;

        private void dataGrid_CellValueChanged(object sender, DataGridViewCellEventArgs e)
        {
            if ((dataGrid.CurrentCell is DataGridViewCheckBoxCell) == false) return;

            // This is column "All option"
            if (e.ColumnIndex == 1)
            {
                // Avoid stackoverflow exception
                if (isUnderUpdateAll) return;

                CheckState allOption = (CheckState)dataGrid["All option", e.RowIndex].Value;
                
                // Don't allow user select 'indeterminate' value
                if (allOption == CheckState.Indeterminate)
                {
                    allOption = CheckState.Unchecked;

                    // These code didn't work
                    dataTable.Rows[e.RowIndex]["All option"] = CheckState.Unchecked;
                    dataGrid["All option", e.RowIndex].Value = CheckState.Unchecked;
                    dataGrid.UpdateCellValue(e.ColumnIndex, e.RowIndex);
                    dataGrid.NotifyCurrentCellDirty(true);
                    dataGrid.Refresh();
                    dataGrid.Update();

                }

                dataGrid["Option 1", e.RowIndex].Value = allOption;
                dataGrid["Option 2", e.RowIndex].Value = allOption;
            }

            // This is column "Option 1" or "Option 2"
            else
            {
                bool option1 = (bool)dataGrid["Option 1", e.RowIndex].Value;
                bool option2 = (bool)dataGrid["Option 2", e.RowIndex].Value;

                isUnderUpdateAll = true;
                dataGrid["All option", e.RowIndex].Value = (option1 && option2) ? CheckState.Checked 
                    : (!option1 && !option2 ? CheckState.Unchecked : CheckState.Indeterminate );
                isUnderUpdateAll = false;
            }
        }
    }

The code seem works, but there is 1 incomplete point: user still able to switch to Indeterminate state in the All option column. In my code, I already add many thing like:

// Don't allow user select 'indeterminate' value
if (allOption == CheckState.Indeterminate)
{
    allOption = CheckState.Unchecked;

    // These code didn't work
    dataTable.Rows[e.RowIndex]["All option"] = CheckState.Unchecked;
    dataGrid["All option", e.RowIndex].Value = CheckState.Unchecked;
    dataGrid.UpdateCellValue(e.ColumnIndex, e.RowIndex);
    dataGrid.NotifyCurrentCellDirty(true);
    dataGrid.Refresh();
    dataGrid.Update();

}

But these code didn't work. When click on All option checkbox, the state still switch between Checked -> indeterminate -> Unchecked -> Checked -> ...
So, could someone help suggest how to remove the indeterminate state from above?
I want it to be: Checked -> Unchecked -> Checked -> Unchecked ...

2

2 Answers

2
votes

Try to set the CheckBox.ThreeState Property to false.

((DataGridViewCheckBoxCell)(dataGrid.CurrentCell)).ThreeState = false;
1
votes

I am not sure if agree with the @Kyle Wang solution. When you set the current cell’s ThreeState to false, then basically this will NOT allow an “Indeterminate” state. This is the behavior you want when the user is clicking into the “All option” check boxes, however this NOT the behavior you want when changing on the "Option 1” or `”Option 2” check boxes.

Let’s say the user clicks on the “All option” check box and changes it value to true. At that point that cells “ThreeState” is set to false so the Indeterminate state is not an option. Then later, the user changes the “Option 1” or “Option 2” check box such that one is true and the other false… then, the code will have to change the “All option” cell’s ThreeState property back to true in order to set it to an Indeterminate state as per your requirement. I can see a nightmare, keeping track of the state of each “All option” cell.

Also, you may want to be careful “which” events you choose to do certain things. Example, it appears the code is wiring up two grid events: CurrentCellDirtyStateChanged and the CellValueChanged. This is perfectly valid and will work, however it may help to see how often those events fire. I did not check this as I am confident, that if you display how many times the events fire, you will find they fire many more times than you are expecting. In my example below, when testing events, it helps to visually see how many times the events fire. I recommend you try this in your current code.

Given this, it would appear obvious that the “best” event for this is the grids CellValueChanged event. Unfortunately, this event does not fire until the user “leaves” the cell. With a check box, we want an event to fire immediately when the user clicks on the check box. One event that may help this is the grids CellContentClick event. This event will fire as soon as the user clicks into the “check box”. Bear in mind it will NOT fire if the user clicks on the check box “cell” but NOT the “check box” itself.

One of the issues with using this event is when it fires. It is fired BEFORE the check box “Value” is actually changed in the grid. Fortunately, since we know it is a check box cell, we can go ahead and “Commit” the grid changes. Then we WILL have the actual final value of the cell.

If you create a new “winform” project, drop a DataGridView and a multiline TextBox onto the form (like below), you should be able to test the code below.

enter image description here

I moved all the initialization code into the forms Load event, however the code is identical with the exception of adding a couple of different rows.

Walking through the grids CellContentClick event: The code adds text to the text box to show the event was fired (as described earlier). Then the code “Commits” the changes in the cell. Then an if statement to check if the changed cell is the “All option” column or one of the “Option”s columns. If the changed cell is in the “All option” column then a variable allOption is set to the CheckState value of the “All option” cell, Checked, Unchecked or Indeterminate.

Next a switch statement on the allOption variable for each of the states, checked, unchecked or Indeterminate. If the state is changed to true or false, then the code simply changes both “options” to the same state. Obviously, it is unnecessary to change the “All option” state.

If the current state of the “All option” cell is Indeterminate, then that means that one option is true and the other is false. This “Indeterminate” state was set by one of the other option check boxes. It is irrelevant which cell is true or false, the code simply changes all states to unchecked. Note; since we are changing the “All option” cell in code, AND, we know the cell is already committed to the Indeterminate state (which we do not want), we need to refresh the edit in the grid to change the cell to “Unchecked” and not leave it at “Indeterminate.”

Next in the else portion a check is made to see if the changed cell is one of the “Option” cells and if so, then the code simply sets the “All option” check box value based on the option values. Here the code can set the “All option” cell to an “Indeterminate” state.

private void Form1_Load(object sender, EventArgs e) {
  dataTable = new DataTable();
  dataTable.Columns.Add("Item");
  dataTable.Columns.Add("All option", typeof(CheckState));
  dataTable.Columns.Add("Option 1", typeof(bool));
  dataTable.Columns.Add("Option 2", typeof(bool));
  dataTable.Rows.Add("Item 1", CheckState.Unchecked, false, false);
  dataTable.Rows.Add("Item 2", CheckState.Checked, true, true);
  dataTable.Rows.Add("Item 3", CheckState.Indeterminate, false, true);
  dataTable.Rows.Add("Item 4", CheckState.Indeterminate, true, false);
  dataGrid.DataSource = dataTable;
}

private void dataGrid_CellContentClick(object sender, DataGridViewCellEventArgs e) {
  textBox1.Text += "CellContentClick" + Environment.NewLine;
  dataGrid.CommitEdit(DataGridViewDataErrorContexts.Commit);
  if (dataGrid.Columns[e.ColumnIndex].Name == "All option") {
    var allOption = (CheckState)dataGrid["All option", e.RowIndex].Value;
    switch (allOption) {
      case CheckState.Unchecked:
        dataGrid["Option 1", e.RowIndex].Value = CheckState.Unchecked;
        dataGrid["Option 2", e.RowIndex].Value = CheckState.Unchecked;
        break;
      case CheckState.Checked:
        dataGrid["Option 1", e.RowIndex].Value = CheckState.Checked;
        dataGrid["Option 2", e.RowIndex].Value = CheckState.Checked;
        break;
      case CheckState.Indeterminate:
        dataGrid["Option 1", e.RowIndex].Value = CheckState.Unchecked;
        dataGrid["Option 2", e.RowIndex].Value = CheckState.Unchecked;
        dataGrid["All option", e.RowIndex].Value = CheckState.Unchecked;
        // we changed the current All option cell so we need to refresh the edit
        dataGrid.RefreshEdit();
        break;
    }
  }
  else {
    if (dataGrid.Columns[e.ColumnIndex].Name == "Option 1" ||
        dataGrid.Columns[e.ColumnIndex].Name == "Option 2") {
      var op1Checked = (bool)dataGrid["Option 1", e.RowIndex].Value;
      var op2Checked = (bool)dataGrid["Option 2", e.RowIndex].Value;
      if (op1Checked && op2Checked) {
        dataGrid["All option", e.RowIndex].Value = CheckState.Checked;
      }
      else {
        if (!op1Checked && !op2Checked) {
          dataGrid["All option", e.RowIndex].Value = CheckState.Unchecked;
        }
        else {
          dataGrid["All option", e.RowIndex].Value = CheckState.Indeterminate;
        }
      }
    }
  }
}

I hope this makes sense.