Progress bar for DataGridView that only accounts for one column

Viewed 306

I am having trouble making code for my progress bar, basically I have a datagridview that has to be filled by the user and I would like for my progressbar to increase only when the checkboxes from column 2 are checked (or to decrease when they are unchecked), I've tried the following code so far in the void norms_TableDataGridView_CurrentCellDirtyStateChanged

        int cnt = this.norms_TableDataGridView.Rows.Count;
        int i = 0;
        string A = this.norms_TableDataGridView.Rows[i].Cells[2].Value.ToString(); //"Approved" Cells 

        for (i = 0; i < cnt; i++)
        {
            if (A == "True")
            {
                s_Checks++;
            }
       }
       progressBar.Value = s_Checks * (progressBar.Maximum / TOTAL_CHECKBOXES);
       

This is making the progress bar increase at a higher rate than it should because, do to the for cycl,e it is counting more than one time the same checked checkbox. Any suggestion would be appreciated since my newbie idea is clearly failing

3 Answers

Filipe,

I'm a little uncertain about your code, but normally I would handle your situation by doing the following:

using System.Linq;

var checkedRows = norms_TableDataGridView.Rows.Cast<DataGridViewRow>()
                  .Count(r => Convert.ToBoolean(r.Cells[2].Value));
double allRows = norms_TableDataGridView.Rows.Count;
progressBar.Value = Math.Round((checkedRows / allRows) * 100,0); 

I am confident there are numerous ways to do this. Below is one possible way to manage the progress bar such that the “checked” check boxes in a grid’s column dictate the progress bars current value.

If no check boxes are checked, then the progress bar would be at zero. If all the check boxes are checked, then the progress bar would be full. If half the check boxes are checked, then the progress bar would show half filled.

It is unclear if the user is allowed to “add” rows. I will assume that the rows are fixed when the grid is loaded and the user is not able to “add” rows to the grid. Otherwise, the code would have to “update" the maximum value for the progress bar when the row is added.

A small trace of the current code...

The current posted code is a little odd and is doing a lot of unnecessary work. For starters, the for loop looks odd in a sense that this code is executed in the grids CurrentCellDirtyStateChanged event… So, this event fires every time a single cells value becomes dirty or is changed. So technically, only ONE (1) cell has changed. It seems odd to loop through all the check box values when only a single check box value may have changed.

I assume this is because the code is not keeping track of the progress bars current value. So, this value needs to be computed every time a single check box value changes. I am confident, there is an easier approach to change the progress bars value even without keeping track of its current value. I am thinking to add 1 to the progress bar when a check box is checked and subtract 1 when it is unchecked.

In addition, it is odd the way the current code is “checking” the value. There is a line before the for loop…

string A = this.norms_TableDataGridView.Rows[i].Cells[2].Value.ToString();

Then, in the for loop there is a “check” to see if A is “True”?

if (A == "True")

This is odd because A never changes in the loop. If it is true, then it will ALWAYS be true in the loop. Same for false. Therefore, s_Checks++ will always either be zero or equal to cnt when the for loop exits.

To fix this, you would need to “change” A to the next check box value with each iteration of the loop. However, even with this fix, the code is still checking values that have not changed since it was last checked. Again, there may be an easier approach.

A different approach....

I am assuming that when the grid is loaded, that all the check boxes are NOT checked and the progress bar would start at zero. In addition, when the grid is loaded with data, the progress bars Maximum value will be set to the number of (fixed) rows in the grid.

With this approach, the code will “add” 1 to the progress bars value when a check box is changed from “unchecked” to “checked.” And, the code will “subtract” 1 from the progress bars value if a “checked” box is “unchecked.”

To avoid errors, and to keep it simple, the code simply checks to make sure that adding 1 to the progress bars value does not go over its Maximum value. In addition, a check to make sure that subtracting 1 does not go below the Minimum value.

So, this approach would go something like below. As soon as the grids data is loaded where all the check boxes are NOT checked, we would set the progress bars, min and max values. Something like…

private void Form2_Load(object sender, EventArgs e) {
  dataGridView1.DataSource = YourDataSource;
  progressBar1.Maximum = dataGridView1.Rows.Count;
  progressBar1.Minimum = 0;
}

Instead of using the grids CurrentCellDirtyStateChanged event, I used the grids CellContentClick event.

