# 事件(Event)

事件(Event) 基本上说是一个用户操作,如按键、点击、鼠标移动等等,或者是一些提示信息,如系统生成的通知。应用程序需要在事件发生时响应事件。例如,中断。
C# 中使用事件机制实现线程间的通信。

提示:

  • 事件多用于桌面、手机等开发的客户端编程,因为这些程序经常是用户通过事件来 “驱动” 的
  • 各种编程语言对这个机制的实现方法不尽相同
  • Java 语言里没有事件这种成员,也没有委托这种数据类型。Java 的 “事件” 是使用接口来实现的
  • MVC、MVP、MVVM 等模式,是事件模式更高级、更有效的 “玩法”
  • 日常开发的时候,使用已有事件的机会比较多,自己声明事件的机会比较少,所以一定要做到熟练使用

# 事件的应用

事件在类中声明且生成,且通过使用同一个类或其他类中的委托与事件处理程序关联。

事件的五个组成部分

  1. 事件的拥有者(event source,对象或类)
    只有事件的拥有者才能触发事件
  2. 事件成员(event,成员)
    事件自己不会主动的去通知事件的响应者,只有在事件的拥有者的某些内部逻辑触发以后,他才会去通知事件的响应者。
  3. 事件的响应者(event subscriber,对象)
    订阅了事件的对象或者类
  4. 事件处理器(eventHandler,成员)
    本质上是一个回调方法,它属于事件的响应者的内部方法
  5. 事件订阅
    把事件处理器(一个函数 / 方法)与事件关联在一起,本质上是一种以委托类型为基础的约定(所以事件是基于委托的)

# 事件的使用

事件的拥有者和事件的响应者之间存在三种情况

  1. 事件的拥有者和事件的响应者是两个不同的对象
  2. 事件的拥有者和事件的响应者是同一个对象
  3. 事件的拥有者是事件的响应者的字段成员(事件的响应者用自己的方法订阅字段成员的事件)

注意

  • 事件处理器是方法成员
  • 挂接事件处理器的时候,可以使用委托实例,也可以直接使用方法名
  • 事件处理器对事件的订阅不是随意的,匹配与否由声明事件时所使用的委托类型来检测
  • 事件可以同步调用也可以异步调用

事件的拥有者和事件的响应者是两个不同的对象

public class EventTest
{
    private static void Main(string[] args)
    {
        //Form 是一个窗体
        Form form = new Form(); // 事件的拥有者
        Controller controller = new Controller(form); // 事件的响应者
        form.ShowDialog();
    }
}
public class Controller
{
    private Form CForm;
    public Controller(Form form)
    {
        if (form != null)
        {
            CForm = form;
            //Click 是事件
            CForm.Click += FormClicked; // 订阅事件
        }
    }
    /// <summary>
    /// 事件处理器
    /// 第二个参数是事件的约定,响应事件的前提是处在同一种约定下
    ///sender 是:事件消息的发送者
    /// EventArgs 是:用来传递事件信息的,也就是事件的约束
    /// </summary>
    /// <param name="sender"> 事件消息的发送者 & lt;/param>
    /// <param name="e"> 事件信息 & lt;/param>
    /// <exception cref="NotImplementedException"></exception>
    private void FormClicked(object sender, EventArgs e)
    {
        CForm.Text = DateTime.Now.ToString();
    }
}

事件的拥有者和事件的响应者是同一个对象

public class EventTest
{
    private static void Main(string[] args)
    {
        MyForm myForm = new MyForm();
        myForm.Click += myForm.MyFormClicked;
        myForm.ShowDialog();
    }
}
public class MyForm : Form
{
    /// <summary>
    /// 事件处理器
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    internal void MyFormClicked(object sender, EventArgs e)
    {
            this.Text = DateTime.Now.ToString();
    }
}

事件的拥有者是事件的响应者的字段成员

