I have a simple UserControl for database paging, that uses a controller to perform the actual DAL calls. I use a BackgroundWorker
to perform the heavy lifting, and on the OnWorkCompleted
event I re-enable some buttons, change a TextBox.Text
property and raise an event for the parent form.
Form A holds my UserControl. When I click on some button that opens form B, even if I don't do anything "there" and just close it, and try to bring in the next page from my database, the OnWorkCompleted
gets called on the worker thread (and not my Main thread), and throws a cross-thread exception.
At the moment I added a check for InvokeRequired
at the handler there, but isn't the whole point of OnWorkCompleted
is to be called on the Main thread? Why wouldn't it work as expected?
EDIT:
I have managed to narrow down the problem to arcgis and BackgroundWorker
. I have the following solution wich adds a Command to arcmap, that opens a simple Form1
with two buttons.
The first button runs a BackgroundWorker
that sleeps for 500ms and updates a counter.
In the RunWorkerCompleted
method it checks for InvokeRequired
, and updates the title to show whethever the method was originaly running inside the main thread or the worker thread.
The second button just opens Form2
, which contains nothing.
At first, all the calls to RunWorkerCompletedare
are made inside the main thread (As expected - thats the whold point of the RunWorkerComplete method, At least by what I understand from the MSDN on BackgroundWorker
)
After opening and closing Form2
, the RunWorkerCompleted
is always being called on the worker thread. I want to add that I can just leave this solution to the problem as is (check for InvokeRequired
in the RunWorkerCompleted
method), but I want to understand why it is happening against my expectations. In my "real" code I'd like to always know that the RunWorkerCompleted
method is being called on the main thread.
I managed to pin point the problem at the form.Show();
command in my BackgroundTesterBtn
- if I use ShowDialog()
instead, I get no problem (RunWorkerCompleted
always runs on the main thread). I do need to use Show()
in my ArcMap project, so that the user will not be bound to the form.
I also tried to reproduce the bug on a normal WinForms project. I added a simple project that just opens the first form without ArcMap, but in that case I couldn't reproduce the bug - the RunWorkerCompleted
ran on the main thread, whether I used Show()
or ShowDialog()
, before and after opening Form2
. I tried adding a third form to act as a main form before my Form1
, but it didn't change the outcome.
Here is my simple sln (VS2005sp1) - it requires
ESRI.ArcGIS.ADF(9.2.4.1420)
ESRI.ArcGIS.ArcMapUI(9.2.3.1380)
ESRI.ArcGIS.SystemUI (9.2.3.1380)
It looks like a bug:
http://connect.microsoft.com/VisualStudio/feedback/ViewFeedback.aspx?FeedbackID=116930
http://thedatafarm.com/devlifeblog/archive/2005/12/21/39532.aspx
So I suggest using the bullet-proof (pseudocode):
if(control.InvokeRequired)
control.Invoke(Action);
else
Action()
Isn't the whole point of OnWorkCompleted
is to be called on the Main thread? Why wouldn't it work as expected?
No, it's not.
You can't just go running any old thing on any old thread. Threads are not polite objects that you can simply say "run this, please".
A better mental model of a thread is a freight train. Once it's going, it's off on it's own track. You can't change it's course or stop it. If you want to influence it, you either have to wait til it gets to the next train station (eg: have it manually check for some events), or derail it (Thread.Abort
and CrossThread exceptions have much the same consequences as derailing a train... beware!).
Winforms controls sort of support this behaviour (They have Control.BeginInvoke
which lets you run any function on the UI thread), but that only works because they have a special hook into the windows UI message pump and write some special handlers. To go with the above analogy, their train checks in at the station and looks for new directions periodically, and you can use that facility to post it your own directions.
The BackgroundWorker
is designed to be general purpose (it can't be tied to the windows GUI) so it can't use the windows Control.BeginInvoke
features. It has to assume that your main thread is an unstoppable 'train' doing it's own thing, so the completed event has to run in the worker thread or not at all.
However, as you're using winforms, in your OnWorkCompleted
handler, you can get the Window to execute another callback using the BeginInvoke
functionality I mentioned above. Like this:
// Assume we're running in a windows forms button click so we have access to the
// form object in the "this" variable.
void OnButton_Click(object sender, EventArgs e )
var b = new BackgroundWorker();
b.DoWork += ... blah blah
// attach an anonymous function to the completed event.
// when this function fires in the worker thread, it will ask the form (this)
// to execute the WorkCompleteCallback on the UI thread.
// when the form has some spare time, it will run your function, and
// you can do all the stuff that you want
b.RunWorkerCompleted += (s, e) { this.BeginInvoke(WorkCompleteCallback); }
b.RunWorkerAsync(); // GO!
}
void WorkCompleteCallback()
{
Button.Enabled = false;
//other stuff that only works in the UI thread
}
Also, don't forget this:
Your RunWorkerCompleted event handler should always check the Error and Cancelled properties before accessing the Result property. If an exception was raised or if the operation was canceled, accessing the Result property raises an exception.
The BackgroundWorker
checks whether the delegate instance, points to a class which supports the interface ISynchronizeInvoke
. Your DAL layer probably does not implement that interface. Normally, you would use the BackgroundWorker
on a Form
, which does support that interface.
In case you want to use the BackgroundWorker
from the DAL layer and want to update the UI from there, you have three options:
- you'd stay calling the
Invoke
method
- implement the interface
ISynchronizeInvoke
on the DAL class, and redirect the calls manually (it's only three methods and a property)
- before invoking the
BackgroundWorker
(so, on the UI thread), to call SynchronizationContext.Current
and to save the content instance in an instance variable. The SynchronizationContext
will then give you the Send
method, which will exactly do what Invoke
does.
The best approach to avoid issues with cross-threading in GUI is to use SynchronizationContext.