建议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的线程模型的更多相关文章

  1. 编写高质量代码改善C#程序的157个建议[1-3]

    原文:编写高质量代码改善C#程序的157个建议[1-3] 前言 本文主要来学习记录前三个建议. 建议1.正确操作字符串 建议2.使用默认转型方法 建议3.区别对待强制转换与as和is 其中有很多需要理 ...

  2. 读书--编写高质量代码 改善C#程序的157个建议

    最近读了陆敏技写的一本书<<编写高质量代码  改善C#程序的157个建议>>书写的很好.我还看了他的博客http://www.cnblogs.com/luminji . 前面部 ...

  3. 编写高质量代码改善C#程序的157个建议——建议157:从写第一个界面开始,就进行自动化测试

    建议157:从写第一个界面开始,就进行自动化测试 如果说单元测试是白盒测试,那么自动化测试就是黑盒测试.黑盒测试要求捕捉界面上的控件句柄,并对其进行编码,以达到模拟人工操作的目的.具体的自动化测试请学 ...

  4. 编写高质量代码改善C#程序的157个建议——建议156:利用特性为应用程序提供多个版本

    建议156:利用特性为应用程序提供多个版本 基于如下理由,需要为应用程序提供多个版本: 应用程序有体验版和完整功能版. 应用程序在迭代过程中需要屏蔽一些不成熟的功能. 假设我们的应用程序共有两类功能: ...

  5. 编写高质量代码改善C#程序的157个建议——建议155:随生产代码一起提交单元测试代码

    建议155:随生产代码一起提交单元测试代码 首先提出一个问题:我们害怕修改代码吗?是否曾经无数次面对乱糟糟的代码,下决心进行重构,然后在一个月后的某个周一,却收到来自测试版的报告:新的版本,没有之前的 ...

  6. 编写高质量代码改善C#程序的157个建议——建议154:不要过度设计,在敏捷中体会重构的乐趣

    建议154:不要过度设计,在敏捷中体会重构的乐趣 有时候,我们不得不随时更改软件的设计: 如果项目是针对某个大型机构的,不同级别的软件使用者,会提出不同的需求,或者随着关键岗位人员的更替,需求也会随个 ...

  7. 编写高质量代码改善C#程序的157个建议——建议153:若抛出异常,则必须要注释

    建议153:若抛出异常,则必须要注释 有一种必须加注释的场景,即使异常.如果API抛出异常,则必须给出注释.调用者必须通过注释才能知道如何处理那些专有的异常.通常,即便良好的命名也不可能告诉我们方法会 ...

  8. 编写高质量代码改善C#程序的157个建议——建议152:最少,甚至是不要注释

    建议152:最少,甚至是不要注释 以往,我们在代码中不写上几行注释,就会被认为是钟不负责任的态度.现在,这种观点正在改变.试想,如果我们所有的命名全部采用有意义的单词或词组,注释还有多少存在的价值. ...

  9. 编写高质量代码改善C#程序的157个建议——建议151:使用事件访问器替换公开的事件成员变量

    建议151:使用事件访问器替换公开的事件成员变量 事件访问器包含两部分内容:添加访问器和删除访问器.如果涉及公开的事件字段,应该始终使用事件访问器.代码如下所示: class SampleClass ...

  10. 编写高质量代码改善C#程序的157个建议——建议150:使用匿名方法、Lambda表达式代替方法

    建议150:使用匿名方法.Lambda表达式代替方法 方法体如果过小(如小于3行),专门为此定义一个方法就会显得过于繁琐.比如: static void SampeMethod() { List< ...

随机推荐

  1. 【BZOJ】2818: Gcd(欧拉函数+质数)

    题目 传送门:QWQ 分析 仪仗队 呃,看到题后感觉很像上面的仪仗队. 仪仗队求的是$ gcd(a,b)=1 $ 本题求的是$ gcd(a,b)=m $ 其中m是质数 把 $ gcd(a,b)=1 $ ...

  2. 【UVa】1601 The Morning after Halloween(双向bfs)

    题目 题目     分析 双向bfs,对着书打的,我还调了好久.     代码 #include<cstdio> #include<cstring> #include<c ...

  3. [Python] Regular Expressions

    1. regular expression Regular expression is a special sequence of characters that helps you match or ...

  4. Python实践练习目录

    缘由 做中学才是最好的方法,通过这些项目来加强自己的Python掌握程度. 原则 成体系地学,不搞"题海战术" 通所有不如精一物,精一物方可通所有 走心学,忌浮躁 项目列表 字符串 ...

  5. Ansible 快速上手

    Ansible优点: 充分利用现有设施.使用 Ansible 无需安装服务端和客户端,只要 SSH 即可.这意味着,任何一台装有 Ansible 的机器都可以成为强大的管理端.我觉得,这种去中心化的思 ...

  6. 汉字转拼音开源工具包Jpinyin介绍

    最近要实现一个根据词语得到词语对应拼音的功能,找到了Jpinyin这个开源工具包,使用下来发现它非常强大,完全满足我的需求,下面对它做一个简单的介绍,希望能够帮助到有需要的朋友. https://gi ...

  7. C# 6 与 .NET Core 1.0 高级编程 - C# 6 的新功能

    个人原创译文,转载请注明出处.有不对的地方欢迎指出与交流. 英文原文:Professional C# 6 and .NET Core 1.0 - What’s New in C# 6 C# 6 的新功 ...

  8. Linux 如何杀死僵尸进程

    问题描述: shell > top top - :: up days, :, user, load average: 0.23, 0.81, 1.07 Tasks: total, running ...

  9. Shell32.ShellClass服务器操作系统无法获取 音频文件时长问题

    前言: 上传音频文件,自动写入此音频文件的时长,这里用 COM组件Microsoft Shell Controls And Automation来实现. 首先 1.引用:Microsoft Shell ...

  10. spring是什么

    spring是一个容器,用于降低代码间的耦合度,根据不同的代码采用了ioc和aop这二种技术来解耦合. 比如转账操作:a用户少1000,b用户多1000.这是主业务逻辑   IOC 涉及到的事务,日志 ...