【转】编写高质量代码改善C#程序的157个建议——建议87:区分WPF和WinForm的线程模型

摘要:
建议87:区分WPF和WinForm的线程模型WPF和WinForm窗体应用程序都有一个要求,那就是UI元素必须由创建它的那个线程进行更新。在WinForm和WPF中,创建主界面的线程就是主线程,也就是UI线程,UI线程负责处理该消息队列。WPF应用程序的线程模型则完全依赖于DispatcherObject类型。这直接决定了本建议开头处那个例子的输出,WPF只要判断出工作线程和UI线程不是同一个线程的,则直接抛出异常,而WinForm却有成功执行的余地。

建议87:区分WPF和WinForm的线程模型
WPF和WinForm窗体应用程序都有一个要求,那就是UI元素(如Button、TextBox等)必须由创建它的那个线程进行更新。WinForm在这方面的限制并不是很严格,所以像下面这样的代码,在WinForm中大部分情况下还能运行(本建议后面会详细解释为什么会出现这种现象):

private void buttonStartAsync_Click(objectsender, 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 inex.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();  
            }));  
    elselabel1.Text =DateTime.Now.ToString();  
    Thread.Sleep(1000);  
} 

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;  
 
boolCheckAccess()  
{  
    return mainThread ==Thread.CurrentThread;  
}  
 
voidVerifyAccess()  
{  
    if (!CheckAccess())  
        throw new InvalidOperationException("调用线程无法访问此对象,因为另一个线程拥有此对象");  
}  
 
private void buttonStartAsync_Click(objectsender, EventArgs e)  
{  
    //当前线程就是主线程  
    mainThread =Thread.CurrentThread;  
    Task t = new Task(() =>{  
            while (true)  
            {  
                if (!CheckAccess())  
                    label1.BeginInvoke(new Action(() =>{  
                            label1.Text =DateTime.Now.ToString();  
                        }));  
                elselabel1.Text =DateTime.Now.ToString();  
                Thread.Sleep(1000);  
            }  
        });  
    //如果有异常,就启动一个新任务  
    t.ContinueWith((task) =>{  
        try{  
            task.Wait();  
        }  
        catch(AggregateException ex)  
        {  
            foreach (Exception inner inex.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(objectsender, RoutedEventArgs e)  
{  
    Task t = new Task(() =>{  
        while (true)  
        {  
            this.Dispatcher.BeginInvoke(new Action(() =>{  
                    textBlock1.Text =DateTime.Now.ToString();  
                }));  
            Thread.Sleep(1000);  
        }  
    });  
    //为了捕获异常,启动了一个新任务  
    t.ContinueWith((task) =>{  
        try{  
            task.Wait();  
        }  
        catch(AggregateException ex)  
        {  
            foreach (Exception inner inex.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的线程模型》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇jQueryMobile学习笔记(二)Ddr2,ddr3,ddr4内存条的读写速率下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

2019-11-29-WPF-元素裁剪-Clip-属性

title author date CreateTime categories WPF 元素裁剪 Clip 属性 lindexi 2019-11-29 08:24:24 +0800 2019-1-3 15:57:0 +0800 WPF 本文介绍如何在 WPF 使用 Clip 裁剪元素 在 WPF 的 UIElement 提供了 Clip 属性...

WPF笔记12: 线程处理模型

WPF笔记12: 线程处理模型 本文摘要: 1:理解与UI相关的多线程操作; 2:多个窗口多个线程 1:理解与UI相关的多线程操作     首先来说说传统Winform。我们知道传统Winform新起工作线程,在工作线程中不能对界面元素进行操作。如下面的代码,运行会报错“线程间操作无效: 从不是创建控件“label1”的线程访问它。”:...

jvm锁的四种状态 无锁状态 偏向锁状态 轻量级锁状态 重量级锁状态

一:java多线程互斥,和java多线程引入偏向锁和轻量级锁的原因? --->synchronized是在jvm层面实现同步的一种机制。    jvm规范中可以看到synchronized在jvm里实现原理,jvm基于进入和退出Monitor对象来实现方法同步和代码块同的。在代码同步的开始位置织入monitorenter,在结束同步的位置(正常结束和...

转载:winform的DataGridView中用C#实现按钮列置灰

DataGridView 控件包括 DataGridViewButtonCell 类,该类用于显示具有类似按钮的用户界面 (UI) 的单元格。但 DataGridViewButtonCell 不提供禁用由单元格显示的按钮外观的方式。下面的代码示例演示如何自定义 DataGridViewButtonCell 类来显示可以显示为禁用的按钮。本示例定义一个新的单...

利用C#线程窗口调试多线程程序

       从网上的资料判断,调试多线程程序似乎就一下3种方法。 1、在日志的某个地方写日志文件。 优点:不会干扰程序的执行,特别是对网络的多线程通信。 缺点:每次都需要打开日志文件以查看进程运行的信息。 2、利用断点进行调试。 优点:直观,可以直接看到运行过程的值 缺点:在多个线程设置断点,可能让程序跳来跳去,还需要额外地分出一部分精力用来理清程序...

【转】JMeter学习(三十二)属性和变量

一、Jmeter中的属性: 1、JMeter属性统一定义在jmeter.properties文件中,我们可以在该文件中添加自定义的属性 2、JMeter属性在测试脚本的任何地方都是可见的(全局),通常被用来定义一些JMeter使用的默认值,可以用于在线程间传递信息。 3、JMeter属性可以在测试计划中通过函数 _P 进行引用,但是不能作为特定线程的变量值...