这应该是每一个做客户端编程的人都曾经遇到过的问题:进行一个耗时较长的运算,并且在运算完成后需要更新UI对象的状态。一般来说这种运算(或者调用)我们会放在一个工作者线程中进行,完成了运算(或调用)之后,再根据运算结果通过一定的机制更新UI对象的状态。在WinForm开发中是通过Invoke方法来完成的。Invoke是在System.Windows.Forms.Control里面实现的,由于这个Control是WinForm所有UI元素类的基类,所以所有的WinForm UI对象都可以提供Invoke调用来实现从工作者线程到UI线程的切换。但是Invoke需要一个delegate参数,下面是一个简单的示例代码:
// This method is run on a worker thread
private void DoWork()
{
...
// Do Some works
this.Invoke((MethodInvoker)delegate(){ UpdateUI();});
}// This method will update ui element
private void UpdateUI()
{
}
这里我们甚至可以进一步改进UpdateUI方法,使其可以适应在UI线程内部被调用和被工作者线程中调用的两种不同情况,这是通过System.Windows.Forms.Control.InvokeRequired来实现的:
// This method is run on a worker thread
private void DoWork()
{
...
// Do Some works
UpdateUI();
}// This method will update ui element
private void UpdateUI()
{
if (InvokeRequired)
{
Invoke((MethodInvoker)delegate(){UpdateUI();});
}
else
{
// Real update code
...
}
}
在.NET Framework2.0里,提供了一个新的类:BackgroundWorker,更简单的实现了工作者线程完成后更新UI对象的问题。BackgroundWorker提供了DoWork和RunWorkerCompleted这两个event供开发者挂接自己的delegate方法(当然还有其他event,但是先让我们只关注这两个)。DoWork挂接的delegate将会在.NET Thread Pool上面执行,而RunWorkerCompleted挂接的delegate将会在创建BackgroundWorker的线程上执行,一般来说也就是UI线程,所以我们可以在RunWorkerCompleted挂接的delegate里面自由的修改UI元素的状态而不用担心出现什么问题。BackgroundWorker还有其他一些方便的功能例如中途取消操作,操作进度报告等等,这里就不详细描述了。
在WPF里,所有的UI元素类都是重新实现的,机制与WinForm大不相同,也不从System.Windows.Forms.Control派生,所以也没有Invoke方法。那么在WPF里面应该怎么样处理工作者线程到UI线程的切换呢?这里需要使用Dispatcher。我们首先来看一下WPF里面UI对象的继承体系:
Dispathcer这个Property就是在DispatcherObject里面引入的。WPF里面的UI控件要嘛就是从图中的Control派生的,要嘛就是从FrameworkElement派生的,所以都包含了Dispatcher这个Property。使用的时候跟WinForm里面是很类似的:
不过Dispatcher有一个问题,它不像WinForm里面,有一个InvokeRequired Property指示是否需要进行线程切换,所以在这里就不管是否是在UI线程内部进行的调用,都会进行一次线程切换了。void DoWork()
{
// do work
...
// call dispatcher to update ui
Dispatcher.Invoke(System.Windows.Threading.DispatcherPriority.Normal, (System.Windows.Forms.MethodInvoker)delegate() { UpdateUI(); });
}