在WinForm应用程序中进行批处理时,如何屈服于UI线程以更新UI?
我有一个用CNET .NET 3.5编写的WinForms应用程序。它运行一个漫长的批处理过程。我希望该应用程序更新批处理过程的状态。更新UI的最佳方法是什么?
解决方案
Application.DoEvents()还是可能在单独的线程上运行批处理?
BackgroundWorker听起来像我们想要的对象。
使用backgroundworker组件在单独的线程中运行批处理,这样就不会影响UI线程。
在后台线程上运行冗长的过程。后台工作程序类是执行此操作的一种简便方法,它为发送进度更新和完成事件提供了简单的支持,为此我们需要在正确的线程上调用事件处理程序。这样可以使代码简洁明了。
要显示更新,进度条或者状态条文本是两种最常用的方法。
要记住的关键是,如果我们在后台线程上执行操作,则必须切换到UI线程才能更新Windows控件等。
快速而肮脏的方法是使用Application.DoEvents(),但这可能会导致处理订单事件出现问题。所以不推荐
问题可能不是我们必须屈服于ui线程,而是在ui线程上进行了处理,阻止了它处理消息。我们可以使用backgroundworker组件在其他线程上执行批处理,而不会阻塞UI线程。
DoEvents()是我一直在寻找的东西,但是我也投票赞成backgroundworker的答案,因为这看起来像是一个很好的解决方案,我将进行进一步的研究。
我想重申一下我以前的评论者提到的内容:请尽可能避免使用DoEvents(),因为这几乎总是"黑客入侵"的一种形式,并且会引起维护方面的噩梦。
如果走BackgroundWorker道路(我建议这样做),则要调用控件的任何方法或者属性,就必须处理对UI的跨线程调用,因为这些方法或者属性是仿射的,必须仅从创建它们的线程。适当使用Control.Invoke()和/或者Control.BeginInvoke()。
如果我们在后台/工作线程中运行,则可以在一个UI控件上调用Control.Invoke
以在UI线程中运行委托。
" Control.Invoke"是同步的(等待委托返回)。如果我们不想等待,可以使用.BeginInvoke()
仅将命令排队。
.BeginInvoke()的返回值使我们可以检查该方法是否完成或者等待该方法完成。
为了增强人们对DoEvents的评价,以下是对可能发生的事情的描述。
假设我们有某种形式的表格,上面有数据,并且长期运行事件是将其保存到数据库或者基于该表格生成报告。我们开始保存或者生成报告,然后定期调用DoEvents,以使屏幕保持绘制状态。
不幸的是,屏幕不仅是绘画,还会对用户操作做出反应。这是因为DoEvents停止了我们正在执行的操作,以处理所有等待Winforms应用程序处理的Windows消息。这些消息包括重画请求,以及任何用户键入,单击等。
因此,例如,当我们保存数据时,用户可以执行一些操作,例如使应用程序显示一个模态对话框,而该对话框与长时间运行的任务完全无关(例如,Help-> About)。现在,我们正在对已经运行长时间运行的任务中的新用户操作做出反应。当我们调用它时所有正在等待的事件完成时,DoEvents将返回,然后长时间运行的任务将继续。
如果用户不关闭模式对话框怎么办?我们长期运行的任务将永远等待,直到关闭此对话框。如果我们要提交数据库并进行交易,那么现在我们在用户喝咖啡时将交易保持为打开状态。事务超时或者丢失持久性工作,或者事务没有超时并且可能使数据库的其他用户陷入僵局。
这里发生的是Application.DoEvents使代码可重入。请参阅此处的维基百科定义。请注意文章顶部的一些要点,即要使代码可重入,请注意以下几点:
- 不得包含任何静态(或者全局)非恒定数据。
- 必须仅对调用方提供的数据起作用。
- 绝对不能依赖于对单例资源的锁定。
- 不得调用非可重入的计算机程序或者例程。
WinForms应用程序中长时间运行的代码不太可能仅对调用方传递给该方法的数据起作用,不保存静态数据,不保存锁并仅调用其他可重入方法。
就像这里很多人说的那样,DoEvents会导致代码中一些非常奇怪的场景。它可能导致的错误很难诊断,用户不太可能告诉我们"哦,这可能是发生的,因为我在等待保存时单击了不相关的按钮"。