举个栗子:“裁判员开枪,运动员开始跑步。”
在上面这个例子中,事件拥有者是裁判员,事件成员是开枪,事件响应者是运动员,事件处理是开始跑步。
至于事件订阅,它不是具体的对象或成员,其作用是将事件和事件处理器关联到一起,其代表了事件拥有者与事件响应者之间的联系,比如裁判员开枪,他所在的比赛现场上的运动员会起跑,但几百里外的其他运动员不会起跑,在现场看比赛的观众也不会起跑,因为他们与裁判员之间没有联系,即没有订阅裁判员的事件。
除此之外,事件订阅还能够约束事件成员能够把什么样的事件参数传递给事件处理器、事件处理器应该拥有什么样的签名和返回值类型(方法的签名由方法名称和参数列表组成,不包括返回值类型)。我们不能随随便便地拿任何事件处理器来处理事件,就像当裁判员开枪时,运动员应该起跑而不是坐下来喝杯茶。
①事件拥有者拥有一个事件→②事件响应者订阅了这个事件→③事件拥有者触发了事件→④事件响应者会被依次通知到(按照订阅的顺序)→⑤事件响应者根据拿到的事件参数对事件进行处理
1、事件拥有者和事件响应者分别属于不同的类:
namespace TestConsole
{
class Program
{
static void Main(string[] args)
{
Form myForm = new Form(); //事件拥有者:myForm
Test myTest = new Test(myForm); //事件响应者:myTest
myForm.ShowDialog(); //显示窗体
}
}
class Test
{
private Form form;
public Test(Form form)
{
if (form != null)
{
this.form = form;
this.form.Click += FormClicked; //鼠标点击事件
//事件:Click;事件处理器:FormClicked;事件订阅:+=
}
}
//事件处理器:(可由编译器自动生成)
private void FormClicked(object sender, EventArgs e)
{
form.Text = DateTime.Now.ToString(); //将当前时间作为窗体的标题
}
}
}
//发生事件:鼠标点击窗体;
//谁拥有这个事件:窗体myForm,鼠标点击的是它;
//谁响应了这个事件:myTest,当窗体被点击时,其要做出处理;
//做出什么处理:为窗体添加当前时间。
2、事件拥有者和事件响应者是同一个对象
namespace TestConsole
{
class Program
{
static void Main(string[] args)
{
MyForm form = new MyForm(); //事件拥有者和事件响应者皆为form
form.Click += form.FormClicked; //事件Click,事件处理器FormClicked,事件订阅+=
form.ShowDialog();
//假如这里写成:
//Form form = new Form();
//form.Click += form.FormClicked;
//“FormClicked”的实现部分无法由编译器自动生成
//因为Form是系统提供的类,我们无法随意更改,
//所以就不能像之前一样用编译器去给事件处理器自动生成方法
//这就需要我们自己写一个类去继承(class MyForm : Form)
}
}
class MyForm : Form
{
//事件处理器
internal void FormClicked(object sender, EventArgs e)
{
this.Text = "Hello World!";
}
}
}
3、事件拥有者是事件响应者的一个字段,或事件响应者是事件拥有者的一个字段
这是WinForm编程中常用的一种方式,比如在一个窗体中,存在一个文本框和按钮,现在要通过点击按钮让文本框上显示出文字“Hello World”。
发生的事件是:鼠标点击;事件拥有者是:按钮(Button),鼠标点击的是它,它是窗体对象的一个字段成员;事件响应者是窗体对象;事件处理是:窗体对象让自己的字段成员文本框(TextBox)显示出文字“Hello World”。
//由可视化编程操作生成文本框和按钮,然后给按钮添加事件处理器:
private void buttonClicked(object sender, EventArgs e)
{
this.textBox1.Text = "Hello World"; //“this.”可以省略
}
只要符合事件订阅的“约定”,不同的事件拥有者可以拥有同一个事件处理器。
比如在WinForm编程中,同样是Button类型,那么多个按钮都可以共用一个事件处理器。
我们先为按钮1添加了“点击”的事件处理器“buttonClicked”,此时再新建一个按钮,在按钮2的事件面板中的“Click”处,下拉列表选择“buttonClicked”,如此一来按钮2的事件处理器也为“buttonClicked”:
private void buttonClicked(object sender, EventArgs e)
{
textBox1.Text = "Hello!";
}
这时点击按钮1或按钮2都可以让textBox1显示出文本“Hello!”
我们可以让不同的按钮做不同的事,在事件处理器的实现代码中,“object sender“表示的是事件拥有者,于是我们可以对代码做出这样的修改:
private void buttonClicked(object sender, EventArgs e)
{
if (sender == button1)
{
textBox1.Text = "这是按钮1";
}
else if (sender == button2)
{
textBox1.Text = "这是按钮2";
}
}
这时我们再点击按钮1,文本框内便会显示"这是按钮1",而点击按钮2则会显示"这是按钮2"。
事件举例:顾客走进餐馆,服务员上前招待(订阅顾客“点菜”事件),接着顾客进行点菜(发生事件),然后服务员进行相应处理(事件处理器)。
在这个案例中,我们需要声明自定义事件: Order(点菜)、事件拥有者: Customer类对象(顾客)、事件响应者: Waiter类对象(服务员)。
在声明事件之前,需要先声明一个委托类型来作为约束,即事件订阅,其约束了事件能够发送什么事件参数给事件响应者,以及当事件响应者的事件处理器符合规定时(即符合委托类型指定的签名和返回值类型),事件订阅要将其保存起来(即委托字段引用方法)。根据命名规范,该委托应该命名为“事件名+EventHandler”。
之后声明事件成员,要在事件名前面加上修饰符public、事件关键字event、约束它的委托类型,然后在实现部分添加它的处理器add和remove。
除此之外还需要一个用来传递事件参数的类EventArgs,但系统提供的EventArgs不能传递任何数据,它适用于不需要传递数据的场景;如果我们需要传递数据的话,需要自己声明一个派生自EventArgs的类,根据命名规范,该类应该命名为“事件名+EventArgs”。
using System;
using System.Threading;
namespace TestConsole
{
class Program
{
static void Main(string[] args)
{
Customer customer = new Customer(); //事件拥有者
Waiter waiter = new Waiter(); //事件响应者
customer.Order += waiter.Action; //事件Order,事件处理器Action,事件订阅+=
customer.OrderingProcess(); //触发事件,模拟顾客点菜过程
}
}
//用于传递事件参数(事件信息)的类
public class OrderEventArgs : EventArgs
{
public String DishName { get; set; } //表示菜名
public String Size { get; set; } //表示规格
}
//声明委托类型(事件订阅)
//第1个参数为事件拥有者,第2个参数是用来存储点菜事件的相关信息(事件参数)
public delegate void OrderEventHandler(Customer customer,OrderEventArgs e);
//顾客类:事件拥有者
public class Customer
{
//根据前面声明的委托类型来创建一个委托类型字段,用来引用事件处理器
private OrderEventHandler orderEventHandler;
//声明事件:
//event为事件关键字,OrderEventHandler表示用此委托来约束该事件
public event OrderEventHandler Order
{
//添加事件处理器
add
{
orderEventHandler += value;
}
//删除事件处理器
remove
{
orderEventHandler -= value;
}
}
//模拟顾客点菜过程:
public void OrderingProcess()
{
Console.WriteLine("输入回车后开始进行模拟");
Console.ReadLine();
Console.WriteLine("顾客进入餐馆");
Thread.Sleep(1000);
for (int i = 0; i < 3; i++)
{
Console.WriteLine("顾客点菜中...");
Thread.Sleep(1000);
}
//触发事件:
if (orderEventHandler != null) //若不存在任何事件处理器则无法触发事件
{
//准备好事件参数
OrderEventArgs e = new OrderEventArgs();
e.DishName = "饺子";
e.Size = "大份的";
//调用事件处理器
orderEventHandler(this,e);
}
}
}
//侍者类:事件响应者
public class Waiter
{
public void Action(Customer customer, OrderEventArgs e)
{
double price = 0;
//根据规格计算价格
switch (e.Size)
{
case "小份的":
price = 5;
break;
case "中份的":
price = 10;
break;
case "大份的":
price = 15;
break;
}
Console.WriteLine("服务员:好的,稍后将为您提供一份"
+ e.Size + e.DishName + ",您一共需要支付" + price + "元。");
}
}
}
//运行结果:
输入回车后开始进行模拟
顾客进入餐馆
顾客点菜中...
顾客点菜中...
顾客点菜中...
服务员:好的,稍后将为您提供一份大份的饺子,您一共需要支付15元。
可以对Customer类的实现部分进行简化:
在类体中可以不去创建委托类型的字段;声明事件时,事件的实现部分也可以省略;在触发事件和调用事件处理器时,将原来的orderEventHandler字段替换为Order事件。
注意C#规定了事件只能出现在+=或-=运算符的左边,该简化方式在表面上是违反语法的,因为它其实是一种语法糖(即不是表面上看上去的那样简单,一些复杂的底层原理由编译器自动实现)。如果没有使用简化方式,在调用事件处理器时,不使用委托字段而是直接用Order,编译器则会报错:Order只能出现在+=或-=的左边。
public class Customer
{
//声明事件:省略实现部分
public event OrderEventHandler Order;
//模拟顾客点菜过程:
public void OrderingProcess()
{
Console.WriteLine("输入回车后开始进行模拟");
Console.ReadLine();
Console.WriteLine("顾客进入餐馆");
Thread.Sleep(1000);
for (int i = 0; i < 3; i++)
{
Console.WriteLine("顾客点菜中...");
Thread.Sleep(1000);
}
//触发事件:
if (Order!= null) //将原来的orderEventHandler字段替换为Order
{
//准备好事件参数
OrderEventArgs e = new OrderEventArgs();
e.DishName = "饺子";
e.Size = "大份的";
//调用事件处理器
Order(this,e); //将原来的orderEventHandler字段替换为Order
}
}
}
由以上我们可知,事件的本质就是一个委托字段的访问器(包装器),对委托字段的访问起限制作用,其对外界隐藏了委托实例的大部分功能,只提供一个添加/移除事件处理器的接口,如此一来就只有事件拥有者才有资格触发事件,使程序更加安全。
需要一个委托类型来约束事件拥有者能对外传递哪些信息(事件参数)。
需要一个委托类型来约束事件响应者能够用什么签名和返回值类型的方法来处理事件。
委托类型的实例要用于存储(引用)事件处理器。
属性是字段的访问器,对外只提供一部分访问字段的接口,保护字段不被滥用;属性本身不是字段。
事件是委托字段的访问器,对外只提供一部分访问委托字段的接口,保护委托字段不被滥用;事件本身不是委托字段。