事件

发布者和订阅者


很多程序都有一个共同的需求,既当一个特定的程序事件发生时,程序的其他部分可以得到该事件已经发生的通知。
发布者/订阅者模式(publisher/subscriber pattern)可以满足这种需求。


  • 发布者(publisher) 发布某个事件的类或结构,其他类可以在该事件发生时得到通知
  • 订阅者(subscriber) 注册并在事件发生时得到通知的类或结构
  • 事件处理程序(event handler) 由订阅者注册到事件的方法,在发布者触发事件时执行。事件处理程序可定义在事件所在的类或结构中,也可定义在不同的类或结构中
  • 触发(raise)事件 调用(invoke)或触发(fire)事件的术语。当事件触发时,所有注册到它的方法都会被依次调用

前一章介绍了委托。事件的很多部分都与委托类似。实际上,事件就像专门用于特殊用途的简单委托。事件包含了一个私有的委托。

有关事件的私有委托需要了解的重要事项如下:

  • 事件提供了对它的私有控制委托的结构化访问。即,你无法直接访问委托
  • 事件中可用的操作比委托少,对于事件我们只可添加、删除或调用事件处理程序
  • 事件被触发时,它调用委托来依次调用调用列表中的方法

源代码组件概览


需要在事件中使用的代码有5部分。

  • 委托类型声明 事件和事件处理程序必须有共同的签名和返回类型,它们通过委托类型进行描述
  • 事件处理程序声明 订阅者类中会在事件触发时执行的方法声明。它们不一定是有显式命名的方法,还可以是第13章描述的匿名方法或Lambda表达式
  • 事件声明 发布者类必须声明一个订阅者类可以注册的事件成员。当声明的事件为public时,称为发布了事件
  • 事件注册 订阅者必须订阅事件才能在它被触发时得到通知
  • 触发事件的代码 发布者类中“触发”事件并导致调用注册的所有事件处理程序的代码

 

声明事件


发布者类必须提供事件对象。创建事件比较简单–只需要委托类型和名字。事件声明的语法如下代码所示。
代码中声明了CountADozen事件。

  • 事件声明在一个类中
  • 它需要委托类型的名称,任何附加到事件(如注册)的处理程序都必须与委托类型的签名和返回类型匹配
  • 它声明为public,这样其他类和结构可以在它上面注册事件处理程序
  • 不能使用对象创建表达式(new 表达式)来创建对象
class Incrementer
{
关键字 委托类型 事件名
↓ ↓ ↓
public event EventHandler CountedADozen;
...
}

可以通过使用逗号分隔同时声明多个事件。

public event EventHandler MyEvent1,MyEvent2,OtherEvent;

还可以使用static关键字让事件变成静态

public static event EventHandler CountedADozen;

事件是成员
一个常见误解是把事件认为是类型。和方法、属性一样,事件是类或结构的成员,这一点引出几个重要特性。

  • 由于事件是成员:

    • 我们不能在一段可执行代码中声明事件
    • 它必须声明在类或结构中
  • 事件成员被隐式自动初始化为null

事件声明需要委托类型的名字,我们可以声明一个委托类型或使用已存在的。如果我们声明一个委托类型,它必须指定事件保存的方法的签名和返回类型。
BCL(Base Class Library,基类库)声明了一个叫做EventHandler的委托,专门用于系统事件。

订阅事件


  • 使用+=运算符来为事件增加事件处理程序
  • 事件处理程序的规范可以是以下任意一种
    • 实例方法的名称
    • 静态方法的名称
    • 匿名方法
    • Lambda表达式

例:为CountedADozen事件增加3个方法。

incrementer.CountedADozen+=IncrementDozensCount;//实例方法
incrementer.CountedADozen+=ClassB.CounterHandlerB;//静态方法
mc.CountedADozen+=new EventHandler(cc.CounterHandlerC);//委托形式

例:Lambda表达式和匿名方法

incrementer.CountedADozen+=()=>DozensCount++;//Lambda表达式
incrementer.CountedADozen+=delegate{DozensCount++;};//匿名方法

触发事件


事件成员本身只保存了需要被调用的事件处理程序。如果事件没触发,什么都不会发生。我们需要确保在合适的时候有代码来做这件事情。
例:下面代码触发了CountedADozen事件。

  • 在触发事件前和null比较,从而查看是否包含事件处理程序,如果事件是null,则表示没有,不能执行
  • 触发事件的语法和调用方法一样
    • 使用事件名称,后面跟参数列表
    • 参数列表需与事件委托类型相匹配
