加入收藏 | 设为首页 | 会员中心 | 我要投稿 李大同 (https://www.lidatong.com.cn/)- 科技、建站、经验、云计算、5G、大数据,站长网!
当前位置: 首页 > 大数据 > 正文

.NET 事件模型教程(一)

发布时间:2020-12-17 00:29:52 所属栏目:大数据 来源:网络整理
导读:目录 事件、事件处理程序概念 问题描述:一个需要较长时间才能完成的任务 高耦合的实现 事件模型的解决方案,简单易懂的VB.NET版本 委托(delegate)简介 C#实现 向“.NETFramework类库设计指南”靠拢,标准实现 事件、事件处理程序概念 在面向对象理论中,

目录

  • 事件、事件处理程序概念
  • 问题描述:一个需要较长时间才能完成的任务
  • 高耦合的实现
  • 事件模型的解决方案,简单易懂的VB.NET版本
  • 委托(delegate)简介
  • C#实现
  • 向“.NETFramework类库设计指南”靠拢,标准实现



事件、事件处理程序概念

在面向对象理论中,一个对象(类的实例)可以有属性(property,获取或设置对象的状态)、方法(method,对象可以做的动作)等成员外,还有事件(event)。所谓事件,是对象内部状态发生了某些变化、或者对象做某些动作时(或做之前、做之后),向外界发出的通知。打个比方就是,对象“张三”肚子疼了,然后他站在空地上大叫一声“我肚子疼了!”事件就是这个通知。

那么,相对于对象内部发出的事件通知,外部环境可能需要应对某些事件的发生,而做出相应的反应。接着上面的比方,张三大叫一声之后,救护车来了把它接到医院(或者疯人院,呵呵,开个玩笑)。外界因应事件发生而做出的反应(具体到程序上,就是针对该事件而写的那些处理代码),称为事件处理程序(eventhandler)

事件处理程序必须和对象的事件挂钩后,才可能会被执行。否则,孤立的事件处理程序不会被执行。另一方面,对象发生事件时,并不一定要有相应的处理程序。就如张三大叫之后,外界环境没有做出任何反应。也就是说,对象的事件和外界对该对象的事件处理之间,并没有必然的联系,需要你去挂接。

在开始学习之前,我希望大家首先区分“事件”和“事件处理程序”这两个概念。事件是隶属于对象(类)本身的,事件处理程序是外界代码针对对象的事件做出的反应。事件,是对象(类)的设计者、开发者应该完成的;事件处理程序是外界调用方需要完成的。简单的说,事件是“内”;事件处理程序是“外”。

了解以上基本概念之后,我们开始学习具体的代码实现过程。因为涉及代码比较多,限于篇幅,我只是将代码中比较重要的部分贴在文章里,进行解析,剩余代码还是请读者自己查阅,我已经把源代码打了包提供下载。我也建议你对照这些源代码,来学习教程。[下载本教程的源代码]
http://www.percyboy.com/p/i/eventmodeldemo.zip

问题描述:一个需要较长时间才能完成的任务

Demo1A,问题描述。这是一个情景演示,也是本教程中其他Demo都致力于解决的一个“实际问题”:Worker类中有一个可能需要较长时间才能完成的方法DoLongTimeTask:

复制 保存
using System;
using System.Threading;

namespace percyboy.EventModelDemo.Demo1A
{
    // 需要做很长时间才能完成任务的 Worker,没有加入任何汇报途径。
    public class Worker
    {
        // 请根据你的机器配置情况,设置 MAX 的值。
        // 在我这里(CPU: AMD Sempron 2400+,DDRAM 512MB)
        // 当 MAX = 10000,任务耗时 20 秒。
        private const int MAX = 10000;

        public Worker()
        {
        }

        public void DoLongTimeTask()
        {
            int i;
            bool t = false;
            for (i = 0; i <= MAX; i++)
            {
                // 此处 Thread.Sleep 的目的有两个:
                // 一个是不让 CPU 时间全部耗费在这个任务上:
                // 因为本例中的工作是一个纯粹消耗 CPU 计算资源的任务。
                // 如果一直让它一直占用 CPU,则 CPU 时间几乎全部都耗费于此。
                // 如果任务时间较短,可能影响不大;
                // 但如果任务耗时也长,就可能会影响系统中其他任务的正常运行。
                // 所以,Sleep 就是要让 CPU 有机会“分一下心”,
                // 处理一下来自其他任务的计算请求。
                //
                // 当然,这里的主要目的是为了让这个任务看起来耗时更长一点。
                Thread.Sleep(1);

                t = !t;
            }
        }
    }
}


界面很简单(本教程中其他Demo也都沿用这个界面,因为我们主要的研究对象是Worker.cs):



单击“Start”按钮后,开始执行该方法。(具体的机器配置条件,完成此任务需要的时间也不同,你可以根据你的实际情况调整代码中的MAX值。)

在没有进度指示的情况下,界面长时间的无响应,往往会被用户认为是程序故障或者“死机”,而实际上,你的工作正在进行还没有结束。此次教程就是以解决此问题为实例,向你介绍.NET中事件模型的原理、设计与具体编码实现。

高耦合的实现

Demo1B,高度耦合。有很多办法可以让Worker在工作的时候向用户界面报告进度,比如最容易想到的:

复制 保存
public void DoLongTimeTask()
{
    int i;
    bool t = false;
    for (i = 0; i <= MAX; i++)
    {
        Thread.Sleep(1);

        t = !t;

        //在此处书写刷新用户界面状态栏的代码
    }
}


如果说DoLongTimeTask是用户界面(Windows窗体)的一个方法,那么上面蓝色部分或许很简单,可能只不过是如下的两行代码:

复制 保存
double rate = (double) i / (double) MAX;
this.statusbar.Text = String.Format(@"已完成 {0:P2} ...",rate);


不过这样的话,DoLongTimeTask就是这个Windows窗体的一部分了,显然它不利于其他窗体调用这段代码。那么:Worker类应该作为一个相对独立的部分存在。源代码Demo1B中给出了这样的一个示例(应该还有很多种、和它类似的方法):

Windows窗体Form1中单击“Start”按钮后,初始化Worker类的一个新实例,并执行它的DoLongTimeTask方法。但你应该同时看到,Form1也赋值给Worker的一个属性,在Worker执行DoLongTimeTask方法时,通过这个属性刷新Form1的状态栏。Form1和Worker之间相互粘在一起:Form1依赖于Worker类(因为它单击按钮后要实例化Worker),Worker类也依赖于Form1(因为它在工作时,需要访问Form1)。这二者之间形成了高度耦合。

高度耦合同样不利于代码重用,你仍然无法在另一个窗体里使用Worker类,代码灵活度大为降低。正确的设计原则应该是努力实现低耦合:如果Form1必须依赖于Worker类,那么Worker类就不应该再反过来依赖于Form1。


下面我们考虑使用.NET事件模型解决上述的“高度耦合”问题:

让Worker类在工作时,向外界发出“进度报告”的事件通知(RateReport)。同时,为了演示更多的情景,我们让Worker类在开始DoLongTimeTask之前发出一个“我要开始干活了!总任务数有N件。”的事件通知(StartWork),并在完成任务时发出“任务完成”的事件通知(EndWork)。

采用事件模型后,类Worker本身并不实际去刷新Form1的状态栏,也就是说Worker不依赖于Form1。在Form1中,单击“Start”按钮后,Worker的一个实例开始工作,并发出一系列的事件通知。我们需要做的是为Worker的事件书写事件处理程序,并将它们挂接起来。

事件模型的解决方案,简单易懂的VB.NET版本

Demo1C,VB.NET代码。虽然本教程以C#为示例语言,我还是给出一段VB.NET的代码辅助大家的理解。因为我个人认为VB.NET的事件语法,能让你非常直观的领悟到.NET事件模型的“思维方式”:

复制 保存
Public Class Worker
    Private Const MAX = 10000

    Public Sub New()
    End Sub

    ' 注:此例的写法不符合 .NET Framework 类库设计指南中的约定,
    ' 只是为了让你快速理解事件模型而简化的。
    ' 请继续阅读,使用 Demo 1F 的 VB.NET 标准写法。
    ' 

    ' 工作开始事件,并同时通知外界需要完成的数量。
    Public Event StartWork(ByVal totalUnits As Integer)

    ' 进度汇报事件,通知外界任务完成的进度情况。
    Public Event RateReport(ByVal rate As Double)

    ' 工作结束事件。
    Public Event EndWork()

    Public Sub DoLongTimeTask()
        Dim i As Integer
        Dim t As Boolean = False
        Dim rate As Double

        ' 开始工作前,向外界发出事件通知
        RaiseEvent StartWork(MAX)

        For i = 0 To MAX
            Thread.Sleep(1)
            t = Not t

            rate = i / MAX
            RaiseEvent RateReport(rate)
        Next

        RaiseEvent EndWork()

    End Sub
End Class


首先是事件的声明部分:你只需写上PublicEvent关键字,然后写事件的名称,后面的参数部分写上需要发送到外界的参数声明。

然后请注意已标记为蓝色的RaiseEvent关键字,VB.NET使用此关键字在类内部引发事件,也就是向外界发送事件通知。请注意它的语法,RaiseEvent后接上你要引发的事件名称,然后是具体的事件参数值。

从这个例子中,我们可以加深对事件模型的认识:事件是对象(类)的成员,在对象(类)内部状态发生了一些变化(比如此例中rate在变化),或者对象做一些动作时(比如此例中,方法开始时,向外界raiseevent;方法结束时,向外界raiseevent),对象(类)发出的通知。并且,你也了解了事件参数的用法:事件参数是事件通知的相关内容,比如RateReport事件通知需要报告进度值rate,StartWork事件通知需要报告总任务数MAX。

我想RaiseEvent很形象的说明了这些道理。

委托(delegate)简介。

在学习C#实现之前,我们首先应该了解一些关于“委托”的基础概念。

你可以简单的把“委托(delegate)”理解为.NET对函数的包装(这是委托的主要用途)。委托代表一“类”函数,它们都符合一定的规格,如:拥有相同的参数个数、参数类型、返回值类型等。也可以认为委托是对函数的抽象,是函数的“类”(类是具有某些相同特征的事物的抽象)。这时,委托的实例将代表一个具体的函数。

你可以用如下的方式声明委托:

复制 保存
public delegate void MyDelegate(int integerParameter);


如上的委托将可以用于代表:有且只有一个整数型参数、且不带返回值的一组函数。它的写法和一个函数的写法类似,只是多了delegate关键字、而没有函数体。(注:本文中的函数(function),取了面向过程理论中惯用的术语。在完全面向对象的.NET/C#中,我用以指代类的实例方法或静态方法(method),希望不会因此引起误解。顺带地,既然完全面向对象,其实委托本身也是一种对象。)

委托的实例化:

既然委托是函数的“类”,那么使用委托之前也需要实例化。我们先看如下的代码:

复制 保存
public class Sample
{
    public void DoSomething(int mode)
    {
        Console.WriteLine("test function.");
    }

    public static void Hello(int world)
    {
        Console.WriteLine("hello,world!");
    }
}


我们看到Sample的实例方法DoSomething和静态方法Hello都符合上面已经定义了的MyDelegate委托的“规格”。那么我们可以使用MyDelegate委托来包装它们,以用于特殊的用途(比如下面要讲的事件模型,或者将来教程中要讲的多线程模型)。当然,包装的过程其实也是委托的实例化过程:

复制 保存
Sample sp = new Sample();
MyDelegate del = new MyDelegate(sp.DoSomething);


这是对上面的实例方法的包装。但如果这段代码写在Sample类内部,则应使用this.DoSomething而不用新建一个Sample实例。对Sample的Hello静态方法可以包装如下:

复制 保存
MyDelegate del = new MyDelegate(Sample.Hello);



调用委托:

对于某个委托的实例(其实是一个具体的函数),如果想执行它:

复制 保存
del(12345);


直接写上委托实例的名字,并在括号中给相应的参数赋值即可。(如果函数有返回值,也可以像普通函数那样接收返回值)。

C#实现

Demo1D,C#实现。这里给出Demo1C中VB.NET代码的C#实现:是不是比VB.NET的代码复杂了一些呢?

复制 保存
using System;
using System.Threading;

namespace percyboy.EventModelDemo.Demo1D
{
    // 需要做很长时间才能完成任务的 Worker,这次我们使用事件向外界通知进度。
    public class Worker
    {
        private const int MAX = 10000;

        //  注:此例的写法不符合 .NET Framework 类库设计指南中的约定,
        // 只是为了让你快速理解事件模型而简化的。
        // 请继续阅读,使用 Demo 1E / Demo 1H 的 C# 标准写法。
        // 

        public delegate void StartWorkEventHandler(int totalUnits);
        public delegate void EndWorkEventHandler();
        public delegate void RateReportEventHandler(double rate);

        public event StartWorkEventHandler StartWork;
        public event EndWorkEventHandler EndWork;
        public event RateReportEventHandler RateReport;

        public Worker()
        {
        }

        public void DoLongTimeTask()
        {
            int i;
            bool t = false;
            double rate;

            if (StartWork != null)
            {
                StartWork(MAX);
            }

            for (i = 0; i <= MAX; i++)
            {
                Thread.Sleep(1);
                t = !t;
                rate = (double) i / (double) MAX;

                if (RateReport != null)
                {
                    RateReport(rate);
                }
            }

            if (EndWork != null)
            {
                EndWork();
            }
        }
    }
}


这份代码和上面VB.NET代码实现一致的功能。通过C#代码,我们可以看到被VB.NET隐藏了的一些实现细节:

首先,这里一开始声明了几个委托(delegate)。然后声明了三个事件,这里请注意C#事件声明的方法:

复制 保存
public event [委托类型] [事件名称];


这里你可以看到VB.NET隐藏了声明委托的步骤。

另外提醒你注意代码中具体引发事件的部分:

复制 保存
if (RateReport != null)
{
    RateReport(rate);
}


在调用委托之前,必须检查委托是否为null,否则将有可能引发NullReferenceException意外;比较VB.NET的代码,VB.NET的RaiseEvent语句实际上也隐藏了这一细节。

好了,到此为止,Worker类部分通过事件模型向外界发送事件通知的功能已经有了第一个版本,修改你的Windows窗体,给它添加RateReport事件处理程序(请参看你已下载的源代码),并挂接到一起,看看现在的效果:



添加了进度指示之后的界面,极大的改善了用户体验,对用户更为友好。


向“.NETFramework类库设计指南”靠拢,标准实现

Demo1E,C#的标准实现。

上文已经反复强调了Demo1C,Demo1D代码不符合CLS约定。微软为.NET类库的设计与命名提出了一些指南,作为一种约定,.NET开发者应当遵守这些约定。涉及事件的部分,请参看事件命名指南(对应的在线网页),事件使用指南(对应的在线网页)。
Demo1E,C#的标准实现。上文已经反复强调了Demo1C,Demo1D代码不符合CLS约定。微软为.NET类库的设计与命名提出了一些指南,作为一种约定,.NET开发者应当遵守这些约定。涉及事件的部分,请参看

事件命名指南
http://msdn.microsoft.com/library/chs/default.asp?url=/library/chs/cpgenref/html/cpconeventnamingguidelines.asp

事件使用指南
http://msdn.microsoft.com/library/chs/default.asp?url=/library/chs/cpgenref/html/cpconeventusageguidelines.asp

复制 保存
using System;
using System.Threading;

namespace percyboy.EventModelDemo.Demo1E
{
    public class Worker
    {
        private const int MAX = 10000;

        public class StartWorkEventArgs : EventArgs
        {
            private int totalUnits;

            public int TotalUnits
            {
                get { return totalUnits; }
            }

            public StartWorkEventArgs(int totalUnits)
            {
                this.totalUnits = totalUnits;
            }
        }

        public class RateReportEventArgs : EventArgs
        {
            private double rate;

            public double Rate
            {
                get { return rate; }
            }

            public RateReportEventArgs(double rate)
            {
                this.rate = rate;
            }
        }

        public delegate void StartWorkEventHandler(object sender,StartWorkEventArgs e);
        public delegate void RateReportEventHandler(object sender,RateReportEventArgs e);

        public event StartWorkEventHandler StartWork;
        public event EventHandler EndWork;
        public event RateReportEventHandler RateReport;

        protected virtual void OnStartWork(StartWorkEventArgs e)
        {
            if (StartWork != null)
            {
                StartWork(this,e);
            }
        }

        protected virtual void OnEndWork(EventArgs e)
        {
            if (EndWork != null)
            {
                EndWork(this,e);
            }
        }

        protected virtual void OnRateReport(RateReportEventArgs e)
        {
            if (RateReport != null)
            {
                RateReport(this,e);
            }
        }

        public Worker()
        {
        }

        public void DoLongTimeTask()
        {
            int i;
            bool t = false;
            double rate;

            OnStartWork(new StartWorkEventArgs(MAX));

            for (i = 0; i <= MAX; i++)
            {
                Thread.Sleep(1);
                t = !t;
                rate = (double) i / (double) MAX;

                OnRateReport(new RateReportEventArgs(rate));
            }

            OnEndWork(EventArgs.Empty);
        }
    }
}


按照.NETFramework类库设计指南中的约定:

(1)事件委托名称应以EventHandler为结尾;

(2)事件委托的“规格”应该是两个参数:第一个参数是object类型的sender,代表发出事件通知的对象(代码中一般是this关键字(VB.NET中是Me))。第二个参数e,应该是EventArgs类型或者从EventArgs继承而来的类型;

事件参数类型,应从EventArgs继承,名称应以EventArgs结尾。应该将所有想通过事件、传达到外界的信息,放在事件参数e中。

(3)一般的,只要类不是密封(C#中的sealed,VB.NET中的NotInheritable)的,或者说此类可被继承,应该为每个事件提供一个protected并且是可重写(C#用virtual,VB.NET用Overridable)的OnXxxx方法:该方法名称,应该是On加上事件的名称;只有一个事件参数e;一般在该方法中进行null判断,并且把this/Me作为sender执行事件委托;在需要发出事件通知的地方,应调用此OnXxxx方法。

对于此类的子类,如果要改变发生此事件时的行为,应重写OnXxxx方法;并且在重写时,一般情况下应调用基类的此方法(C#里的base.OnXxxx,VB.NET用MyBase.OnXxxx)。

我建议你能继续花些时间研究一下这份代码的写法,它是C#的标准事件实现代码,相信你会用得着它!

在Demo1D中我没有讲解如何将事件处理程序挂接到Worker实例的事件的代码,在这个Demo中,我将主要的部分列在这里:

复制 保存
private void button1_Click(object sender,System.EventArgs e)
{
    statusBar1.Text = "开始工作 ....";
    this.Cursor = Cursors.WaitCursor;

    long tick = DateTime.Now.Ticks;

    Worker worker = new Worker();

    // 将事件处理程序与 Worker 的相应事件挂钩
    // 这里我只挂钩了 RateReport 事件做示意
    worker.RateReport += new Worker.RateReportEventHandler(this.worker_RateReport);

    worker.DoLongTimeTask();

    tick = DateTime.Now.Ticks - tick;
    TimeSpan ts = new TimeSpan(tick);

    this.Cursor = Cursors.Default;
    statusBar1.Text = String.Format("任务完成,耗时 {0} 秒。",ts.TotalSeconds);
}

private void worker_RateReport(object sender,Worker.RateReportEventArgs e)
{
    this.statusBar1.Text = String.Format("已完成 {0:P0} ....",e.Rate);
}



请注意C#的挂接方式(“+=”运算符)。 到这里为此,你已经看到了事件机制的好处:Worker类的代码和这个WindowsForm没有依赖关系。Worker类可以单独存在,可以被重复应用到不同的地方。

(编辑:李大同)

【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容!

    推荐文章
      热点阅读