I never noticed this before but the BackgroundWorker.ReportProgress
method returns before the control’s ProgressChanged
event has completed. It may return before the ProgressChanged
event has even started!
For those not familiar with the BackgroundWorker
control, this control simplifies creating a worker thread, especially for the purpose of keeping the user interface responsive while the worker thread performs a lengthy process.
One issue it simplifies relates to the fact that the worker thread cannot directly access the form or its controls because those objects were created by the UI thread. Instead, code running in the worker thread can call the control’s ReportProgress
method, which raises the control’s ProgressChanged
event. You can pass information to ReportProgress
that describes the current state of the lengthy process, and the handler for the ProgressChanged
can use that data to display it to the user in your form controls.
I had been using this approach for a lengthy operation that could run for days. A lot was going on so I was passing an instance of a custom class that contained various bits of progress information. But, at one point, I saw that the progress information being displayed was not correct. On further inspection, I could see that my worker thread was updating the progress information object before the ProgressChanged
event handler had a chance to display that information.
It is very easy to get caught up with multi-threading issues as some things are just not very intuitive. When I called the ReportProgress
method, I had just assumed that it would not return until the ProgressChanged
event had completed. But I was wrong.
Thinking about it, the way this control works makes sense. If, instead, the worker thread was blocked until the event had returned, some of the benefits of a worker thread would be lost as one thread would be shut down during that time. Also, note that the ProgressChanged
method is overloaded. One version simply takes an integer argument. Since integers are passed by value, there would be no reason to suspend the worker thread when using this version of the ProgressChanged
method.
The other version takes an object in addition to the integer argument. That’s the version I was using. Since class objects are passed by reference, changes to this data made in the worker thread would be reflected in the same object being used in the ProgressChanged
event.
At first thought, I wondered if maybe I should resolve this by blocking the worker thread somehow until the event had run to completion. But, as I’ve already pointed out, this eliminates some of the advantage of having a worker thread in the first place. A much simpler solution is to simply make a copy of my progress class object. This way, the worker thread can modify its copy as needed while the ProgressChanged
event is reading its copy, perhaps both at the same time.
Note that I only required a “shallow” copy. In the case of value members, a shallow copy will create a true copy of those members. In the case of reference members, the copy is actually a reference to the same object. The only reference members in my case were strings. Since string
s are immutable and cannot be changed, if my code updated one of these members, that would create a new string
and not affect the original one referenced in the object passed to ReportProgress
.
protected class ProgressInfo
{
public int current;
public int total;
public string message;
public object Clone()
{
return (ImportStatus)MemberwiseClone();
}
}
private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
{
ProgressInfo info = new progressInfo();
info.total = 1000;
for (info.current = 0; info.current < info.total; info.current++)
{
info.message = String.Format("Processing item {0}",
info.current + 1);
backgroundWorker1.ReportProgress(0, info.Clone());
}
}
Listing 1: Passing copy of object to BackgroundWorker.ReportProgress
Listing 1 shows some sample code. The ProgressInfo
class declares a Clone()
method, which calls MemberwiseClone()
. MemberwiseClone()
performs a shallow copy of the object. Note that this method is protected
and, therefore, can only be called from a method of the class (or a derived class). This is why it was necessary to create the additional, public
, “wrapper
” method in my class, which my worker thread can call.
Using this code, my ProgressChanged
event handler can take its time displaying the progress data and will not be affected by my background worker thread making changes to its copy of that data at the same time.