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.
我有一个简单的UserControl用于数据库分页,它使用控制器来执行实际的DAL调用。我使用BackgroundWorker来执行繁重的工作,并在OnWorkCompleted事件上重新启用一些按钮,更改TextBox.Text属性并为父窗体引发事件。
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.
表单A保存我的UserControl。当我点击打开表单B的某个按钮时,即使我没有做任何“那里”并且只是关闭它,并尝试从我的数据库引入下一页,OnWorkCompleted会在工作线程上调用(而不是我的主线程),并抛出一个跨线程异常。
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?
目前我在那里的处理程序中添加了对InvokeRequired的检查,但是不是要在主线程上调用OnWorkCompleted的全部内容吗?为什么不按预期工作?
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.
我设法将问题缩小到arcgis和BackgroundWorker。我有以下解决方案,它向arcmap添加一个命令,打开一个带有两个按钮的简单Form1。
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.
第一个按钮运行一个休眠500毫秒的BackgroundWorker并更新计数器。在RunWorkerCompleted方法中,它检查InvokeRequired,并更新标题以显示方法最初在主线程或工作线程内运行。第二个按钮只打开Form2,它什么都不包含。
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
)
首先,所有对RunWorkerCompletedare的调用都是在主线程内完成的(正如预期的那样 - 这就是RunWorkerComplete方法的最后一点,至少我从BackgroundWorker上的MSDN中了解到的)
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.
打开和关闭Form2后,始终在工作线程上调用RunWorkerCompleted。我想补充一点,我可以将此解决方案保留原样(在RunWorkerCompleted方法中检查InvokeRequired),但我想了解为什么它会违背我的期望。在我的“真实”代码中,我想知道在主线程上调用RunWorkerCompleted方法。
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.
我设法在form.Show();我的BackgroundTesterBtn中的命令 - 如果我使用ShowDialog(),我没有问题(RunWorkerCompleted总是在主线程上运行)。我需要在我的ArcMap项目中使用Show(),这样用户就不会绑定到表单了。
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.
我还尝试在正常的WinForms项目上重现该错误。我添加了一个简单的项目,只打开没有ArcMap的第一个表单,但在这种情况下我无法重现该错误 - RunWorkerCompleted在主线程上运行,无论我在打开Form2之前和之后使用Show()或ShowDialog() 。我尝试在Form1之前添加第三个表单作为主表单,但它没有改变结果。
Here is my simple sln (VS2005sp1) - it requires
这是我的简单sln(VS2005sp1) - 它需要
ESRI.ArcGIS.ADF(9.2.4.1420)
ESRI.ArcGIS.ArcMapUI(9.2.3.1380)
ESRI.ArcGIS.SystemUI (9.2.3.1380)
4 个解决方案
#1
It looks like a bug:
它看起来像一个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()
#2
Isn't the whole point of
OnWorkCompleted
is to be called on the Main thread? Why wouldn't it work as expected?是不是要在主线程上调用OnWorkCompleted的全部内容?为什么不按预期工作?
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!).
线程的更好的心理模型是货运列车。一旦它开始,它就在它自己的轨道上。你无法改变它的路线或阻止它。如果你想影响它,你要么必须等到它到达下一个火车站(例如:让它手动检查一些事件),或者使它脱轨(Thread.Abort和CrossThread异常与脱轨一样有很多相同的后果火车......小心!)。
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.
Winforms控件支持这种行为(它们具有Control.BeginInvoke,它允许你在UI线程上运行任何函数),但这只能起作用,因为它们有一个特殊的钩子进入Windows UI消息泵并编写一些特殊的处理程序。按照上述类比,他们的火车在车站登记并定期查找新方向,您可以使用该设施发布您自己的指示。
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.
BackgroundWorker旨在用于通用(它不能绑定到Windows GUI),因此它无法使用Windows Control.BeginInvoke功能。它必须假设你的主线程是一个不可阻挡的“火车”做它自己的事情,所以完成的事件必须在工作线程中运行或根本不运行。
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:
但是,当您使用winforms时,在OnWorkCompleted处理程序中,您可以使用上面提到的BeginInvoke功能让Window执行另一个回调。像这样:
// 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
}
另外,不要忘记这个:
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.
在访问Result属性之前,RunWorkerCompleted事件处理程序应始终检查Error和Canceled属性。如果引发了异常或操作已取消,则访问Result属性会引发异常。
#3
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.
BackgroundWorker检查委托实例是否指向支持ISynchronizeInvoke接口的类。您的DAL层可能没有实现该接口。通常,您可以在表单上使用BackgroundWorker,它支持该接口。
In case you want to use the BackgroundWorker
from the DAL layer and want to update the UI from there, you have three options:
如果您想使用DAL层中的BackgroundWorker并希望从那里更新UI,您有三个选择:
- 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 callSynchronizationContext.Current
and to save the content instance in an instance variable. TheSynchronizationContext
will then give you theSend
method, which will exactly do whatInvoke
does.
你会继续调用Invoke方法
在DAL类上实现接口ISynchronizeInvoke,并手动重定向调用(它只有三个方法和一个属性)
在调用BackgroundWorker之前(因此,在UI线程上),调用SynchronizationContext.Current并将内容实例保存在实例变量中。然后SynchronizationContext将为您提供Send方法,它将完全执行Invoke所做的操作。
#4
The best approach to avoid issues with cross-threading in GUI is to use SynchronizationContext.
避免GUI中的跨线程问题的最佳方法是使用SynchronizationContext。
#1
It looks like a bug:
它看起来像一个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()
#2
Isn't the whole point of
OnWorkCompleted
is to be called on the Main thread? Why wouldn't it work as expected?是不是要在主线程上调用OnWorkCompleted的全部内容?为什么不按预期工作?
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!).
线程的更好的心理模型是货运列车。一旦它开始,它就在它自己的轨道上。你无法改变它的路线或阻止它。如果你想影响它,你要么必须等到它到达下一个火车站(例如:让它手动检查一些事件),或者使它脱轨(Thread.Abort和CrossThread异常与脱轨一样有很多相同的后果火车......小心!)。
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.
Winforms控件支持这种行为(它们具有Control.BeginInvoke,它允许你在UI线程上运行任何函数),但这只能起作用,因为它们有一个特殊的钩子进入Windows UI消息泵并编写一些特殊的处理程序。按照上述类比,他们的火车在车站登记并定期查找新方向,您可以使用该设施发布您自己的指示。
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.
BackgroundWorker旨在用于通用(它不能绑定到Windows GUI),因此它无法使用Windows Control.BeginInvoke功能。它必须假设你的主线程是一个不可阻挡的“火车”做它自己的事情,所以完成的事件必须在工作线程中运行或根本不运行。
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:
但是,当您使用winforms时,在OnWorkCompleted处理程序中,您可以使用上面提到的BeginInvoke功能让Window执行另一个回调。像这样:
// 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
}
另外,不要忘记这个:
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.
在访问Result属性之前,RunWorkerCompleted事件处理程序应始终检查Error和Canceled属性。如果引发了异常或操作已取消,则访问Result属性会引发异常。
#3
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.
BackgroundWorker检查委托实例是否指向支持ISynchronizeInvoke接口的类。您的DAL层可能没有实现该接口。通常,您可以在表单上使用BackgroundWorker,它支持该接口。
In case you want to use the BackgroundWorker
from the DAL layer and want to update the UI from there, you have three options:
如果您想使用DAL层中的BackgroundWorker并希望从那里更新UI,您有三个选择:
- 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 callSynchronizationContext.Current
and to save the content instance in an instance variable. TheSynchronizationContext
will then give you theSend
method, which will exactly do whatInvoke
does.
你会继续调用Invoke方法
在DAL类上实现接口ISynchronizeInvoke,并手动重定向调用(它只有三个方法和一个属性)
在调用BackgroundWorker之前(因此,在UI线程上),调用SynchronizationContext.Current并将内容实例保存在实例变量中。然后SynchronizationContext将为您提供Send方法,它将完全执行Invoke所做的操作。
#4
The best approach to avoid issues with cross-threading in GUI is to use SynchronizationContext.
避免GUI中的跨线程问题的最佳方法是使用SynchronizationContext。