public class EventTest
{
    private static void Main(string[] args)
    {
        MyForm myForm = new MyForm(); // 事件的响应者
        myForm.ShowDialog();
    }
}
public class MyForm : Form
{
    private TextBox MyTextBox;
    private Button MyButton; // 事件的拥有者,是一个字段成员
    public MyForm()
    {
        MyTextBox = new TextBox();
        MyButton = new Button();
        Controls.Add(MyButton);
        Controls.Add(MyTextBox);
        MyButton.Text = "MyButton";
        MyTextBox.Top = 25;
        //Click 是事件
        MyButton.Click += MyButtonClicked; // 订阅事件
    }
    /// <summary>
    /// 事件处理器
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    /// <exception cref="NotImplementedException"></exception>
    private void MyButtonClicked(object sender, EventArgs e)
    {
        MyTextBox.Text = DateTime.Now.ToString();
    }
}

# 事件信息类

  • 命名规定:如果类是用来传递事件信息的,需要在名字后面加上 EventArgs(事件参数)
  • 继承规定:必须继承 EventArgs,这是微软准备好的基类
public class MyEventArgs : EventArgs
{
    
}

# 用于事件的委托

  • 命名规定:如果委托是为了声明某个事件的,需要在名字后面加上 EventHandler(事件处理程序)
  • EventHandler 的意义:这个委托是用来 声明事件、约束事件处理器,创建的实例是用来储存事件处理器的
  • 一般情况下,事件信息的参数名是 e
  • 一般情况下,使用微软做好的委托就行了(EventHandler)
public delegate void MyEventHandler(object sender, MyEventArgs e);

# 事件的声明

完整的声明方式

// 声明委托
// 保护级别必须是 private
private MyEventHandler myEventHandler;
// 声明事件
public event MyEventHandler Order
{
    add { myEventHandler += value; }
    remove { myEventHandler -= value; }
}

简略的声明方式

// 反编译后可以得知:系统会自动生成委托类型的字段并将其隐藏,内部逻辑与完整的声明方式相同
public event MyEventHandler Order;

使用微软做好的委托来声明事件

public event EventHandler Order;

# 触发事件

// 定义委托
public delegate void OrderEventHandler(object sender, EventArgs e);
public class Test
{
    // 声明委托
    private OrderEventHandler orderEventHandler;
    // 声明事件
    public event OrderEventHandler Order
    {
        add { orderEventHandler += value; }
        remove { orderEventHandler -= value; }
    }
    // 声明事件
    public event EventHandler Order;
    /// <summary>
    /// 触发事件的方法
    /// 保护级别是 protected 或 private
    /// </summary>
    /// <param name="e"></param>
    private void OnOrder(EventArgs e)
    {
        /* 完整的触发方式 */
        // 判断是否有事件处理器(判断储存事件处理器的委托是否为空)
        if (this.orderEventHandler != null)
        {
            // 只有事件的拥有者才能触发事件,所以第一个参数是 this
            orderEventHandler.Invoke(this, e); // 触发事件
        }
        /* 简略的触发方式 */
        this.orderEventHandler?.Invoke(this, e);
        /* 使用事件触发 */
        // 事件只能用 += 和 -=
        // 使用简略的声明方式时,委托类型的字段被隐藏了,
        // 所以这里的做法是 不得已而为之
        // 这就导致了语法的冲突,是微软的问题
        if (Order != null)
        {
            // 只有事件的拥有者才能触发事件,所以一个参数是 this
            Order.Invoke(this, e); // 触发事件
        }
        /* 事件的简略触发方式 */
        this.Order?.Invoke(this, e);
    }
}

# 一个完整的事件处理流程

接下来,我们模拟一个情况:顾客点餐

  • 顾客(事件的拥有者)
  • 顾客点餐(事件)
  • 服务员(事件的响应者)
  • 服务员等待顾客发起点餐(事件订阅)
  • 服务员处理顾客的点餐(事件处理器)
