编写高质量代码改善C#程序的157个建议——建议87:区分WPF和WinForm的线程模型
建议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();
}
});
//如果有异常,就启动一个新任务
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环境中,就肯定会抛出System.InvalidOperationException异常。
理论上,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();
}
BeginInvoke方法接受的是一个Delegate类型的参数,在这里我们用一个Action来实现。
WPF应用程序的线程模型则完全依赖于DispatcherObject类型。所有的WPF控件都继承自一个抽象类Visual,而这个抽象类又最终继承自DispatcherObject类型。在这个DispatcherObject类型中有一个属性,两个方法。属性Dispatcher完成所有的工作线程和UI线程之间的调度任务。CheckAccess方法负责检测工作线程是否可以访问控件,如果是,则返回True;否则返回False。VerifyAccess方法则负责检测工作线程是否具有控件的访问权限,如果不能访问则抛出异常InvalidOperationException。
WinForm应用程序用类似CheckAccess的方式进行访问权限的判断;WPF应用程序则进行了改进,所有的UI控件都采用VerifyAccess的方式进行工作线程访问权限的判断。这直接决定了本建议开头处那个例子的输出,WPF只要判断出工作线程和UI线程不是同一个线程的,则直接抛出异常,而WinForm却有成功执行的余地。但是,WinForm的这种机制直接造成了程序的不稳定,因为即使在大部分情况下代码能很好的工作,可是在不确定的情况下,那样的代码中工作线程会直接操作UI元素,这样还是会抛出异常的。
考虑到WinForm在这个问题上的局限性,再次对WinForm的线程模型处理进行改进:
//用于表示主线程,在本例中就是UI线程
Thread mainThread; bool CheckAccess()
{
return mainThread == Thread.CurrentThread;
} void VerifyAccess()
{
if (!CheckAccess())
throw new InvalidOperationException("调用线程无法访问此对象,因为另一个线程拥有此对象");
} private void buttonStartAsync_Click(object sender, EventArgs e)
{
//当前线程就是主线程
mainThread = Thread.CurrentThread;
Task t = new Task(() =>
{
while (true)
{
if (!CheckAccess())
label1.BeginInvoke(new Action(() =>
{
label1.Text = DateTime.Now.ToString();
}));
else
label1.Text = DateTime.Now.ToString();
Thread.Sleep();
}
});
//如果有异常,就启动一个新任务
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中DispatcherObject的两个方法CheckAccess和VerifyAccess对线程模型进行了重新处理,增强了系统的稳定性。在实际工作中,我们也可以提取这两个方法为扩展方法,以便项目中的所有UI类型都能使用到。
WPF支持这两个方法,其全部代码如下所示(注意查看While循环部分):
private void buttonStart_Click(object sender, RoutedEventArgs e)
{
Task t = new Task(() =>
{
while (true)
{
this.Dispatcher.BeginInvoke(new Action(() =>
{
textBlock1.Text = DateTime.Now.ToString();
}));
Thread.Sleep();
}
});
//为了捕获异常,启动了一个新任务
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();
}
注意 为了演示方便,本建议中的异常没有传递到主线程。在实际编码中,应当始终考虑将异常包装到主线程。
转自:《编写高质量代码改善C#程序的157个建议》陆敏技
编写高质量代码改善C#程序的157个建议——建议87:区分WPF和WinForm的线程模型的更多相关文章
- 编写高质量代码改善C#程序的157个建议[1-3]
原文:编写高质量代码改善C#程序的157个建议[1-3] 前言 本文主要来学习记录前三个建议. 建议1.正确操作字符串 建议2.使用默认转型方法 建议3.区别对待强制转换与as和is 其中有很多需要理 ...
- 读书--编写高质量代码 改善C#程序的157个建议
最近读了陆敏技写的一本书<<编写高质量代码 改善C#程序的157个建议>>书写的很好.我还看了他的博客http://www.cnblogs.com/luminji . 前面部 ...
- 编写高质量代码改善C#程序的157个建议——建议157:从写第一个界面开始,就进行自动化测试
建议157:从写第一个界面开始,就进行自动化测试 如果说单元测试是白盒测试,那么自动化测试就是黑盒测试.黑盒测试要求捕捉界面上的控件句柄,并对其进行编码,以达到模拟人工操作的目的.具体的自动化测试请学 ...
- 编写高质量代码改善C#程序的157个建议——建议156:利用特性为应用程序提供多个版本
建议156:利用特性为应用程序提供多个版本 基于如下理由,需要为应用程序提供多个版本: 应用程序有体验版和完整功能版. 应用程序在迭代过程中需要屏蔽一些不成熟的功能. 假设我们的应用程序共有两类功能: ...
- 编写高质量代码改善C#程序的157个建议——建议155:随生产代码一起提交单元测试代码
建议155:随生产代码一起提交单元测试代码 首先提出一个问题:我们害怕修改代码吗?是否曾经无数次面对乱糟糟的代码,下决心进行重构,然后在一个月后的某个周一,却收到来自测试版的报告:新的版本,没有之前的 ...
- 编写高质量代码改善C#程序的157个建议——建议154:不要过度设计,在敏捷中体会重构的乐趣
建议154:不要过度设计,在敏捷中体会重构的乐趣 有时候,我们不得不随时更改软件的设计: 如果项目是针对某个大型机构的,不同级别的软件使用者,会提出不同的需求,或者随着关键岗位人员的更替,需求也会随个 ...
- 编写高质量代码改善C#程序的157个建议——建议153:若抛出异常,则必须要注释
建议153:若抛出异常,则必须要注释 有一种必须加注释的场景,即使异常.如果API抛出异常,则必须给出注释.调用者必须通过注释才能知道如何处理那些专有的异常.通常,即便良好的命名也不可能告诉我们方法会 ...
- 编写高质量代码改善C#程序的157个建议——建议152:最少,甚至是不要注释
建议152:最少,甚至是不要注释 以往,我们在代码中不写上几行注释,就会被认为是钟不负责任的态度.现在,这种观点正在改变.试想,如果我们所有的命名全部采用有意义的单词或词组,注释还有多少存在的价值. ...
- 编写高质量代码改善C#程序的157个建议——建议151:使用事件访问器替换公开的事件成员变量
建议151:使用事件访问器替换公开的事件成员变量 事件访问器包含两部分内容:添加访问器和删除访问器.如果涉及公开的事件字段,应该始终使用事件访问器.代码如下所示: class SampleClass ...
- 编写高质量代码改善C#程序的157个建议——建议150:使用匿名方法、Lambda表达式代替方法
建议150:使用匿名方法.Lambda表达式代替方法 方法体如果过小(如小于3行),专门为此定义一个方法就会显得过于繁琐.比如: static void SampeMethod() { List< ...
随机推荐
- AIX存储LV PV VG (转载)
1.基本概念:PV 物理卷:普通的直接访问的存储设备,有固定的和可移动的之分,代表性的就是硬盘.vg 卷组:AIX中最大的存储单位,一个卷组由一组物理硬盘组成,也就是由一个或多个物理卷组成.pp 物理 ...
- css引用方式
网页中引用CSS样式 内联样式 行内样式表 外部样式表 链接式 导入式 内嵌方式 style标签 <!doctype html> <html> <head> < ...
- Centos如何设置IP地址,LINUX怎么修改IP地址
对于很多刚刚接触linux的朋友来说,如何设置linux系统的IP地址,作为第一步,下面小编以centos系统为例,给大家演示如何给centos设置IP地址,如何修改linux 系统IP地址? 步骤阅 ...
- DOM库及常用方法封装
节点 nodeType nodeName nodeValue 元素节点 1 大写的标签名 null 文本节点 3 #text 文本内容 注释节点 8 #comment 注释内容 document 9 ...
- VMware Workstation 12 Pro(安装CentOS7)
之前安装了一版 Ubuntu 14.04版本,发现蛮不好用的,果断放弃,换上CentOS7版本(在远程服务器上的安装方式除了网络设置有差异,基本相同) VMware Workstation 12 Pr ...
- java根据pdf模版动态生成pdf
java根据pdf模版动态生成pdf package com.utils; import java.io.ByteArrayOutputStream; import java.io.File; imp ...
- nginx-rtmp-module--------------WIKI
https://github.com/arut/nginx-rtmp-module/wiki/Directives#idle_streams ============================= ...
- ffmpeg源码分析四:transcode_step函数 (转4)
原帖地址:http://blog.csdn.net/austinblog/article/details/25099979 该函数的主要功能是一步完整的转换工作,下面看看源代码: static int ...
- FastDFS 分布式文件系统
1 学习目标 了解项目中使用FastDFS的原因和意义. 掌握FastDFS的架构组成部分,能说出tracker和storage的作用. 了解FastDFS+nginx上传和下载的执行流程 ...
- go_指针
值传递:相当于在内存中拷贝一分变量出来,拷贝变量的改变对原变量不影响 引用传递:直接引用内存中的变量,会被改变 c,c++值传递和引用传递都有 Java,python多用引用传递 go语言指针简单之处 ...