There are two threads involved: the UI-thread (the "main" thread), and your BackGroundWorker. Only the UI-thread is allowed to access any of the visual items. So only the UI-thread may fetch text from text boxes, only the UI-thread may show or hide labels, or redraw forms. Whenever any other thread tries to access a Control, you get the exception that you described.
So: how to make sure that the UI-thread will update the data that the BackgroundWorker has created?
Answer: send this data to the UI-thread using events ProgressChanged
and RunWorkerCompleted
. Both events allow data to be sent to the UI-thread.
You have to ask yourself: must the datagridview be updated while the BackgroundWorker is still busy, or is it good enough that the operator sees that the BackgroundWorker is progressing?
You chose a separate thread to do the lengthy calculations, in order to keep your UI responsive. So you want to allow the operator to do something with the form, while you are doing the calculations.
But what if the operator changes the DataGridView during these calculations? Or maybe not change, but only selects some cells to copy-paste to something else? Won't your constantly updating the DataGridView interfere with this?
You're right, I only want to show progress, and update when all data if calculated.
In that case event ProgressChanged will only show that some progress has been made. Event RunWorkerCompleted will contain all calculated data that must be updated in the DataGridView.
No! whenever the backgroundworker has calculated some data, I want to show it as soon as possible!
In that case: attach the new data to ProgressChanged event.
In your form:
void StartBackGroundCalculations(...)
{
// this method is run by the UI-thread!
if (this.backgroundWorker1.IsBusy)
{
// background calculations already busy. Decide what you want to do:
// show message to operator?
// restart the calculations?
// exception?
this.ProcessBusyBackgroundWorker();
return;
}
// if you want to show progress, for instance in a ProgressBar
// initiate it now:
this.IntiateProgressDisplay();
// start the background work
// if needed, transfer data from your form to the background worker,
// so the backgroundworker doesn't have to access your text boxes, datagridview etc
MyBackGroundWorkerData startData = this.CreateBackgroundStartData() // todo: implement
this.backgroundWorker1.RunWorkerAsync(startData);
}
Event DoWork: run by the backgroundWorker, so no access to any UI element.
Three versions:
- don't show any progress, only show the end result
- show Progress in a Progress bar and the end result
- show the actual progress data as soon as it is calculated
(1) No progress at all:
void OnBackgroundWorkerDoWork(object sender, DoWorkEventArgs e)
{
// run by BackgroundWorker, no UI-access allowed
MyBackGroundWorkerData startData = (MyBackGroundWorkerData)e.Argument;
MyEndResult endResult = this.CalculateTotalData(startData);
// report end result to UI-thread
e.Result = endResult;
}
(2) Show simple Progress
void OnBackgroundWorkerDoWork(object sender, DoWorkEventArgs e)
{
BackgroundWorker backgroundWorker = (BackgroundWorker)sender;
// run by BackgroundWorker, no UI-access allowed
MyBackGroundWorkerData startData = (MyBackGroundWorkerData)e.Argument;
List<MyData> endResult = new List<MyData>()
int progressPercentage = 0;
MyData intermediateResult = this.CalculateIntermediateResult();
// method returns null if there is nothing to calculate anymore
// expect no more than 50 calculations
while (intermediateResult != null)
{
endResult.Add(intermediateResult);
progressPercentage += 2;
// report progress:
backgroundWorker.ReportProgress(progressPercentage, null);
// null: no extra data in the progress report
}
// if here: all data calculated; report end result to UI-thread
backgroundWorker.ReportProgress(100, null); // report progress: 100%
e.Result = endResult;
}
(3) Show progress data
void OnBackgroundWorkerDoWork(object sender, DoWorkEventArgs e)
{
BackgroundWorker backgroundWorker = (BackgroundWorker)sender;
// run by BackgroundWorker, no UI-access allowed
MyBackGroundWorkerData startData = (MyBackGroundWorkerData)e.Argument;
List<MyData> endResult = new List<MyData>()
int progressPercentage = 0;
MyData intermediateResult = this.CalculateIntermediateResult();
// method returns null if there is nothing to calculate anymore
// expect no more than 50 calculations
while (intermediateResult != null)
{
endResult.Add(intermediateResult);
progressPercentage += 2;
// report progress, include intermediateResult
backgroundWorker.ReportProgress(progressPercentage, intermediateResult);
}
// if here: all data calculated; report end result to UI-thread
backgroundWorker.ReportProgress(100, null); // report progress: 100%
e.Result = endResult;
}
Sometimes it is very difficult to give a percentage as progress. In that case, don't use this field, try to invent a different method to tell the operator that the background work is really progressing.
The event progress changed is handled by the UI-thread: this thread is allowed to access all UI elements, including progress bar and datagridview
void backgroundWorker1_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
// if you agreed that the percentage is filled, update your progress display
this.UpdateProgressDisplay(e.ProgressPercentage);
// if you agreed to communicate actual progress data
MyData intermediateResult = (IntermediateResult)e.UserState;
this.ProcessProgress(intermediateResult);
// this will do what you want to do with the intermediate data,
// for instance update the DataGridView
}
And the result if the Background worker reports complete. This is run by the UI-thread, so you can access the DataGridView and the progress bar, or whatever method you use to show the progress
private void backgroundWorker1_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
// check if Error / Cancelled
if (e.Error) ... // TODO: decide what to do if error
if (e.Cancelled) ... // TODO: decide what to do if cancelled
// depending on what you agreed as the end result data:
List<MyData> endResult = (List<MyData>)e.Result;
this.UpdateEndResult(endResult); // updates the datagridview
// if you had some Progress display, like a progress bar, hid it now
this.HideProgressDisplay();
}
Conclusion
The event handler of event DoWork is run by the BackgroundWorker: it may not touch any user interface elements. If it needs data, use the eventargs to send it data
Use e.Result to fill in the end result
The event handers of events ProgressChanged and RunWorkerCompleted are run by the UI. These procedures can access the UI elements