2006年12月26日星期二

WinForm和WPF中在工作者线程里更新UI对象

这应该是每一个做客户端编程的人都曾经遇到过的问题:进行一个耗时较长的运算,并且在运算完成后需要更新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里面是很类似的:



void DoWork()
{
// do work
...
// call dispatcher to update ui
Dispatcher.Invoke(System.Windows.Threading.DispatcherPriority.Normal, (System.Windows.Forms.MethodInvoker)delegate() { UpdateUI(); });
}

不过Dispatcher有一个问题,它不像WinForm里面,有一个InvokeRequired Property指示是否需要进行线程切换,所以在这里就不管是否是在UI线程内部进行的调用,都会进行一次线程切换了。



Technorati :

System.Windows.Forms.Timer和System.Timers.Timer的区别

.NET Framework里面提供了三种Timer:

  • System.Windows.Forms.Timer

  • System.Timers.Timer

  • System.Threading.Timer

在我的记忆中,Visual Studio 2003的工具箱里面默认提供了System.Windows.Forms.Timer和System.Timers.Timer两种,而Visual Studio 2005中确只默认提供了System.Windows.Forms.Timer这一种。这里简单的介绍一下这两种Timer的区别。

System.Windows.Forms.Timer是使用得比较多的Timer,Timer Start之后定时(按设定的Interval)调用挂接在Tick事件上的EvnetHandler。在这种Timer的EventHandler中可以直接获取和修改UI元素而不会出现问题--因为这种Timer实际上就是在UI线程自身上进行调用的。也正是因为这个原因,导致了在Timer的EventHandler里面进行长时间的阻塞调用,将会阻塞界面响应的后果。下面是一个简单的例子:

public class MainForm : Form
{
private void MainForm_Load(object sender, EventArgs e)
{
timer.Interval = 1000;
timer.Tick += delegate(object o, EventArgs args)
{
DoWork();
};
timer.Start();
}

private void DoWork()
{
for (int i = 0; i < 10; i++)
{
System.Threading.Thread.Sleep(1000);
}
}
System.Windows.Forms.Timer timer = new System.Windows.Forms.Timer();
}

在这个例子中,DoWork方法里面将会阻塞10秒,在这10秒之内,UI将会失去响应。而通过使用System.Timers.Timer,就可以解决这个问题。因为System.Timers.Timer是在.NET的Thread Pool上面运行的,而不是直接在UI Thread上面运行,所以在这种Timer的EventHandler里面进行耗时较长的计算不会导致UI失去响应。但是这里有两个地方需要注意:

  1. 因为一般来说System.Timers.Timer不是运行在UI Thread上面的,所以如果要在这种Timer的EventHandler里面更新UI元素的话,需要进行一次线程切换,在WinForm开发中一般通过UI元素的Invoke方法完成:

    private void DoWork()
    {
    for (int i = 0; i < 10; i++)
    {
    System.Threading.Thread.Sleep(1000);
    }
    this.Invoke(new UpdateUICallBack(UpdateUI));
    }

    private delegate void UpdateUICallBack();

    private void UpdateUI()
    {
    }

  2. System.Timers.Timer有一个Property:SynchronizingObject 。如果设置了这个Property(一般是某个Form),那么之后对Timer挂接的EventHandler的调用将会在创建这个UI元素的线程上进行(一般来说就是UI线程)。值得注意的是,如果你通过WinForm设计器把System.Timers.Timer拖放到Form上,那么这个Property将会自动被设置。此时这种Timer就和System.Windows.Forms.Timer的效果一样:长调用将会阻塞界面。




Technorati :