3

我有一个用于数据库分页的简单 UserControl,它使用控制器来执行实际的 DAL 调用。我使用 aBackgroundWorker来执行繁重的工作,并在OnWorkCompleted事件中重新启用某些按钮、更改TextBox.Text属性并为父表单引发事件。

表单 A 包含我的用户控件。当我单击打开表单 B 的某个按钮时,即使我没有在“那里”做任何事情并关闭它,并尝试从我的数据库中引入下一页,OnWorkCompleted在工作线程(而不是我的主线程),并引发跨线程异常。

目前我InvokeRequired在那里的处理程序处添加了一个检查,但不是OnWorkCompleted要在主线程上调用整个点吗?为什么它不能按预期工作?

编辑:

我设法将问题缩小到 arcgis 和BackgroundWorker. 我有以下解决方案,它向 arcmap 添加了一个命令,Form1用两个按钮打开一个简单的命令。

第一个按钮运行BackgroundWorker睡眠 500 毫秒并更新计数器。在RunWorkerCompleted方法中它检查InvokeRequired并更新标题以显示该方法最初是在主线程还是在工作线程中运行。第二个按钮刚刚打开Form2,其中不包含任何内容。

起初,所有的调用RunWorkerCompletedare都是在主线程内进行的(正如预期的那样——这就是 RunWorkerComplete 方法的全部要点,至少我从MSDN上了解到的BackgroundWorker

在打开和关闭之后Form2RunWorkerCompleted总是在工作线程上被调用。我想补充一点,我可以让这个问题的解决方案保持原样(InvokeRequiredRunWorkerCompleted方法中检查),但我想了解为什么它会违背我的期望。在我的“真实”代码中,我想始终知道该RunWorkerCompleted方法是在主线程上调用的。

我设法form.Show();在我的命令中指出问题BackgroundTesterBtn- 如果我ShowDialog()改用,我没有问题(RunWorkerCompleted总是在主线程上运行)。我确实需要Show()在我的 ArcMap 项目中使用,这样用户就不会被绑定到表单。

我还尝试在普通的 WinForms 项目中重现该错误。我添加了一个简单的项目,它只打开没有 ArcMap 的第一个表单,但在那种情况下,我无法重现错误 - 在RunWorkerCompleted主线程上运行,无论是我使用Show()还是ShowDialog()打开之前和之后Form2。我尝试在 my 之前添加第三种形式作为主要形式Form1,但它并没有改变结果。

是我的简单 sln (VS2005sp1) - 它需要

ESRI.ArcGIS.ADF(9.2.4.1420)

ESRI.ArcGIS.ArcMapUI(9.2.3.1380)

ESRI.ArcGIS.SystemUI (9.2.3.1380)

4

4 回答 4

6

重点不是OnWorkCompleted要在主线程上调用吗?为什么它不能按预期工作?

不,这不对。
你不能在任何旧线程上运行任何旧东西。线程不是礼貌的对象,您可以简单地说“请运行它”。

更好的线程心智模型是货运列车。一旦开始,它就会走上自己的轨道。你不能改变它的路线或停止它。如果你想影响它,你要么必须等到它到达下一个火车站(例如:让它手动检查某些事件),要么让它脱轨(Thread.AbortCrossThread 异常的后果与让火车脱轨的后果大致相同。. 。 谨防!)。

Winforms 控件有点支持这种行为(它们可以Control.BeginInvoke让您在 UI 线程上运行任何功能),但这只是因为它们在 Windows UI 消息泵中有一个特殊的挂钩并编写了一些特殊的处理程序。按照上面的类比,他们的火车会在车站登​​记并定期寻找新的方向,您可以使用该设施发布您自己的方向。

BackgroundWorker被设计为通用目的(它不能绑定到 Windows GUI),因此它不能使用 WindowsControl.BeginInvoke功能。它必须假设你的主线程是一个不可阻挡的“火车”做它自己的事情,所以完成的事件必须在工作线程中运行或根本不运行。

但是,当您使用 winforms 时,在您的处理程序中,您可以使用我上面提到的功能OnWorkCompleted让 Window 执行另一个回调。BeginInvoke像这样:

// 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
}

另外,不要忘记这一点:

您的 RunWorkerCompleted 事件处理程序应始终在访问 Result 属性之前检查 Error 和 Canceled 属性。如果引发异常或取消操作,则访问 Result 属性会引发异常。

于 2009-05-04T04:32:00.683 回答
2

BackgroundWorker检查委托实例是否指向支持接口的类ISynchronizeInvoke。您的 DAL 层可能没有实现该接口。通常,您会使用BackgroundWorkeron a Form,它确实支持该接口。

如果您想BackgroundWorker从 DAL 层使用并想从那里更新 UI,您有三个选项:

  • 你会一直调用这个Invoke方法
  • 在 DAL 类上实现接口ISynchronizeInvoke,并手动重定向调用(它只有三个方法和一个属性)
  • 在调用BackgroundWorker(因此,在 UI 线程上)之前,调用SynchronizationContext.Current内容实例并将其保存在实例变量中。然后SynchronizationContext将为您提供Send方法,该方法将完全执行该Invoke操作。
于 2009-05-04T04:49:52.073 回答
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()
于 2009-05-22T13:26:43.323 回答
1

避免 GUI 中的跨线程问题的最佳方法是使用 SynchronizationContext

于 2009-05-04T04:51:55.173 回答