if(CountedADozen!=null)
{
CountedADozen(source,args);
}

把事件声明和触发事件的代码放在一起便有了如下的发布者类声明。
下面展示了整个程序,代码需要注意的地方如下:

  • 在构造函数中,Dozens类订阅事件,将IncrementDozensCount作为事件处理程序
  • 在Incrementer类的DoCount方法中,每增长12个数就触发CountedADozen事件
delegate void Handler();    //声明委托
//发布者
class Incrementer
{
public event Handler CountedADozen;//创建事件并发布
void DoCount(object source,EventArgs args)
{
for(int i=;i<;i++)
{
if((i%==)&&(CountedADozen!=null))
{
CountedADozen(source,args);
}
}
}
}
//订阅者
class Dozens
{
public int DozensCount{get;private set;}
public Dozens(Incrementer incrementer)
{
DozensCount=;
incrementer.CountedADozen+=IncrementDozensCount;//订阅事件
}
void IncrementDozensCount()//声明事件处理程序
{
DozensCount++;
}
}
class Program
{
static void Main()
{
var incrementer=new Incrementer();
var dozensCounter=new Dozens(incrementer);
incrementer.DoCount();
Console.WriteLine("Number of dozens = {0}",dozensCounter.DozensCount);
}
}

标准事件的用法


GUI编程是事件驱动的,即程序运行时,它可以在任何时候被事件打断,比如鼠标点击,按下按键或系统定时器。在这些情况发生时,程序需要处理事件然后继续其他事情。
程序事件的异步处理是使用C#事件的绝佳场景。Windows GUI编程广泛的使用事件,对于事件的使用,.NET框架提供了一个标准模式。事件使用的标准模式根本就是System命名空间声明的EventHandler委托类型。EventHandler委托类型的声明代码如下。

  • 第一个参数用来保存触发事件的对象的引用。由于是object类型,所以可以匹配任何类型的实例
  • 第二个参数用来保存状态信息,指明什么类型适用于该程序
  • 返回类型是void
public delegate void EventHandler(object sender,EventArgs e);

EventHandler委托类型的第二个参数是EventArgs类的UI项,它声明在System命名空间中。既然第二个参数用于传递数据,你可能会误认为EventArgs类的对象应该可以保存一些类型的数据。

  • EventArgs设计为不能传递任何数据。它用于不需要传递数据的事件处理程序–通常会被忽略
  • 如果你希望传递数据,必须声明一个派生自EventArgs的类,使用合适的字段来保存需要传递的数据

例:Incrementer+EventHandler

  • 在声明中使用系统定义的EventHandler委托替换Handler
  • 订阅者中声明的事件处理程序签名必须与事件委托(object、EventArgs参数)的签名(和返回类型)匹配。对于IncrementDozensCount事件处理程序来说,该方法忽略了正式参数
  • 触发事件的代码在调用事件时必须使用适当的参数类型的对象
public delegate void EventHandler(object sender,EventArgs e);
//发布者
class Incrementer
{
public event EventHandler CountedADozen;//创建事件并发布
public void DoCount()
{
for(int i=;i<;i++)
{
if((i%==)&&(CountedADozen!=null))
{
CountedADozen(this,null);
}
}
}
}
//订阅者
class Dozens
{
public int DozensCount{get;private set;}
public Dozens(Incrementer incrementer)
{
DozensCount=;
incrementer.CountedADozen+=IncrementDozensCount;//订阅事件
}
void IncrementDozensCount(object source,EventArgs e)//声明事件处理程序
{
DozensCount++;
}
}
class Program
{
static void Main()
{
var incrementer=new Incrementer();
var dozensCounter=new Dozens(incrementer);
incrementer.DoCount();
Console.WriteLine("Number of dozens = {0}",dozensCounter.DozensCount);
}
}
通过扩展EventArgs来传递数据

为了向EventArgs传入数据,并且符合标准惯例,我们需要声明一个派生自EventArgs的自定义类,用于保存需要传入的数据。类的名称应该以EventArgs结尾。
例:声明自定义的EventArgs类,它将字符串存储在IterationCount字段中。

public class IncrementerEventArgs:EventArgs
{
public int IterationCount{get;set;}
}

