3

I am a newbie doing my first C# project (with minimal experience in Haskell and C as well) looking for guidance on how to implement a small feature on my program.

I have a DataGridView table (with 3 columns of checkboxes among other stuff) for the user to fill. When in a row, there gets a second checkbox checked, the first one that got checked must be unchecked. I can do this already but the problem is, the first checked one only gets unchecked after I select something else in my table.

Here is the code pertaining to the event of CellValueChanged (What is in the comments is what I've tried to help me)

if (e.ColumnIndex == 0 || e.ColumnIndex == 1 || tabela_NormasDataGridView.Rows.Count == 0)
{
    return;
}

var isChecked = (bool)tabela_NormasDataGridView.Rows[e.RowIndex].Cells[e.ColumnIndex].Value;

if (isChecked)
{
    for (int i = 2; i < 5; i++)
    {
        //Console.WriteLine("og " + e.ColumnIndex);
        DataGridViewCell cell = tabela_NormasDataGridView.Rows[e.RowIndex].Cells[i];
        //Console.WriteLine("segunda " + cell.ColumnIndex);
        if (cell.ColumnIndex != e.ColumnIndex)
        {
            cell.Value = false;
            //this.Refresh();
        }
    }
}
Uwe Keim
  • 39,551
  • 56
  • 175
  • 291

2 Answers2

5

Try committing the change to force the refresh:

void tabela_NormasDataGridView_CurrentCellDirtyStateChanged(object sender, EventArgs e) {
    tabela_NormasDataGridView.CommitEdit(DataGridViewDataErrorContexts.Commit);
}

Make sure you wire the event up.

LarsTech
  • 80,625
  • 14
  • 153
  • 225
  • I am sorry, I don't really know how to create an event handle for that one – Filipe Almeida Dec 21 '20 at 16:35
  • @FilipeAlmeida How did you create it for CellValueChanged? – LarsTech Dec 21 '20 at 16:46
  • copying from the CellContentClick, just changing the names this.tabela_NormasDataGridView.CellValueChanged += new System.Windows.Forms.DataGridViewCellEventHandler(this.tabela_NormasDataGridView_CellValueChanged); – Filipe Almeida Dec 21 '20 at 16:56
  • 2
    @FilipeAlmeida In the form's constructor, add `tabela_NormasDataGridView.CurrentCellDirtyStateChanged += tabela_NormasDataGridView_CurrentCellDirtyStateChanged;` and then copy my code snippet above. – LarsTech Dec 21 '20 at 16:57
  • sorry to bother you after such a long time, but I have a question: any idea why the code you gave me keeps disappearing? If so, is there any way I can prevent this from happening? (not sure but I think it happens every time I close and open VB) Thanks! – Filipe Almeida Feb 23 '21 at 15:08
  • @FilipeAlmeida Code just doesn't "disappear". Will have to have more information to help you. – LarsTech Feb 23 '21 at 15:11
  • Sorry, I explained it really poorly, what disappears is the code in the Designer.cs, the reference itself, the one you shared with me in these comments. The function stays there, it just loses its reference – Filipe Almeida Feb 23 '21 at 15:15
  • @FilipeAlmeida Does your form's constructor not have the `tabela_NormasDataGridView.CurrentCellDirtyStateChanged += tabela_NormasDataGridView_CurrentCellDirtyStateChanged;` line? – LarsTech Feb 23 '21 at 15:28
  • Exactly, that's the one that disappears. I know it sounds really stupid, but I don't know what I am doing for it to happen – Filipe Almeida Feb 23 '21 at 16:14
  • @FilipeAlmeida Are you writing in the designer file? If so, don't do that. – LarsTech Feb 23 '21 at 16:16
  • Yes indeed I was, I assumed the constructor you mentioned was that – Filipe Almeida Feb 23 '21 at 16:47
2

It is wise to use a little caution when wiring up certain events to do certain things. It may not be immediately obvious why a particular event is not necessarily a good "fit" for what you want the code to do IN that event.

In other words, the code works, and does not crash, however looking under the hood may reveal that the code is executing steps you may not have intended. Often this may be harmless and just waste CPU cycles. And it can also lead to a code crash.

A more subtle problem could arise where one of the grid's events waste a large amount of CPU cycles such that application using the grid becomes sluggish. Example; if the grid is anchored into a resizable control and the user resizes the control. The user may see a stutter in the re-drawing of the grid as it is resized.

In your current code, when the line below is executed...

cell.Value = false;

the cell's value will be changed. This appears harmless, however this code is IN the grid's CellValueChanged event... and the code is "changing a cells value." This will cause the event to fire again. And in this case we really do not want this.

LarsTech's solution will work and we will use it in the example below, however, it should be noted, that this solution works with the caveat that it creates extra unnecessary steps during execution.

On possible easy solution…

One possible solution to avoid this re-firing is to “turn off” the grid's CellValueChanged event just before the code sets the cells value to false. Then turn it back "on" after the code changes the value.

Another option using a different grid event…

Another possible (better) solution is to change the event the code is in. This can be done if you wire up the grid's CellContentClick event.

The key advantage to using the CellContnetClick event is that it will fired BEFORE the grid's CellValueChanged event gets fired. This will allow the code to "change" the cell values and they will be immediately updated since the grid's CellValueChanged event will get fired "automatically" when the code changes the value.

A full example...

To help visualize this, an example is below. Drop two DataGridViews onto a Form. The only change that needs to be done is that you will need to change the top grids name to tabela_NormasDataGridView. The columns are added in the code.

The two grids can be seen on the left in the picture below.
In this example, both grids wire up the same events. The wired up events are the CellValueChanged, CurrentCellDirtyStateChanged and the CellContentClick events.

In each event Debug statements are added to output "when" the event is "entered" and "when" the event "exits" (leaves.)

What code is in the Top grid events

  • CellValueChanged - this event contains our code.
  • CurrentCellDirtyStateChanged - this event contains code to "commit" the edits.
  • CellContentClick - this event contains NO code.

What code is in the Bottom grid events

  • CellValueChanged - this event contains NO code.
  • CurrentCellDirtyStateChanged - this event contains NO code.
  • CellContentClick - this event contains our code.

enter image description here

Te replicate, run the code below, click on the top grids check box cell in the "Check2" column. Then do the same in the bottom grid. This should produce the picture above.

Tracing the code in the top grid (Yellow)...

  • We can see the grids CurrentCellDirtyStateChanged event is fired first and the code in that event is committing the edits to the grid. It is entered… however…

    ... We can see that it does NOT immediately "leave" the event. At least not yet. In fact, the event gets re-entered well down the list of debug statements. We can also see this event is fired TWICE.

  • Next, we have SIX (6) calls to the grids CellValueChanged event. Initally the event is re-entered BEFORE it is finished (enter-enter).

    Calling the grids CellValueChanged event SIX (6) times when we only want to change TWO cells, is "creating" unnecessary work. In addition, this opens the code to infinite loop possibilities.

  • After the six calls to the event (where our code is), we can see that the CurrentCellDirtyStateChanged event is re-entered, then the code leaves the event twice.

  • Finally, the grids CellContentClick is fired and leaves immediately as there is no code in the event.

This code appears to “create” a lot of unnecessary calls to the CellValueChanged event... And this is where our code is... hmm.

Tracing the code in the bottom grid (Red)…

  • We can see that the grids CurrentCellDirtyStateChanged event is fired first and immediately exits as there is no code in that event.

  • The CellContentClick event is fired and entered. This is where our code is.

  • BEFORE we leave the CellContnetClick event, the grid's CellValueChanged event is fired (entered/leave) TWICE as there is no code there. The two calls to the grid's CellValueChanged event are coming from our code when the code sets the two cell values to false.

  • Finally, the code leaves the grids CellContentClick event.

This looks much cleaner and clearly is not creating extra steps.

Code that is used in the picture above…

First preliminaries to add the columns and wire up the events.

private void Form1_Load(object sender, EventArgs e) {
  AddCheckBoxColumnsToGrid(tabela_NormasDataGridView);
  tabela_NormasDataGridView.CellValueChanged += new DataGridViewCellEventHandler(tabela_NormasDataGridView_CellValueChanged);
  tabela_NormasDataGridView.CurrentCellDirtyStateChanged += new EventHandler(tabela_NormasDataGridView_CurrentCellDirtyStateChanged);
  tabela_NormasDataGridView.CellContentClick += new DataGridViewCellEventHandler(tabela_NormasDataGridView_CellContentClick);

  AddCheckBoxColumnsToGrid(dataGridView2);
  dataGridView2.CellValueChanged += new DataGridViewCellEventHandler(dataGridView2_CellValueChanged);
  dataGridView2.CurrentCellDirtyStateChanged += new EventHandler(dataGridView2_CurrentCellDirtyStateChanged);
  dataGridView2.CellContentClick += new DataGridViewCellEventHandler(dataGridView2_CellContentClick);
}

private void AddCheckBoxColumnsToGrid(DataGridView dgv) {
  DataGridViewTextBoxColumn txtCol = new DataGridViewTextBoxColumn();
  txtCol.Name = "col0";
  dgv.Columns.Add(txtCol);
  txtCol = new DataGridViewTextBoxColumn();
  txtCol.Name = "col0";
  dgv.Columns.Add(txtCol);
  DataGridViewCheckBoxColumn col = new DataGridViewCheckBoxColumn();
  col.Name = "Check1";
  dgv.Columns.Add(col);
  col = new DataGridViewCheckBoxColumn();
  col.Name = "Check2";
  dgv.Columns.Add(col);
  col = new DataGridViewCheckBoxColumn();
  col.Name = "Check3";
  dgv.Columns.Add(col);
  dgv.Rows.Add(4);
}

Events for the top grid…

private void tabela_NormasDataGridView_CellValueChanged(object sender, DataGridViewCellEventArgs e) {
  Debug.WriteLine("DGV1 - Cell Value Changed -> enter");
  if (e.ColumnIndex == 0 || e.ColumnIndex == 1 || tabela_NormasDataGridView.Rows.Count == 0) {
    return;
  }
  var isChecked = (bool)tabela_NormasDataGridView.Rows[e.RowIndex].Cells[e.ColumnIndex].Value;
  if (isChecked) {
    for (int i = 2; i < 5; i++) {
      DataGridViewCell cell = tabela_NormasDataGridView.Rows[e.RowIndex].Cells[i];
      if (cell.ColumnIndex != e.ColumnIndex) {
        cell.Value = false;
      }
    }
  }
  Debug.WriteLine("DGV1 - Cell Value Changed -> leave");
}

private void tabela_NormasDataGridView_CurrentCellDirtyStateChanged(object sender, EventArgs e) {
  Debug.WriteLine("DGV1 - Current Cell Dirty Cell State Changed -> enter");
  tabela_NormasDataGridView.CommitEdit(DataGridViewDataErrorContexts.Commit);
  Debug.WriteLine("DGV1 - Current Cell Dirty Cell State Changed -> leave");
}

private void tabela_NormasDataGridView_CellContentClick(object sender, DataGridViewCellEventArgs e) {
  Debug.WriteLine("DGV1 - CellContentClick -> enter");
  // Do nothing
  Debug.WriteLine("DGV1 - CellContentClick -> leave");
}

Events for the bottom grid…

private void dataGridView2_CellValueChanged(object sender, DataGridViewCellEventArgs e) {
  Debug.WriteLine("DGV2 - Cell Value Changed -> enter");
  // do nothing
  Debug.WriteLine("DGV2 - Cell Value Changed -> leave");
}

private void dataGridView2_CurrentCellDirtyStateChanged(object sender, EventArgs e) {
  Debug.WriteLine("DGV2 - Current Cell Dirty Cell State Changed -> enter");
  // do nothing
  //dataGridView1.CommitEdit(DataGridViewDataErrorContexts.Commit);
  Debug.WriteLine("DGV2 - Current Cell Dirty Cell State Changed -> leave");
}

private void dataGridView2_CellContentClick(object sender, DataGridViewCellEventArgs e) {
  Debug.WriteLine("DGV2 - Cell Content Click -> enter");
  string colName = dataGridView2.Columns[e.ColumnIndex].Name;
  if (colName == "Check1" || colName == "Check2" || colName == "Check3") {
    int rowIndex = e.RowIndex;
    switch (colName) {
      case "Check1":
        dataGridView2.Rows[rowIndex].Cells["Check2"].Value = false;
        dataGridView2.Rows[rowIndex].Cells["Check3"].Value = false;
        break;
      case "Check2":
        dataGridView2.Rows[rowIndex].Cells["Check1"].Value = false;
        dataGridView2.Rows[rowIndex].Cells["Check3"].Value = false;
        break;
      case "Check3":
        dataGridView2.Rows[rowIndex].Cells["Check1"].Value = false;
        dataGridView2.Rows[rowIndex].Cells["Check2"].Value = false;
        break;
    }
  }
  Debug.WriteLine("DGV2 - Cell Content Click -> leave");
}

I hope this makes sense.

JohnG
  • 9,259
  • 2
  • 20
  • 29
  • Only one checkbox is acceptable.Thank you for your input, I will try it out right now :) – Filipe Almeida Dec 21 '20 at 16:11
  • So I checked and the code works perfectly for the added columns! But how do I adapt for mine only? I just deleted everything related to your 3 columns in my Form Load but should i leave something there? – Filipe Almeida Dec 21 '20 at 16:36
  • Yes, the code in the form `Load` event is to complete the example and is unnecessary in your code. The code in the `CellContentClick` event checks to see if the clicked cell is one of the check box columns… Check1, Check2 or Check3. Here you would need to change the names in my code to the names of the check box column names in your code. Then if the cell clicked is one of those columns, then use the switch statement to identify “which” check box columns you want to set to false. – JohnG Dec 21 '20 at 16:45
  • So, in the switch/case code section, you will need to change the names of the `case` statements to match the names of YOUR check box column names. Basically, change the column names Check1, Check2 and Check3 in my code, to the names of the check box columns in your code. – JohnG Dec 21 '20 at 16:46
  • I was asking because i did exactly like you said (remove the code responsible for creating the Check1, Check2, Check3 collumns in Form Load and change every remaining name to my version) and it doesn't work I am supposed to delete the code i originally posted here correct? – Filipe Almeida Dec 21 '20 at 16:57
  • You can comment out the code in the `CellValueChanged` event that pertains to this “checking/unchecking” of the check box column cells as it is not needed. To do as I have described will only need the grids `CellContentClick` event. Nothing else is needed. – JohnG Dec 21 '20 at 17:00
  • I tried exactly as you instructed (I think) but for some reason it didn't work, thank you very much for your input and the time teaching me – Filipe Almeida Dec 21 '20 at 17:07
  • Thank you for your detailed input, I will carefully analyze it when I am done with this project, I am sure I can learn a great deal with it – Filipe Almeida Dec 22 '20 at 21:44