若要写一个事件:首先要明确:一个完整的事件模型由五部分组成:事件拥有者、事件、事件响应者、事件处理器、订阅。
用前面的例子:顾客Customer去餐馆点餐Order然后Waiter来服务这个事件。
第一步:事件拥有者和事件响应者
用C#来写,从简单的入手。首先写自己有把握的,比如。先定义两个对象类:一个Customer类,一个Waiter类。其他信息我们啥也不知道。但是我们知道肯定要有这两个人来做这事。
public class Customer
{
}
public class Waiter
{
}
并且去主函数中实例化对象备用:
static void Main(string[] args)
{
//实例化对象:事件拥有者、事件响应者
Customer customer = new Customer();
Waiter waiter = new Waiter();
}
这都是C#最基础的,直接写。成不成,草稿要划一划嘛。
第二步:事件
其次我们要明确一点,点餐这个Order事件拥有者是谁?谁点餐,顾客点餐,所以事件拥有者就是顾客。事件响应者就是Waiter。
那么我们就可以在Customer里面就可以写上一个事件:Order。
不论是以字段式声明的写法还是在class平级中以委托类型的自定义声明都可以。字段式写法简单,一会看。如果是委托类型的自定义事件声明,就需要在Customer类内声明一个事件类型的字段。并且为事件写上事件处理器的添加器和移除器。注意命名方式:后缀是EventHandler和EventArgs,其中EventArgs基类继承。是最复杂的。在上一篇文章中,已经有了完整过程。非必要不写完整声明。这里咱们只是简单的看看哈。
咱们还是从简单的开始:字段式声明,直接用DotNet给我们准备好的EventHandler方法。
public event EventHandler Order;
按住Ctrl点进去看看定义。
本质还是基于委托。注意其委托方法的参数类型,一个是object,一个EventArgs。要知道这俩参数干啥用的。一会讲。现在不管。(对应下文,VS智能提示补全的事件处理器Action的参数。其实这个委托就是要委托这个事件处理器。所以跟事件处理器的参数类型相同)
我们在Customer类中写上事件:至此五部分就有了三个。
namespace Event07默写一遍
{
class Program
{
static void Main(string[] args)
{
//实例化对象:事件拥有者、事件响应者
Customer customer = new Customer();
Waiter waiter = new Waiter();
}
}
public class Customer
{
// 事件声明好了
public event EventHandler Order;
}
public class Waiter
{
}
}
第三步:硬写事件处理器以及订阅
硬写,这个词,就是为了搭起主要架构。在主函数中写事件的订阅。事件的订阅是通过事件订阅操作符+=完成。左边必须是事件,右边是响应者的事件处理器。
写的时候没注意到用的类方法,实例化对象Tab没用上,用类定义的事件处理器,所以导致方法是静态的。最后面有重构。不影响。请继续阅读。
using System;
namespace Event07默写一遍
{
class Program
{
static void Main(string[] args)
{
//实例化对象:事件拥有者、事件响应者
Customer customer = new Customer();
Waiter waiter = new Waiter();
customer.Order += Waiter.Action;
}
}
public class Customer
{
public event EventHandler Order;
}
public class Waiter
{
internal static void Action(object sender, EventArgs e)
{
throw new NotImplementedException();
}
}
}
通过硬写用VS的智能提示,补全了我们的事件处理器方法体。
至此主要的框架搭起来了,主干有了。后面开始补全。
事件的委托类型定义: public delegate void EventHandler(object sender, EventArgs e);
事件处理器的定义: internal static void Action(object sender, EventArgs e)
第一句来源我们在Customer类内声明事件时,查看EventHandler方法时。发现是一个委托类型。
第二句是我们在Waiter类中生成的事件处理器;两者参数相同。提出委托的对象找到了。
所以隐藏在事件处理器背后的是委托。
响应者通过委托把处理器传递给事件。供事件去跨类之间调用。
事件处理器本质就是一个函数方法,当事件发生了,响应者如何做。编程语言表述“如何做”就是用一个函数方法。
订阅,就是将一个方法作为参数赋值给另一个变量,这样的操作在C#叫做委托。
何为委托:因为C#是强类型的语言,所有变量或参数必须要有数据类型。如果我们想把function()方法交给别人来完成,比如另起线程执行它。那么这个function()函数,就必须作为一个参数传递给另外一个函数。这个参数必须要有数据类型,C#没办法了,定义了一个这样的数据类型,叫委托:定义一个变量,变量的内容为函数体。换句话说,就是C++中的函数指针。
这个变量保存在栈中,其值是函数体在堆中的内存位置。一般说,指向函数地址(位置)。将这个值赋值给另外一个变量,作为参数传递。这个参数的类型必须是委托类型才可以接受这个地址值。
第四步:完善事件机制并触发。
上面事件模型的五个部分都已经写完了,下面针对每个部分进行完善。
事件有了,如何触发事件。让整个过程动起来。早先的文章中提到:事件是由事件拥有者(事精 or TroubleMaker)内部逻辑触发的。。。。别管这么多了,触发这事肯定写在事件拥有者内部。
如何写呢?我们要从我们搭好的框架中来寻找线索。目前Waiter中的事件处理器要求两个参数:我们来看一下这两个参数:
internal static void Action(object sender, EventArgs e)
- object sender:肯定是事件的发生者,也就说应该传入的是一个对象。这个简单,就是传入实例化对象customer。
- EventArgs e:想一下,事件的本质功能:通知。那么事件通知的啥?通知的内容就是EventArgs。消息装在了EventArgs中。所以在事件内部需要有消息发送出来。
我们需要补全事件内的消息。这个消息是EventArgs类型。这个消息应该是Order点餐这个事件的消息,有啥消息:比如,点的什么食物、大份小份、几份等。这个消息我们单独抽象出来。
也就是将事件发送的消息单独抽象为一类,于此同时我们也不想造轮子,自己写太多。所以自定义的基础上,派生自EventArgs这个类。
public class OrderEventArgs:EventArgs
{
public string DishName { get; set; }
public string Size { get; set; }
public int Number { get; set; }
}
有了事件中的消息,此时我们把注意力回归到事件本身。
public event EventHandler Order;
事件此时在类中安安静静的做个事件成员美男子。等着我们去包装然后触发发送。
触发之前,我们要明确这个事件成员是否为空。不为空才可以发送,并且将消息内容封装到事件消息中。
具体步骤:首先判断事件非空,然后实例化消息,接下来将消息写入事件触发内包装好,等到触发。
public class Customer
{
public event EventHandler Order;
public void Action()
{
if (this.Order != null)
{
// 实例化消息
OrderEventArgs e = new OrderEventArgs();
// 将事件消息封装。
e.DishName = "Beer";
e.Size = "Large";
e.Number = 10;
// 事件触发
Console.WriteLine("事件将要触发了,请响应者注意!!!");
Console.ReadLine();
this.Order.Invoke(this, e); //会立即调用事件处理器。
Console.WriteLine("事件触发完毕。");
Console.ReadLine();
}
}
}
我们来看看事件EventHandler方法下事件机制触发的真相。用鼠标选中Invoke,按F1查看MSDN中帮助文档。联机版。事件委托的案例。
也就是说事件是在事件拥有者内部,通过逻辑Action方法来触发的。其实就是通过Invoke直接调用委托来盘活整个过程。
接下来只要在主函数调用Action方法就可以触发事件了。
static void Main(string[] args)
{
//实例化对象:事件拥有者、事件响应者
Customer customer = new Customer();
Waiter waiter = new Waiter();
customer.Order += Waiter.Action;
customer.Action();
}
第五步:触发响应。
接下来:我们来重写事件处理器,来观察响应的流程。
public class Waiter
{
internal static void Action(object sender, EventArgs e)
{
Console.WriteLine("我是响应者的事件处理器,我已知道你那点破事发生了。剩下的我懂,交给我吧。") ;
Console.ReadLine();
}
}
按两次回车:结果如下。Invoke之后会直接调用事件处理器方法并执行。实现跨类、快速调用绑定的方法。
至此整个事件的过程基本算完成了。但是咱是点餐嘛,业务逻辑代码要完善。
第六步:完善业务逻辑代码
客户已经在事件消息中将餐点好10瓶啤酒,需要服务人员确认下单。并计算金额,客户喝完付费走人。
这里面就新增了好多内容:首先服务人员要将事件消息解读出来。这个解码的过程,必定跟编码(封装)的是同一套东西,否则订阅不了。这个解码编码的规定就是一种约束,这个约束后面讲。现在讲一点好处。
Waiter将EventArgs类型的OrderEventArgs中的消息解读并计算金额,解读肯定是在事件处理器中完成,别的地方也解读不了这个信息,这就是一种封装,这种封装就避免了消息数据的泄漏,避免了其他客人点餐信息混串一起。一对一绑定。精准服务并处理。
(代码是对现实世界的模拟和抽象,更是理想化的现实世界。)
public class Waiter
{
internal static void Action(object sender, EventArgs e)
{
Console.WriteLine("我是响应者的事件处理器,我已知道你那点破事发生了。剩下的我懂,交给我吧。") ;
Console.ReadLine();
Customer customer = sender as Customer;
OrderEventArgs orderInfo = e as OrderEventArgs;
Console.WriteLine("Waiter serve you the dish {0}.",orderInfo.DishName);
//服务方负责计算金额,并且知晓价格,实际中价格应该公开。此处为了简化。
double price = 10;
switch (orderInfo.Size)
{
case "Large":
price = price * 2;
break;
case "Small":
price = price * 0.75;
break;
default:
break;
}
customer.Bill += price * orderInfo.Number;
}
}
想到哪里写到哪里。然后借助VS硬写。
账单有了,Customer功能也需要完善。Customer需要喝完,然后付账单。
public class Customer
{
public double Bill { get; internal set; }
public event EventHandler Order;
// 实例化消息,为了避免浪费,点多少喝多少,把数据类内共享
// 实例化放在了方法外。
OrderEventArgs e = new OrderEventArgs();
public void Action()
{
if (this.Order != null)
{
// 将事件消息封装。
e.DishName = "Beer";
e.Size = "Large";
e.Number = 10;
// 事件触发
Console.WriteLine("事件将要触发了,请响应者注意!!!");
Console.ReadLine();
this.Order.Invoke(this, e);
this.Drink();
this.PayTheBill();
Console.WriteLine("事件触发完毕。");
Console.ReadLine();
}
}
public void Drink()
{
Console.WriteLine("开始整:");
for (int i = 0; i < e.Number; i++)
{
Thread.Sleep(500);
Console.WriteLine("Drink-{0}.....",i);
}
}
public void PayTheBill()
{
Console.WriteLine("各位,今晚全场的消费由赵公子买单!");
Console.WriteLine("这桌消费了{0}。", this.Bill);
}
}
反思:
五种颜色:五个部分。
事(shei)是他的,怎么做就是你的了,订阅了说明你关注这事。
代码中不可能多管闲事,现实中可能是吃咸萝卜操淡心。代码中不管就是没有订阅,现实中可能是真不关心,还有可能是就是能力不够管不了,订阅不了。
问题:现实中可能能力不够管不了,代码中为啥能力不够订阅不了?
回答:通过VS智能补全的事件处理器中的参数我们可以知道,我们的事件处理器只能接受指定的事件,或者说指定类型的事件,或者说指定内容的事件。
一个事件可以绑定多个事件处理器。一个事件处理器也可以绑定多个事件。但并非随意一个事件处理器就能绑定任意一个事件。能否响应订阅,要看他们之间的约束是否满足。
因为委托中定义(约束)了的参数的类型。你的事件处理器中的参数类型,跟人家的不一致。这个约束条件是通过委托传给你的。委托嘛,可能所托非人。你就假装委托人没通知你。如果我们可以通过定义多个事件来看。
(我们写代码的思路跟正好相反,是通过事件处理器反推的事件中传递消息的类型和事件。设计的时候,这个过程正面推进。)
结束语:
真正我们设计的地方:事件可以定义多个。针对每一个事件,设计不同的消息类型类,对不同的事件拥有者来绑定不同的事件处理器。
文末附:整个代码:(事件处理器本例中是静态的,其实可以修改为动态的,这样可以不断实例化waiter服务更多人。文后有重构。)
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace Event07默写一遍
{
class Program
{
static void Main(string[] args)
{
//实例化对象:事件拥有者、事件响应者
Customer customer = new Customer();
Waiter waiter = new Waiter();
customer.Order += Waiter.Action;
customer.Action();
}
}
public class Customer
{
public double Bill { get; internal set; }
public event EventHandler Order;
// 实例化消息,为了避免浪费,点多少喝多少,把数据类内共享
// 实例化放在了方法外。
OrderEventArgs e = new OrderEventArgs();
public void Action()
{
if (this.Order != null)
{
// 将事件消息封装。
e.DishName = "Beer";
e.Size = "Large";
e.Number = 10;
// 事件触发
Console.WriteLine("事件将要触发了,请响应者注意!!!");
Console.ReadLine();
this.Order.Invoke(this, e);
this.Drink();
this.PayTheBill();
Console.WriteLine("事件触发完毕。");
Console.ReadLine();
}
}
public void Drink()
{
Console.WriteLine("开始整:");
for (int i = 0; i < e.Number; i++)
{
Thread.Sleep(500);
Console.WriteLine("Drink-{0}.....",i);
}
}
public void PayTheBill()
{
Console.WriteLine("各位,今晚全场的消费由赵公子买单!");
Console.WriteLine("这桌消费了{0}。", this.Bill);
}
}
public class OrderEventArgs:EventArgs
{
public string DishName { get; set; }
public string Size { get; set; }
public int Number { get; set; }
}
public class Waiter
{
internal static void Action(object sender, EventArgs e)
{
Console.WriteLine("我是响应者的事件处理器,我已知道你那点破事发生了。剩下的我懂,交给我吧。") ;
Console.ReadLine();
Customer customer = sender as Customer;
OrderEventArgs orderInfo = e as OrderEventArgs;
Console.WriteLine("Waiter serve you the dish {0}.",orderInfo.DishName);
//服务方负责计算金额,并且知晓价格,实际中价格应该公开。此处为了简化。
double price = 10;
switch (orderInfo.Size)
{
case "Large":
price = price * 2;
break;
case "Small":
price = price * 0.75;
break;
default:
break;
}
customer.Bill += price * orderInfo.Number;
}
}
}
代码重构:类内不具体点餐。类外实现。多人点餐,各自结算。各自绑定各自的事件。其实可以把点餐再抽象一步。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace Event08重构
{
class Program
{
static void Main(string[] args)
{
//实例化对象:事件拥有者、事件响应者
Customer customer1 = new Customer(1);
Waiter waiter1 = new Waiter(1);
customer1.Order += waiter1.Serve;
customer1.Think("Beer", "Large", 10);
Customer customer2 = new Customer(2);
Waiter waiter2 = new Waiter(2);
customer2.Order += waiter2.Serve;
customer2.Think("Chicken", "Small", 5);
Customer badGuy = new Customer(13);
Waiter waiter11 = new Waiter(11);
badGuy.Order += waiter11.Serve;
// 这个坏蛋想吃满汉全席,让1号服务员服务,挂账到1号桌。结果还是自己账单。
badGuy.Order += waiter1.Serve;
badGuy.Think("Manhanquanxi", "Large", 10);
Console.ReadLine();
}
}
public class Customer
{
public double Bill { get; internal set; }
public int DeskID { get; set; }
public int DishNumber { get; set; }
public Customer(int deskID)
{
this.DeskID = deskID;
}
//声明事件
public event EventHandler Order;
// 触发器一般命名方式是On+事件。“事出有因”,“因何引发”
// 这个方法的级别应该是protected,否则又可以被其他人利用,用来借刀杀人。
// 如果改为protected那么实例无法直接访问,为此。需要新的方法再类内调用工外部使用。
// 所以才有了说事件是由拥有者内部逻辑触发的。
protected void OnOrder(OrderEventArgs e)
{
// 类似“单例模式”中判断窗口字段是否已存在。
if (this.Order != null)
{
this.Order.Invoke(this, e);
this.Eat(e.Number);
this.PayTheBill();
}
}
public void Think(string dishName, string size, int number)
{
OrderEventArgs e = new OrderEventArgs();
e.DishName = dishName;
e.Size = size;
e.Number = number;
this.OnOrder(e);
}
public void Eat(int EatNumber)
{
Console.WriteLine("开始干饭:");
for (int i = 0; i < DishNumber; i++)
{
Thread.Sleep(100);
Console.WriteLine("Eat-{0}.....", i);
}
}
public void PayTheBill()
{
Console.WriteLine("{0}这桌消费了{1}。", this.DeskID,this.Bill);
}
}
public class OrderEventArgs : EventArgs
{
public string DishName { get; set; }
public string Size { get; set; }
public int Number { get; set; }
}
public class Waiter
{
public int ServeID { get; set; }
public Waiter(int serveID)
{
this.ServeID = serveID;
}
public void Serve(object sender, EventArgs e)
{
Customer customer = sender as Customer;
OrderEventArgs orderInfo = e as OrderEventArgs;
Console.WriteLine("Waiter{0} serve you the dish {1}.", this.ServeID, orderInfo.DishName);
double price = 10;
switch (orderInfo.Size)
{
case "Large":
price = price * 2;
break;
case "Small":
price = price * 0.75;
break;
default:
break;
}
customer.Bill += price * orderInfo.Number;
customer.DishNumber += orderInfo.Number;
}
}
}
如果想让一个类成员只能够被自己的派生类访问,不能够被其他的类访问的时候,这些成员的访问级别就应该是protected。不能为public。