除了自定义类外,你还需要一个使用自定义类的委托类型。要获取该类,可以使用泛型(第17章)版本的委托EventHandler<>。要使用泛型委托,需要做到以下两点:

  • 将自定义类的名称放在尖括号内
  • 在需要使用自定义委托类型的时候使用整个字符串。

例:使用了自定义类和自定义委托的事件示例

public class IncrementerEventArgs:EventArgs
{
public int IterationCount{get;set;}
}
//发布者
class Incrementer
{
使用自定义类的泛型委托

public event EventHandler<IncrementerEventArgs> CountedADozen;//创建事件并发布
public void DoCount()
{
IncrementerEventArgs args=new IncrementerEventArgs();
for(int i=;i<;i++)
{
if((i%==)&&(CountedADozen!=null))
{
args.IterationCount=i;
CountedADozen(this,args);
}
}
}
}
//订阅者
class Dozens
{
public int DozensCount{get;private set;}
public Dozens(Incrementer incrementer)
{
DozensCount=;
incrementer.CountedADozen+=IncrementDozensCount;//订阅事件
}
void IncrementDozensCount(object source,IncrementerEventArgs e)//声明事件处理程序
{
Console.WriteLine("Incremented at iteration: {0} in {1}",e.IterationCount,source.ToString());
DozensCount++;
}
}
class Program
{
static void Main()
{
var incrementer=new Incrementer();
var dozensCounter=new Dozens(incrementer);
incrementer.DoCount();
Console.WriteLine("Number of dozens = {0}",dozensCounter.DozensCount);
}
}

移除事件处理程序

用完事件处理程序后,可以使用-=运算符把事件处理程序从事件中移除。

例:移除事件处理程序示例

class Publisher
{
public event EventHandler SimpleEvent;
public void RaiseTheEvent(){SimpleEvent(this,null);}
}
class Subscriber
{
public void MethodA(object o,EventArgs e)
{
Console.WriteLine("AAA");
}
public void MethodB(object o,EventArgs e)
{
Console.WriteLine("BBB");
}
}
class Program
{
static void Main()
{
var p=new Publisher();
var s=new Subscriber();
p.SimpleEvent+=s.MethodA;
p.SimpleEvent+=s.MethodB;
p.RaiseTheEvent();
Console.WriteLine("\r\nRemove MethodB");
p.SimpleEvent-=s.MethodB;
p.RaiseTheEvent();
}
}

如果一个处理程序向事件注册了多次,那么移除程序时,将只移除列表中该处理程序的最后一个实例。

事件访问器


之前我提到+=和-=运算符是事件允许的唯一运算符。看到这里我们应该知道,这些运算符有预定义行为。
我们可以修改这些运算符的行为,并且使用它们时可以让事件执行任何我们希望的自定义代码。但这是高级主题,此处只做简单介绍。
要改变这两个运算符的操作,可以为事件定义事件访问器。

  • 有两个访问器:add和remove
  • 声明事件的访问器看上去和声明一个属性差不多

例:具有访问器的事件声明。两个访问器都有隐式值参数value,它接受实例或静态方法的引用。

public event EventHandler CountedADozen
{
add
{
... //执行+=运算符的代码
}
remove
{
... //执行-=运算符的代码
}
}

声明事件访问器后,事件不包含任何内嵌委托对象。我们必须实现自己的机制来存储和移除事件注册方法。
事件访问器表现为void方法,即没有返回值。

