BackgroundWorker OnWorkCompleted throws cross-thread exception

送分小仙女□ 提交于 2019-11-28 10:32:10

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.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!