我有一个简单的UserControl用于数据库分页,它使用控制器来执行实际的DAL调用。我使用BackgroundWorker
执行繁重的工作,并在OnWorkCompleted
事件中重新启用某些按钮,更改TextBox.Text
属性并为父表单引发事件。
表单A保存我的UserControl。当我点击打开表单B的某个按钮时,即使我没有做任何“那里”并且只是关闭它,并尝试从我的数据库中引入下一页,OnWorkCompleted
会被调用线程(而不是我的主线程),并抛出一个跨线程异常。
目前我在那里的处理程序中添加了InvokeRequired
的检查,但是不是要在主线程上调用OnWorkCompleted
的全部内容吗?为什么不按预期工作?
编辑:
我设法将问题缩小到arcgis和BackgroundWorker
。我有以下解决方案,它向arcmap添加一个命令,打开一个带有两个按钮的简单Form1
。
第一个按钮运行BackgroundWorker
,睡眠时间为500毫秒并更新计数器。
在RunWorkerCompleted
方法中,它检查InvokeRequired
,并更新标题以显示方法最初在主线程或工作线程内运行。
第二个按钮只会打开Form2
,其中不包含任何内容。
首先,对RunWorkerCompletedare
的所有调用都是在主线程中进行的(正如预期的那样 - 这是RunWorkerComplete方法的最后一点,至少我从MSDN Here的理解来看。 1}})
打开和关闭BackgroundWorker
后,始终在工作线程上调用Form2
。我想补充一点,我可以按原样保留此问题的解决方案(在RunWorkerCompleted
方法中检查InvokeRequired
),但我想了解为什么它会违背我的期望。在我的“真实”代码中,我想知道在主线程上调用RunWorkerCompleted
方法。
我设法在我RunWorkerCompleted
的{{1}}命令中指出了问题 - 如果我改用form.Show();
,我就没有问题(BackgroundTesterBtn
始终在主线)。我需要在我的ArcMap项目中使用ShowDialog()
,这样用户就不会绑定到表单。
我还尝试在正常的WinForms项目中重现该错误。我添加了一个简单的项目,只打开没有ArcMap的第一个表单,但在这种情况下我无法重现错误 - 主线程上运行RunWorkerCompleted
,无论我使用Show()
还是{{1在打开RunWorkerCompleted
之前和之后。我尝试在Show()
之前添加第三个表单作为主要表单,但它没有改变结果。
{{3}}是我的简单sln(VS2005sp1) - 它需要
ESRI.ArcGIS.ADF(9.2.4.1420)
ESRI.ArcGIS.ArcMapUI(9.2.3.1380)
ESRI.ArcGIS.SystemUI(9.2.3.1380)
答案 0 :(得分:7)
是不是要在主线程上调用
OnWorkCompleted
的全部内容?为什么不按预期工作?
不,不是。
你不能在任何旧线程上运行任何旧东西。线程不是礼貌的对象,你可以简单地说“请运行这个,请”。
线程的更好的心理模型是货运列车。一旦它开始,它就在它自己的轨道上。你无法改变它的路线或阻止它。如果你想要影响它,你要么必须等到它到达下一个火车站(例如:让它手动检查一些事件),或者让它脱轨(Thread.Abort
和CrossThread异常会产生与让火车脱轨......小心!)。
Winforms控件排序支持这种行为(他们有Control.BeginInvoke
允许你在UI线程上运行任何函数),但这只能工作,因为他们有一个特殊的挂钩到窗口UI消息泵并编写一些特殊的处理程序。按照上述类比,他们的火车在车站办理登机手续并定期查找新方向,您可以使用该设施发布自己的指示。
BackgroundWorker
旨在用于通用目的(它不能绑定到Windows GUI),因此无法使用Windows Control.BeginInvoke
功能。它必须假设你的主线程是一个不可阻挡的“火车”做它自己的事情,所以完成的事件必须在工作线程中运行或根本不运行。
但是,当您使用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
}
在访问Result属性之前,RunWorkerCompleted事件处理程序应始终检查Error和Cancelled属性。如果引发了异常或操作被取消,则访问Result属性会引发异常。
答案 1 :(得分:2)
BackgroundWorker
检查委托实例是否指向支持接口ISynchronizeInvoke
的类。您的DAL层可能没有实现该接口。通常,您会在BackgroundWorker
上使用Form
,BackgroundWorker
支持该界面。
如果您想使用DAL图层中的Invoke
并想要从那里更新用户界面,您有三种选择:
ISynchronizeInvoke
方法BackgroundWorker
,并手动重定向调用(它只有三个方法和一个属性)SynchronizationContext.Current
之前(因此,在UI线程上),调用SynchronizationContext
并将内容实例保存在实例变量中。然后Send
将为您提供Invoke
方法,该方法将完全执行{{1}}所做的工作。答案 2 :(得分:2)
看起来像个错误:
http://connect.microsoft.com/VisualStudio/feedback/ViewFeedback.aspx?FeedbackID=116930
http://thedatafarm.com/devlifeblog/archive/2005/12/21/39532.aspx
所以我建议使用防弹(伪代码):
if(control.InvokeRequired)
control.Invoke(Action);
else
Action()
答案 3 :(得分:1)
避免GUI中交叉线程问题的最佳方法是use SynchronizationContext。