发布者/订阅者模式(publish/subscriber pattern): 很多程序都有一个共同的需求,即当一个特定的程序事件发生时,程序的其他部分可以得到该事件已经发生的通知。
发布者:
订阅者:
事件:
事件是一种特殊的多播委托。(术语定义来源:Microsoft 开发文档:事件)
事件是类或结构的成员。
事件处理程序:
事件包含了一个私有的委托。
有关事件的私有委托:
图15-3演示:
源代码组件:
public event EventHandler CountedADpzen;
//声明多个事件
public event EventHandler MyEvent1,MyEvent2,OtherEvent;
//静态事件
public static event EventHandler CountedADozen;
事件是类或结构的成员。
由于事件是成员:
事件成员被隐式自动初始化为 null;
订阅者向事件添加事件处理程序。
使用 += 运算符来为事件添加事件处理程序。
事件处理程序的规范可以是以下任意一种:
```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的声明:
public delegate void EventHandler(object sender,EventArgs e);
EventArgs 参数的作用:
为了能够通过事件参数的 EventArgs 来传递数据,我们需要声明一个派生自 EventArgs 的自定义类。
public class IncrementerEventArgs : EventArgs
{
public int IterationCount{get;set;}
}
实现代码:
//自定义EventArgs
public class IncrementerEventArgs : EventArgs
{
public int IterationCount { get; set; }
}
//发送者
public class Incrementer
{
public event EventHandler 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;
如果一个处理程序向事件注册了多次,那么当执行命令移除处理程序时,将只移除列表中该处理程序的最后一个实例。(如果一个处理程序多次重复了注册事件,移除时,只移除最后一个相同的处理程序实例,而其他相同的事件处理程序仍然可被回调。)
一般情况下,事件只能许 += 和 -= 运算符。但是我们可以修改这两个运算符的行为,在使用它们时让事件执行任何我们希望执行的自定义代码。
事件访问器: 为了改变这两个运算符的操作而定义。
public event EventHandler CountedADozen
{
add{...} //执行 +=
remove{...} //执行 -=
}
声明了事件访问器之后,事件不包含任何内嵌委托对象。我们必须实现自己的机制来存储和移除事件注册的方法。(就是说在事件访问器里来编写“存储和移除事件注册的方法”的其他代码逻辑)
事件访问器表现为 void 方法,也就是不能使用返回值的 return 语句。(此处跟属性有不同的是,属性 get 是有对应类型的返回值的。而事件访问器 get 和 set 都有 value。)
书上没有提供事件访问器的代码例子,但我们也不能只学理论而不亲自动手去实现这个代码逻辑吧。
所以还是要动手实现一下事件访问器的代码例子(模仿按钮触发事件的例子):
根据已学知的识点,以下有使用到:
1、自定义鼠标行为状态和鼠标 EventArgs 事件参数:
//枚举鼠标的行为状态
public enum MouseState
{
LeftDown,
LeftUp,
RightDown,
RightUp,
}
//自定义按钮含有鼠标状态的EventArgs
public class BtnEventArgs : EventArgs
{
public MouseState BtnClickMouseState { get; private set; }
public BtnEventArgs(MouseState mouseState)
{
BtnClickMouseState = mouseState;
}
}
2、发布者类:
//发布者类
class ButtonPublisher
{
private event EventHandler _tnEvent;
public event EventHandler BtnEvent
{
//若add 和 remove 访问器内不写任何代码,则添加移除事件注册无效。
add
{
//加锁:避免在该事件实例正处理其他事情时,
//同时执行该段代码,可能会产生某些问题
lock (this)
{
//在事件的注册列表里是否存在已注册的方法
bool isHavedEvent = false;
if (_tnEvent != null)
{
//遍历事件的注册列表
foreach (var en in _tnEvent.GetInvocationList())
{
var btnEventHandler = en as EventHandler;
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(object sender, TEventArgs e);
所以如果自定义一个委托,这时仅仅是一个委托,而不是事件,但也可以输出同样的结果。
//自定义一个委托
public delegate void CustomEventHandler(Object obj, BtnEventArgs e);
//把以上代码例子中的所有EventHandler替换为:
CustomEventHandler
//执行的功能和输出的结果一样。