建议87:区分WPF和WinForm的线程模型
WPF和WinForm窗体应用程序都有一个要求,那就是UI元素(如Button、TextBox等)必须由创建它的那个线程进行更新。WinForm在这方面的限制并不是很严格,所以像下面这样的代码,在WinForm中大部分情况下还能运行(本建议后面会详细解释为什么会出现这种现象):
private void buttonStartAsync_Click(object sender, EventArgs e) { Task t = new Task(() => { while (true) { label1.Text = DateTime.Now.ToString(); Thread.Sleep(1000); } }); //如果有异常,就启动一个新任务 t.ContinueWith((task) => { try { task.Wait(); } catch (AggregateException ex) { foreach (Exception inner in ex.InnerExceptions) { MessageBox.Show(string.Format("异常类型:{0}{1}来自: {2}{3}异常内容:{4}", inner.GetType(),Environment.NewLine, inner.Source, Environment.NewLine, inner.Message)); } } }, TaskContinuationOptions.OnlyOnFaulted); t.Start(); } |
但是,相同的一段代码如果放到WPF环境中,就肯定会抛出异常,如图所示。
图 WPF环境中的异常
理论上,WinForm和WPF的线程模型非常接近,它们最后都是调用API(GetMessage或PeekMessage)来处理其他线程发送过来的消息,这些消息存储在系统的一个消息队列中。在WinForm和WPF中,创建主界面的线程就是主线程,也就是UI线程,UI线程负责处理该消息队列。只是两者在处理消息队列的上层机制上稍微有一些不同,这就造成了同样的代码得到不同的结果。
在WinForm框架中有一个ISynchronizeInvoke接口,所有的UI元素(表现为Control)都继承了该接口。其中,接口中的InvokdRequired属性表示了当前线程是否是创建它的线程。接口中的Invoke和BeginInvoke方法负责将消息发送到消息队列中,这样,UI线程就能够正确处理它了。那么,上面的这段代码在WinForm上的改进版本为(仅列出While循环部分):
while (true) { if (label1.InvokeRequired) label1.BeginInvoke(new Action(() => { label1.Text = DateTime.Now.ToString(); })); else label1.Text = DateTime.Now.ToString(); Thread.Sleep(1000); } |