C#图解教程 第十四章 事件的更多相关文章

  1. C#图解教程 第二十四章 反射和特性

    反射和特性 元数据和反射Type 类获取Type对象什么是特性应用特性预定义的保留的特性 Obsolete(废弃)特性Conditional特性调用者信息特性DebuggerStepThrough 特 ...

  2. python 教程 第十四章、 地址薄作业

    第十四章. 地址薄作业 #A Byte of Python #!/usr/bin/env python import cPickle import os #define the contacts fi ...

  3. Flask 教程 第十四章:Ajax

    本文翻译自The Flask Mega-Tutorial Part XIV: Ajax 这是Flask Mega-Tutorial系列的第十四部分,我将使用Microsoft翻译服务和少许JavaSc ...

  4. 【WPF学习】第十四章 事件路由

    由上一章可知,WPF中的许多控件都是内容控件,而内容控件可包含任何类型以及大量的嵌套内容.例如,可构建包含图形的按钮,创建混合了文本和图片内容的标签,或者为了实现滚动或折叠的显示效果而在特定容器中放置 ...

  5. C#图解教程 第二十五章 其他主题

    其他主题 概述字符串使用 StringBuilder类把字符串解析为数据值关于可空类型的更多内容 为可空类型赋值使用空接合运算符使用可空用户自定义类型 Main 方法文档注释 插入文档注释使用其他XM ...

  6. C#图解教程 第十五章 接口

    接口 什么是接口 使用IComparable接口的示例 声明接口实现接口 简单接口示例 接口是引用类型接口和as运算符实现多个接口实现具有重复成员的接口多个接口的引用派生成员作为实现显式接口成员实现 ...

  7. C#图解教程 第十九章 LINQ

    LINQ 什么是LINQLINQ提供程序 匿名类型 方法语法和查询语法查询变量查询表达式的结构 from子句join子句什么是联结查询主体中的from-let-where片段 from子句let子句w ...

  8. C#图解教程 第十二章 数组

    数组 数组 定义重要细节 数组的类型数组是对象一维数组和矩形数组实例化一维数组或矩形数组访问数组元素初始化数组 显式初始化一维数组显式初始化矩形数组快捷语法隐式类型数组综合内容 交错数组 声明交错数组 ...

  9. C#图解教程 第十六章 转换

    转换 什么是转换隐式转换显式转换和强制转换 强制转换 转换的类型数字的转换 隐式数字转换溢出检测上下文 1.checked和unchecked运算符2.checked语句和unchecked语句 显式 ...

随机推荐

  1. BZOJ 2244: [SDOI2011]拦截导弹 [CDQ分治 树状数组]

    传送门 题意:三维最长不上升子序列以及每个元素出现在最长不上升子序列的概率 $1A$了好开心 首先需要从左右各求一遍,长度就是$F[0][i]+F[1][i]-1$,次数就是$G[0][i]*G[1] ...

  2. 小甲鱼OD学习第1讲

    这一讲我们的目标是修改Hello.exe对话框的标题和内容,如图所示 把程序放进OD,按F8一步步运行,直到程序弹出对话框为止,然后在弹出对话框位置处下断点 按Ctrl+F2重新载入程序,然后按F9直 ...

  3. 微信小程序页面跳转的问题(app.json中设置tarBar后wx.redirectTo和wx.navigateTo均不能实现跳转到指定的页面)

    1.设置的tabBar代码片段: "tabBar": { "list": [ { "pagePath": "pages/homep ...

  4. [Python Study Notes]WdSaveFormat 枚举

    WdSaveFormat 枚举 指定要在保存文档时使用的格式. 版本信息 已添加版本: 名称 值 说明 wdFormatDocument 0 Microsoft Word 格式. wdFormatDO ...

  5. 浅谈PHP答题卡识别(一)

    最近期末考试考完了,我们也要放寒假了.于是突发奇想,想用PHP写一个答题卡识别程序.已经实现了一些,现分享给大家. 具体的步骤如下: 上传答题卡=>图片二值化(已实现)=>寻找定位点(已实 ...

  6. Linux上查看用户名和组并把特定用户放到特定的组之下

    cat /etc/passwd             //查看所有的用户信息 cat /etc/passwd|grep 用户名       //查看某一个用户的信息 cat /etc/group   ...

  7. uva1347 经典dp

    详细的思路书上面有,有一点要强调的是题意容易理解错:必须严格向右或则向左移动,不能到了第3个点又回到第2个点.否则这个状态方程是不成立的,变成了NP难问题 状态方程: dp[i][j]=min(dp[ ...

  8. UVA - 242 线性DP

    题意:给定多种邮票的组合,邮票最多只能用S张,这些邮票能组成许多不同面额,问最大连续面额的长度是多少,如果有多个组合输出组合中邮票数量最少的,如果仍有长度一致的,输出邮票从大到小排序后字典序最大的那个 ...

  9. .net Winfrom 僵尸窗口 的问题

    执行顺序如下: Form1 form1 =new Form1(); form1.ShowDialog(); Form2 form2= Application.OpenForms["Form2 ...

  10. typeahead + JDK 8 并行流 + redis 高速即时查询.

    感谢JDK8,让我们JAVA 程序员暂时不用担心失业. 有些情况,需要根据用户输入值,即时查询数据库,MYSQL显然不再适合这种业务. mongoDB看似最适合,但是为了这么一个破功能,也不值得特意去 ...