namespace EventsAndDelegates
{
    public class EventTest
    {
        private static void Main(string[] args)
        {
            Customer customer_0 = new Customer(); // 事件的拥有者
            Waiter waiter_0 = new Waiter(); // 事件的响应者
            OrderEventArgs e = new OrderEventArgs()
            {
                FoodName = "XXXX",
                Size = OrderEventArgs.AllSize.Large,
            };
            //Order 是事件
            customer_0.Order += waiter_0.WaiterAction; // 订阅事件
            customer_0.Action();
            string temp = Console.ReadLine();
            while (temp == "1" || temp == "2")
            {
                switch (temp)
                {
                    case "1":
                        customer_0.CustomerOrder(e);
                        break;
                    case "2":
                        customer_0.PayMoney();
                        break;
                }
                temp = Console.ReadLine();
            }
            Console.ReadLine();
        }
    }
    /// <summary>
    /// 传递事件信息
    /// </summary>
    public class OrderEventArgs : EventArgs
    {
        public enum AllSize
        {
            Small,
            Medium,
            Large,
        }
        public string FoodName { get; set; }
        public AllSize Size { get; set; }
    }
    /// <summary>
    /// 事件的拥有者
    /// </summary>
    public class Customer
    {
        // 使用微软做好的委托来声明事件
        public event EventHandler Order;
        public float Money { get; set; } // 客人需要付的钱
        /// <summary>
        /// 触发事件的方法
        /// </summary>
        /// <param name="e"></param>
        protected void OnOrder(OrderEventArgs e)
        {
            if (Order != null)
            {
                Order.Invoke(this, e); // 触发事件
            }
        }
        public void PayMoney()
        {
            Console.WriteLine("I will pay: " + Money);
        }
        private void WalkIn()
        {
            Console.WriteLine("WalkIn");
        }
        private void SetDown()
        {
            Console.WriteLine("SetDown");
        }
        private void Think()
        {
            Console.WriteLine("Let me think...");
            Thread.Sleep(1000);
        }
        public void Action()
        {
            WalkIn();
            SetDown();
        }
        public void CustomerOrder(OrderEventArgs e)
        {
            Think();
            OnOrder(e);
        }
    }
    /// <summary>
    /// 事件的响应者
    /// </summary>
    public class Waiter
    {
        /// <summary>
        /// 事件处理器
        /// </summary>
        /// <param name="customer"> 事件消息的发送者 & lt;/param>
        /// <param name="e"> 事件信息 & lt;/param>
        /// <exception cref="NotImplementedException"></exception>
        public void WaiterAction(object sender /*Customer customer*/, EventArgs e)
        {
            Customer customer = sender as Customer;
            OrderEventArgs orderEventArgs = e as OrderEventArgs;
            float price = 20;
            switch (orderEventArgs.Size)
            {
                case OrderEventArgs.AllSize.Small:
                    price *= 0.5f;
                    break;
                case OrderEventArgs.AllSize.Medium:
                    break;
                case OrderEventArgs.AllSize.Large:
                    price *= 1.5f;
                    break;
            }
            customer.Money += price;
            Console.WriteLine("Food Name: " + orderEventArgs.FoodName + "\tPrice: " + price);
        }
    }
}

# 事件与委托的关系

  • 事件真的是 “以特殊方式声明的委托字段 / 实例” 吗?
    • 不是!只是声明的时候 “看起来像”(对比委托字段与事件的简化声明,field-like)
    • 事件声明的时候使用了委托类型,简化声明造成事件看上去像一个委托的字段 (实例),而 event 关键字则更像是一个修饰符 —— 这就是错觉的来源之一
    • 订阅事件的时候 += 操作符后面可以是一个委托实例,这与委托实例的赋值方法语法相同,这也让事件看起来像是一个委托字段 —— 这是错觉的又一来源
    • 重申:事件的本质是加装在委托字段上的一个 “蒙板”(mask),是一个起掩蔽作用的包装器。这个用于阻挡非法操作的 “蒙板” 绝不是委托字段本身
  • 为什么要使用委托类型来声明事件?
    • 站在 source 的角度来看,是为了表明 source 能对外传递哪些消息
    • 站在 subscriber 的角度来看,它是一种约定,是为了约束能够使用什么样签名的方法来处理 (响应) 事件
    • 委托类型的实例将用于存储 (引用) 事件处理器
  • 对比事件与属性
    • 属性不是字段 —— 很多时候,属性是字段的包装器,这个包装器用来保护字段不被滥用
    • 事件不是委托字段 —— 它是委托字段的包装器,这个包装器用来保护委托字段不被滥用
    • 包装器永远都不可能是被包装的东西