One issue with using the CellContentClick event is that when it fires, the cells value has not changed yet. We could easily reverse the checking logic, however, to keep the code in a readable state, I will call the grids CommitEdit to update the changes in the grid.

In the event, the code checks if the check box is checked… in this example, the check box is in column 1.

(bool)dataGridView1.Rows[e.RowIndex].Cells[1].Value == true

If the check box went from “unchecked” to “checked,” then the code will add 1 to the progress bars value and subtract 1 from its value if the check box went from “checked” to “unchecked.”

private void dataGridView1_CellContentClick(object sender, DataGridViewCellEventArgs e) {
  if (e.ColumnIndex == 1) {
    dataGridView1.CommitEdit(DataGridViewDataErrorContexts.Commit);
    if ((bool)dataGridView1.Rows[e.RowIndex].Cells[1].Value == true) {
      if (progressBar1.Value < progressBar1.Maximum) {
        progressBar1.Value++;
      }
    }
    else {
      if (progressBar1.Value > 0) {
        progressBar1.Value--;
      }
    }
  }
}

I hope this makes sense.

Whenever you are using windows forms, and you decide that you need to read or write directly to the cells of a DataGridView, you should sit down, take deep breaths, and think again if it is wise to directly access the cells instead of using databinding.

People hesitate to use DataBinding, because they think it is difficult, or they don't really know how to use it.

However if you use Databinding to display your data, you separate the data from how it is displayed. This enables you to change your display, without changing your data. You can reuse your data for other purposes, and You can unit test your data without the need of a DataGridView.

Apparently you have a DataGridView that shows rows of similar items. Let's say Reports. You'll need a class Report. I don't know much about the report, only that apparently it has something that can be displayed as checked/not checked: a Boolean property.

The other properties I just invented for the example.

public class Report
{
    public int Id {get; set;}
    public string Title {get; set;}
    public string Author {get; set;}
    public DateTime PublicationDate {get; set;}

    public bool IsRead {get; set;}   // will be a checkbox in the datagridview
}

Using visual studio designer you have added a DataGridView, and some columns. Maybe all columns, or maybe you are not interested in the PublicationDate, or the Author.

DataGridView reportView =  new DataGridView();
DataGridViewColumn columnId = new DataGridViewColumn()
columnId.DataPropertyName = nameof(Report.Id);
DatagridViewColumn columnTitle = new DataGridViewColumn();
columnTitle.DataPropertyName = nameof(Report.Title);
DataGridViewCheckBoxColumn columnIsRead = new DataGridViewCheckBoxColumn();
columnIsRead.DataPropertyName = nameof(Report.IsRead);
// TODO: add the columns to reportView.Columns.

Now suppose you have a method to fetch the reports:

private IEnumerable<report> FetchReports()
{
    ...
}

Now all you have to do is assign the fetched reports to the DataSource:

private void DisplayReports()
{
    this.reportView.DataSource = this.FetchReports();
}

And presto! All reports are in the DataGridView.

However, this is one direction. If the operator changes anything, then they are not reflected in the original data. If you want two way changes, you need to use an object that implement IBindingList, like BindingList.

Good practice is to add the following lines to your Form:

public BindingList<Report> DisplayedReports
{
    get => (BindingList<Report>)this.reportView.DataSource;
    set => this.reportView.DataSource = value;
}

public Report CurrentReport => (Report)this.reportView.CurrentRow?.DataBoundItem;

public IEnumerable<Report> SelectedReports => this.reportView.SelectedCells
    .Cast<DataGridViewCell>()
    .Select(cell => (Report)cell.OwningRow.DataBoundItem)
    .Distinct();

The Distinct in the end is in case you have selected two cells in one row.

Usage:

private void Form_Load(object sender, ...)
{
    // initialize the datagridview
    this.DisplayedReports = new BindingList<Report>(this.FetchReports().ToList());
}

While the operator add / remove / changes displayed Reports, the BindingList is automatically update.

After a while the operator indicates that he finished editing:

private void ButtonOk_Clicked(object sender, ...)
{
    ICollection<Report> reports = this.DisplayedReports;
    // Todo: if desired: find out which reports are added / removed / changed
    this.ProcessEditedReports(reports);
}

Note: because you separated the view from your data, the operator can rearrange columns, sort the rows etc, without you having to care about on what row the Report with Id 25 is shown.

Related