一、发布者和订阅者
发布者/订阅者模式(publish/subscriber pattern): 很多程序都有一个共同的需求,即当一个特定的程序事件发生时,程序的其他部分可以得到该事件已经发生的通知。
发布者:
- 发布者类定义了一系列程序的其他部分可能感兴趣的事件。
- 发布某个事件的类或结构,其他类可以在该事件发生时得到通知。
订阅者:
- 订阅者类可以“注册”,以便在这些事件发生时收到发布者的通知。这些订阅者类通过向发布者提供一个方法来“注册”以获取通知。
- 注册并在事件发生时得到通知的类或结构。
事件:
- 当事件发生时,发布者“触发事件”,然后执行订阅者提交的所有事件。
- 调用(invoke)或触发(fire)事件的术语。当事件被触发时,所有注册到它的方法都会被依次调用。
事件是一种特殊的多播委托。(术语定义来源:Microsoft 开发文档:事件)
事件是类或结构的成员。
事件处理程序:
- 回调方法。由订阅者提供的方法称为回调方法,因为发布者通过执行这些方法来“往回调用订阅者的方法”。它们是为处理事件而调用的代码。
- 由订阅者注册到事件的行为,在发布者出发事件时执行。
事件包含了一个私有的委托。
有关事件的私有委托:
- 事件提供了对它的私有控制委托的结构化访问。也就是说,你无法直接访问委托。
- 事件中可用的操作比委托要少,对于事件我们只可以添加、删除或者调用事件处理程序。
- 事件被触发时,它调用委托来依次调用调用列表中的方法。
图15-3演示:
- Incrementer 定义了一个 CountedADozen 事件。
- 订阅者类 Dozens 和 SomeItherClass 各有一个注册到 CountedADozen 事件的事件处理程序。
- 每当触发事件时,都会调用这些处理程序。
二、源代码组件概览
源代码组件:
- 委托类型声明: 事件和事件处理程序必须有共同的签名和返回类型,它们通过委托类型进行描述。
- 事件处理程序声明: 订阅者类中会在事件触发时执行的方法声明。它们不一定是显式命名的方法,还可以是匿名方法或 Lambda 表达式。
- 事件声明: 发布者类必须声明一个订阅者可以注册的事件成员。当类声明的事件为 public 时,称为发布了事件。
- 事件注册: 订阅者必须注册事件才能在事件被触发时得到通知。这是将事件处理程序与事件相连的代码。
- 触发事件的代码: 发布者类中的”触发“事件并导致调用注册的所有事件处理器的代码。
三、声明事件
public event EventHandler CountedADpzen;//声明多个事件
public event EventHandler MyEvent1,MyEvent2,OtherEvent;//静态事件
public static event EventHandler CountedADozen;
事件是类或结构的成员。
由于事件是成员:
- 我们不能在一段可执行代码中声明事件;
- 它必须声明在类或结构中,和其他成员一样。
事件成员被隐式自动初始化为 null;
四、订阅事件
订阅者向事件添加事件处理程序。
-
使用 += 运算符来为事件添加事件处理程序。
-
事件处理程序的规范可以是以下任意一种:
- 实例方法的名称;
- 静态方法的名称;
- 匿名方法;
- Lambda 表达式。
```c#
class Incrementer
{
public event EventHandler CountedADpzen;
}class ClassB
{
public static CounterHnadlerb(){}
}class ClassC
{
public static CounterHnadlerC(){}
}class Pargam
{
static void Main()
{
Incrementer incrementer = new Incrementer();
//添加实例方法
incrementer.CountedADozen += IncrementDzensCount;
//添加静态方法
incrementer.CountedADozen += ClassB.CounterHnadlerb;ClassC cc = new ClassC();
//以委托形式添加实例方法
incrementer.CountedADozen += new EventHandler(cc.CounterHandlerC);//Lambda 表达式
int DozensCount = 0;
incrementer.CountedADozen += ()=> DozensCount++;
//匿名方法
incrementer.CountedADozen += delegate { DozensCount++; };
}}
五、触发事件
if(CountedADozen != null)
{
CountedADozen(source,args)
}//CountedADozen:事件名称
//source,args:参数列表
整个程序的代码:
//1、声明委托delegate void Handler();//发布者class Incrementer{//2、创建事件并发布public event Handler CountedADozen;public void DoCount(){for(int i = 1; i < 100; i++){if(i %12 ==0 && CountedADozen != null){//3、每增加12个计数触发事件一次CountedADozen();}}}}//订阅者class Dozens{public int DozensCount { get; private set; }public Dozens(Incrementer incrementer){DozensCount = 0;//5、订阅事件incrementer.CountedADozen += IncrementDozensCount;}//4、声明事件处理程序void IncrementDozensCount(){DozensCount++;}}class Program{static void Main(string[] args){Incrementer incrementer = new Incrementer();Dozens dozensCounter = new Dozens(incrementer);incrementer.DoCount();Console.WriteLine("Number of dozens = {0}", dozensCounter.DozensCount);Console.ReadKey();}}
输出结果:
Number of dozens = 8
六、标准事件的用法
在程序需要处理事件然后继续作其他事情时,就要对程序事件进行异步处理。Windows GUI 编程广泛使用例如事件。
对事件的使用,.NET 框架提供了一个标准模式,Ssytem命名空间中声明的 EventHandler 委托类型。
EventHandler的声明:
- 第一个参数,用来保存触发事件的对象的引用。
- 第二参数用来保存状态信息,指明什么类型适用于该应用程序。
- 返回类型是 void。
public delegate void EventHandler(object sender,EventArgs e);
EventArgs 参数的作用:
- EventArgs 不能传递任何数据。它用于不需要传递数据的事件处理程序——通常会被忽略。(比如EventArgs 可以传递状态:左键鼠标事件的是释放状态还是按下状态)
- 如果你希望传递数据,必须声明一个派生自 EventArgs 的类,并使用合适的字段来保存需要传递的数据。
七、通过扩展 EventArgs 来传递数据
为了能够通过事件参数的 EventArgs 来传递数据,我们需要声明一个派生自 EventArgs 的自定义类。
public class IncrementerEventArgs : EventArgs
{
public int IterationCount{get;set;}
}
实现代码:
//自定义EventArgspublic class IncrementerEventArgs : EventArgs{public int IterationCount { get; set; }}//发送者public class Incrementer{public event EventHandler<IncrementerEventArgs> CountedDozen;public void DoCount(){IncrementerEventArgs args = new IncrementerEventArgs();for(int i = 1; i < 100;i++){if(i % 12 == 0 && CountedDozen != null){args.IterationCount = i;CountedDozen(this, args);}}}}//订阅者class Dozens{public int DozensCount { get; private set; }public Dozens(Incrementer incrementer){DozensCount = 0;incrementer.CountedDozen += IncrementDozensCount;}void IncrementDozensCount(object source, IncrementerEventArgs e){Console.WriteLine($"Incremented at iteration:{ e.IterationCount } in { source.ToString() }");DozensCount++;}}class Program{static void Main(string[] args){Incrementer incrementer = new Incrementer();Dozens dozensCounter = new Dozens(incrementer);incrementer.DoCount();Console.WriteLine($"Number of dozens = { dozensCounter.DozensCount }");Console.ReadKey();}}
输出结果:
Incremented at iteration:12 in ConsoleApplication2.Incrementer
Incremented at iteration:24 in ConsoleApplication2.Incrementer
Incremented at iteration:36 in ConsoleApplication2.Incrementer
Incremented at iteration:48 in ConsoleApplication2.Incrementer
Incremented at iteration:60 in ConsoleApplication2.Incrementer
Incremented at iteration:72 in ConsoleApplication2.Incrementer
Incremented at iteration:84 in ConsoleApplication2.Incrementer
Incremented at iteration:96 in ConsoleApplication2.Incrementer
Number of dozens = 8
八、移除事件处理程序
incrementer.CountedDozen -= IncrementDozensCount;
如果一个处理程序向事件注册了多次,那么当执行命令移除处理程序时,将只移除列表中该处理程序的最后一个实例。(如果一个处理程序多次重复了注册事件,移除时,只移除最后一个相同的处理程序实例,而其他相同的事件处理程序仍然可被回调。)
九、事件访问器
一般情况下,事件只能许 += 和 -= 运算符。但是我们可以修改这两个运算符的行为,在使用它们时让事件执行任何我们希望执行的自定义代码。
事件访问器: 为了改变这两个运算符的操作而定义。
- 有两个访问器:add 和 remove。
- 声明事件的访问器看上去和声明一个属性差不多。
public event EventHandler CountedADozen
{
add{...} //执行 +=
remove{...} //执行 -=
}
-
声明了事件访问器之后,事件不包含任何内嵌委托对象。我们必须实现自己的机制来存储和移除事件注册的方法。(就是说在事件访问器里来编写“存储和移除事件注册的方法”的其他代码逻辑)
-
事件访问器表现为 void 方法,也就是不能使用返回值的 return 语句。(此处跟属性有不同的是,属性 get 是有对应类型的返回值的。而事件访问器 get 和 set 都有 value。)
书上没有提供事件访问器的代码例子,但我们也不能只学理论而不亲自动手去实现这个代码逻辑吧。
所以还是要动手实现一下事件访问器的代码例子(模仿按钮触发事件的例子):
根据已学知的识点,以下有使用到:
- 扩展 EventArgs:用来描述点击按钮后鼠标的行为状态,比如鼠标被按下或被释放的状态。
- 测试代码例子之后,关于委托与事件之间的关系。
1、自定义鼠标行为状态和鼠标 EventArgs 事件参数:
//枚举鼠标的行为状态public enum MouseState{LeftDown,LeftUp,RightDown,RightUp,}//自定义按钮含有鼠标状态的EventArgspublic class BtnEventArgs : EventArgs{public MouseState BtnClickMouseState { get; private set; }public BtnEventArgs(MouseState mouseState){BtnClickMouseState = mouseState;}}
2、发布者类:
//发布者类class ButtonPublisher{private event EventHandler<BtnEventArgs> _tnEvent;public event EventHandler<BtnEventArgs> BtnEvent{//若add 和 remove 访问器内不写任何代码,则添加移除事件注册无效。add{//加锁:避免在该事件实例正处理其他事情时,//同时执行该段代码,可能会产生某些问题lock (this){//在事件的注册列表里是否存在已注册的方法bool isHavedEvent = false;if (_tnEvent != null){//遍历事件的注册列表foreach (var en in _tnEvent.GetInvocationList()){var btnEventHandler = en as EventHandler<BtnEventArgs>;if (btnEventHandler == null || btnEventHandler.Method == null)continue;var method = btnEventHandler.Method;//对比事件处理程序是否相同if (method == value.Method){isHavedEvent = true;break;}}}//若还没有注册,就注册;否则,不执行重复注册if (isHavedEvent == false){_tnEvent += value;}}}remove{lock (this){_tnEvent -= value;//移除事件注册}}}public void RaiseBtnEvent(MouseState mouseState){BtnEventArgs args = new BtnEventArgs(mouseState);if(_tnEvent != null)_tnEvent(this, args);}}
3、订阅者类
//订阅者类class Subscriber{public void MethodMouse(object o, BtnEventArgs e){string str = Enum.GetName(typeof(MouseState), e.BtnClickMouseState);Console.WriteLine("{0}", str);}}
4、测试代码:
class Program{static void Main(string[] args){ButtonPublisher p = new ButtonPublisher();Subscriber s = new Subscriber();//注册了一次p.BtnEvent += s.MethodMouse;//由于add 访问器里有做了判断:相同的事件处理程序不能再被关联一遍p.BtnEvent += s.MethodMouse;Console.WriteLine("注册BtnEvent事件后,准备触发该事件:");//触发事件p.RaiseBtnEvent(MouseState.LeftUp);p.RaiseBtnEvent(MouseState.LeftDown);p.BtnEvent -= s.MethodMouse;Console.WriteLine("移除事件注册后,没法触发该事件");//因移除了该事件的注册,无法触发该事件p.RaiseBtnEvent(MouseState.LeftUp);p.RaiseBtnEvent(MouseState.LeftDown);Console.ReadKey();}}
输出结果:
注册事件后,触发事件:
LeftUp
LeftDown
移除事件注册后,不触发事件
加强理解委托和事件之间的关系
记住以下几点:
事件是类或结构提供具有通知能力的成员。
事件是一种特殊的多播委托。
事件包含了一个私有的委托。
事件成员被隐式自动初始化为 null;
委托是一个类,它封装了一个调用列表。使用到事件是因为要把委托包装起来,为了避免委托的滥用,使得保障使用委托的安全性。同时,事件还起到因隐藏对委托字段的访问限制作用,仅仅提供添加和移除事件处理程序的功能。于是事件作为发送者的成员,发送者调用它就不会对委托里所有功能都能操作,因为委托在事件里是私有的。
关于扩展 EventArgs,实际上是根据事件里私有委托已经设计好的泛型来实现的:
//在源代码中,有一个泛型委托,TEventArgs 是一个泛型参数public delegate void EventHandler<TEventArgs>(object sender, TEventArgs e);
所以如果自定义一个委托,这时仅仅是一个委托,而不是事件,但也可以输出同样的结果。
//自定义一个委托
public delegate void CustomEventHandler(Object obj, BtnEventArgs e);//把以上代码例子中的所有EventHandler<BtnEventArgs>替换为:
CustomEventHandler
//执行的功能和输出的结果一样。