Skip to content

8. 用户事件

上一章我们讨论了与表单组件相关的事件概念。现在我们将了解如何在自己的类中创建事件。

8.1. 预定义的委托对象

上一章曾提及 delegate 对象的概念,但当时仅进行了简要介绍。在探讨表单组件事件处理程序的声明方式时,我们曾遇到过类似以下代码:


            this.buttonAfficher.Click += new System.EventHandler(this.buttonAfficher_Click);

其中 buttonAfficher 是 [Button] 类型的组件。该类有一个名为 Click 的字段,定义如下:

  • [1]:类 [Button]
  • [2]:其事件
  • [3,4]:事件 Click
  • [5]:事件 [Control.Click] [4] 的声明。
    • EventHandler 是一个称为委托的方法原型(模板)。
    • event 是一个关键字,用于限制 delegate 的功能。EventHandlerdelegate 对象的功能比 event 对象更丰富。

delegate EventHandler 的定义如下:

 

delegate EventHandler 表示一种方法模板:

  • 其第一个参数为 Object 类型
  • 第二个参数类型为 EventArgs
  • 不返回任何结果

符合由 EventHandler 定义的模型的方法可能如下:


        private void buttonAfficher_Click(object sender, EventArgs e);

要创建类型为 EventHandler 的对象,请按以下步骤操作:

EventHandler evtHandler=new EventHandler(méthode correspondant au prototype  défini par le type EventHandler);

因此可以编写如下代码:

EventHandler evtHandler=new EventHandler(buttonAfficher_Click);

类型为 delegate 的变量实际上是一组对 delegate 模型中相应方法的引用列表。 若要在上述变量 evtHandler 中添加新方法 M,请使用以下语法:

evtHandler+=new EvtHandler(M);

即使 evtHandler 是一个空列表,也可以使用 += 语法。

语句:


            this.buttonAfficher.Click += new System.EventHandler(this.buttonAfficher_Click);

将类型为 EventHandler 的方法添加到事件 buttonAfficher.Click 的方法列表中。 当组件 buttonAfficher 上的事件 Click 发生时,VB 执行以下语句:

            buttonAfficher.Click(source, evt);

其中:

  • source 是类型为 object 的组件,该组件触发了事件
  • evt(类型为 EventArgs)的事件,且不包含任何信息

所有通过以下方式与事件 Click 关联的 void M(object,EventArgs) 签名方法:


            this.buttonAfficher.Click += new System.EventHandler(M);

将使用由 VB 传递的参数 (source, evt) 被调用。

8.2. 定义委托对象

语句


    public delegate int Opération(int n1, int n2);

定义了一个名为 Opération 的类型,该类型表示一个接受两个整数并返回一个整数的函数原型。正是关键字 delegate 使 Opération 成为函数原型的定义。

一个类型为 Opération 的变量 op 将用于存储与原型 Opération 对应的函数列表:

int f1(int,int)
int f2(int,int)
...
int fn(int,int)

将方法 fi 存储在变量 op 中,可通过以下方式实现:op=new 操作(fi) 或者更简单地写成 op=fi.若要将方法 fj 添加到已存储的函数列表中,应写为 op+= fj。若要删除已存储的方法 fk,应写为 op-=fk。 如果在本例中输入 n=op(n1,n2),则变量 op 中记录的所有方法都将使用参数 n1n2 执行。 最终获取的结果 n 将是最后一个执行的方法的结果。无法获取所有方法生成的结果。因此,如果将一组方法存储在委托函数中,这些方法通常会返回类型为 void 的结果。

请看以下示例:


using System;
namespace Chap6 {
    class Class1 {
        // 定义函数原型
        // 接受 2 个整数作为参数,并返回一个整数
        public delegate int Opération(int n1, int n2);

        // 两个与原型对应的实例方法
        public int Ajouter(int n1, int n2) {
            Console.WriteLine("Ajouter(" + n1 + "," + n2 + ")");
            return n1 + n2;
        }//添加

        public int Soustraire(int n1, int n2) {
            Console.WriteLine("Soustraire(" + n1 + "," + n2 + ")");
            return n1 - n2;
        }//减法

        // 一个与原型对应的静态方法
        public static int Augmenter(int n1, int n2) {
            Console.WriteLine("Augmenter(" + n1 + "," + n2 + ")");
            return n1 + 2 * n2;
        }//增加

        static void Main(string[] args) {

            // 定义一个操作类型的对象,用于存储函数
            // 注册静态函数“增加”
            Opération op = Augmenter;
            // 执行委托
            int n = op(4, 7);
            Console.WriteLine("n=" + n);

            // 创建一个类型为 class1 的对象 c1
            Class1 c1 = new Class1();
            // 将 c1 的 add 方法注册到委托中
            op = c1.Ajouter;
            // 执行委托对象
            n = op(2, 3);
            Console.WriteLine("n=" + n);
            // 将 c1 的减法方法注册到委托中
            op = c1.Soustraire;
            n = op(2, 3);
            Console.WriteLine("n=" + n);
            //将两个函数注册到委托中
            op = c1.Ajouter;
            op += c1.Soustraire;
            // 执行委托对象
            op(0, 0);
            // 从委托中移除一个函数
            op -= c1.Soustraire;
            // 执行委托
            op(1, 1);
        }
    }
}
  • 第 3 行:定义类 Class1
  • 第 6 行:定义 delegateOpération:一个方法原型,接受两个 int 类型的参数,并返回 int 类型的结果
  • 第 9-12 行:实例方法 Ajouter 具有委托 Opération 的签名。
  • 第 14-17 行:实例方法 Soustraire 具有 Opération 委托的签名。
  • 第 20-23 行:方法 Augmenter 的签名与 Opération 委托的签名一致。
  • 第 25 行:方法 Main 已执行
  • 第20行:变量op的类型为Opération委托。它将包含一组具有Opération委托签名的方法列表此时为其赋值了第一个方法引用,即静态方法Class1.Augmenter的引用。
  • 第 31 行:执行 delegate opop 引用的所有方法都将被执行。 它们将使用传递给 delegateop. 的参数进行执行。在此,仅会执行静态方法 Class1.Augmenter
  • 第 35 行:创建了类 Class1 的实例 c1
  • 第 37 行:实例方法 c1.Ajouter 被分配给委托 opAugmenter 是静态方法,Ajouter 是实例方法。此处旨在说明这并不重要。
  • 第 39 行: 执行 delegate op:方法 Ajouter 将使用传递给 delegate op 的参数进行执行。
  • 第 42 行:对实例方法 Soustraire 重复上述操作。
  • 第 46-47 行:将方法 Ajouter Soustraire 放入 delegate op 中。
  • 第49行:执行delegateop: 方法 Ajouter Soustraire 将使用传递给 delegate op 的参数进行执行。
  • 第 51 行:方法 Soustraire delegate op 中移除。
  • 第 53 行:执行 delegate op:剩余的方法 Ajouter 将被执行。

执行结果如下:

1
2
3
4
5
6
7
8
9
Augmenter(4,7)
n=18
Ajouter(2,3)
n=5
Soustraire(2,3)
n=-1
Ajouter(0,0)
Soustraire(0,0)
Ajouter(1,1)

8.3. 委托还是接口?

delegates与接口的概念看似相当接近,人们可能会质疑这两者之间究竟有何区别。让我们以一个与先前研究过的示例相似的例子来说明:


using System;
namespace Chap6 {
    class Program1 {
        // 定义函数原型
        // 接受 2 个整数作为参数并返回一个整数
        public delegate int Opération(int n1, int n2);

        // 两个与原型对应的实例方法
        public static int Ajouter(int n1, int n2) {
            Console.WriteLine("Ajouter(" + n1 + "," + n2 + ")");
            return n1 + n2;
        }//添加

        public static int Soustraire(int n1, int n2) {
            Console.WriteLine("Soustraire(" + n1 + "," + n2 + ")");
            return n1 - n2;
        }//减法

        // 执行委托
        public static int Execute(Opération op, int n1, int n2){
            return op(n1, n2);
        }

        static void Main(string[] args) {
            // 执行“加法”委托
            Console.WriteLine(Execute(Ajouter, 2, 3));
            // 执行减法委托
            Console.WriteLine(Execute(Soustraire, 2, 3));
            // 执行多播委托
            Opération op = Ajouter;
            op += Soustraire;
            Console.WriteLine(Execute(op, 2, 3));
            // 从代理中移除一个函数
            op -= Soustraire;
            // 执行委托
            Console.WriteLine(Execute(op, 2, 3));
        }
    }
}

第 20 行,方法 Execute 期望接收一个指向第 6 行定义的 Opération 委托类型对象的引用。这使得该方法能够调用 Execute 中的不同方法(第 26、28、32 和 36 行)。 这种多态特性也可以通过接口实现:


using System;

namespace Chap6 {

    // 接口 IOperation
    public interface IOperation {
        int operation(int n1, int n2);
    }

    // Add类
    public class Ajouter : IOperation {
        public int operation(int n1, int n2) {
            Console.WriteLine("Ajouter(" + n1 + "," + n2 + ")");
            return n1 + n2;
        }
    }

    // Subtract 类
    public class Soustraire : IOperation {
        public int operation(int n1, int n2) {
            Console.WriteLine("Soustraire(" + n1 + "," + n2 + ")");
            return n1 - n2;
        }
    }

    // 测试类
    public static class Program2 {
        // 执行接口的唯一方法 IOperation
        public static int Execute(IOperation op, int n1, int n2) {
            return op.operation(n1, n2);
        }

        public static void Main() {
            // 执行“加法”委托
            Console.WriteLine(Execute(new Ajouter(), 2, 3));
            // 执行减法委托
            Console.WriteLine(Execute(new Soustraire(), 2, 3));
        }
    }
}
  • 第 6-8 行:接口 [IOperation] 定义了方法 operation
  • 第 11-16 行和第 19-24 行:类 [Ajouter] 和 [Soustraire] 实现了接口 [IOperation]。
  • 第29-31行:方法Execute,其第一个参数的类型为接口IOperation。 方法 Execute 将依次接收 Ajouter 类的实例和 Soustraire 类的实例作为第一个参数。

这确实体现了前一个示例中 delegate 类型参数所具有的多态性。这两个示例同时也展示了这两个概念之间的差异。

类型 delegateinterface 是可互换的

  • 。实际上,类型 delegate 是针对单一方法的封装,而接口本身可以定义多个方法。
  • 如果未使用 delegate 的多播特性。事实上,接口中并不存在多播这一概念。

如果满足上述两个条件,则方法 Execute 可在以下两种签名中进行选择:


int Execute(IOperation op, int n1, int n2)
int Execute(Opération op, int n1, int n2)

使用 delegate 的第二种签名可能更灵活。因为在第一种签名中,方法的第一个参数必须实现 IOperation 接口。 这迫使开发者必须创建一个类,并在其中定义将作为 Execute 方法第一个参数传递的方法。而在第二种签名中,任何具有正确签名的现有方法均可适用。无需进行额外的构建工作。

8.4. 事件管理

delegate 对象可用于定义事件。C1 类可以如下方式定义 evt 事件:

  • 在 C1 类内部或外部定义一个 delegate 类型:
delegate TResult Evt(T1 param1, T2 param2, ...);
  • C1 定义了一个类型为 delegate 的字段 Evt
public Evt Evt1;
  • 当类 C1 的实例 c1 需要触发事件时,它将调用其委托 Evt1,并向其传递由委托 Evt 定义的参数。 此时,delegate Evt1中注册的所有方法都将使用这些参数被执行。可以说,它们已收到Evt1事件的通知。
  • 如果一个使用 c1 对象的 c2 对象希望收到 c1 对象上 Evt1 事件发生的通知, 它将把其方法 c2.M 之一注册到对象 c1. 的委托对象 c1.Evt1 中。因此,每当当事件 Evt1 在对象 c1 上发生时,其方法 c2.M 就会被执行。当它不再希望收到该事件的通知时,也可以取消注册。
  • 由于代理对象 c1.Evt1 可以注册多个方法, 不同的 ci 对象可以向代理 c1.Evt1 注册,以便接收关于 c1Evt1 事件的通知。

在此场景中,我们有:

  • 一个发布事件的类
  • 若干接收该事件通知的类。通常称其为“订阅该事件”或“订阅该事件”。
  • 一个类型 delegate,用于定义将收到事件通知的方法的签名

.NET框架定义了:

  • delegate事件的标准签名
public delegate void MyEventHandler(object source, EventArgs evtInfo);
  • source:触发该事件的对象
  • evtInfo:一个 EventArgs 类型或其派生对象,提供有关该事件的信息
  • delegate 的名称必须以 EventHandler 结尾
  • 在类中声明 MyEventHandler 类型事件的标准方式:
1
2
3
4
public Class C1{
    public event MyEventHandler Evt1;
...
}

字段 Evt1 的类型为 delegate。关键字 event 的作用是限制对其可执行的操作:

  • C1 类外部,仅允许执行 += 和 -= 操作。这可防止(例如因开发人员失误)误删订阅该事件的方法。用户仅能对事件进行订阅(+=)或取消订阅(-=)。
  • 只有 C1 类型的实例才能调用 Evt1(source,evtInfo),从而触发订阅了 Evt1. 事件的方法的执行

框架 .NET 提供了一个符合事件 delegate 签名要求的通用方法:

public delegate void EventHandler<TEventArgs>(object source, TEventArgs evtInfo) where TEventArgs : EventArgs
  • delegate EventHandler 使用泛型类型 TEventArgs,该类型是其第二个参数的类型
  • 类型 TEventArgs 必须继承自类型 EventsArgs其中 TEventArgs 继承自 EventArgs

对于这个通用的 delegate 类型,在 C 类中声明 X 事件时,应遵循以下建议的模式:

  • 定义一个从 EventArgs 派生的 XEventArgs 类型,用于封装 X 事件的信息
  • 在类 C 中定义类型为 EventHandler<XEventArgs> 的事件。
  • 在类 C 中定义一个受保护的方法
protected void OnXHandler(XEventArgs e);

,用于向订阅者“发布”事件 X。

考虑以下示例:

  • Emetteur 封装了一个温度值。该温度值被持续监测。当温度超过某个阈值时,应触发一个事件。我们将该事件命名为 TemperatureTropHaute。该事件的相关信息将被封装在类型 TemperatureTropHauteEventArgs
  • Souscripteur 订阅了上述事件。当接收到事件通知时,它会在控制台显示一条消息。
  • 一个控制台程序创建了一个发布者和两个订阅者。它通过键盘输入温度,并将温度记录到 Emetteur 实例中。如果温度过高,Emetteur 实例将发布事件 TemperatureTropHaute。

为了遵循推荐的事件管理方法,我们首先定义类型 TemperatureTropHauteEventArgs 来封装事件信息:


using System;

namespace Chap6 {
    public class TemperatureTropHauteEventArgs:EventArgs {
        // 事件发生时的温度
        public decimal Temperature { get; set; }
        // 制造商
        public TemperatureTropHauteEventArgs() {
        }
        public TemperatureTropHauteEventArgs(decimal temperature) {
            Temperature = temperature;
        }
    }
}
  • 第 6 行:由类 TemperatureTropHauteEventArgs 封装的信息是引发事件 TemperatureTropHaute 的温度。

Emetteur 如下所示:


using System;

namespace Chap6 {
    public class Emetteur {
        static decimal SEUIL = 19;

        // 观测温度
        private decimal temperature;
        // 数据源名称
        public string Nom { get; set; }
        // 事件报告
        public event EventHandler<TemperatureTropHauteEventArgs> TemperatureTropHaute;

        // 温度读写
        public decimal Temperature {
            get {
                return temperature;
            }
            set {
                temperature = value;
                if (temperature > SEUIL) {
                    // 向订阅者报告事件
                    OnTemperatureTropHaute(new TemperatureTropHauteEventArgs(temperature));
                }
            }
        }

        // 事件报告
        protected virtual void OnTemperatureTropHaute(TemperatureTropHauteEventArgs evt) {
            // 向订阅者发送事件 TemperatureTropHaute
            TemperatureTropHaute(this, evt);
        }
    }
}
  • 第5行:超过该温度阈值时,将发布事件TemperatureTropHaute
  • 第10行:发送者有一个用于识别的名称
  • 第12行:事件 TemperatureTropHaute
  • 第15-26行:返回温度的get方法,以及记录温度的set方法。 当待记录的温度超过第5行的阈值时,set方法会触发发布TemperatureTropHaute事件。 它通过第29行的OnTemperatureTropHauteHandler方法发布该事件,并向其传递一个TemperatureTropHauteEventArgs对象作为参数,该对象中记录了超过阈值的温度。
  • 第29-32行:发布事件TemperatureTropHaute,其第一个参数为发布者本身,第二个参数为作为参数接收到的TemperatureTropHauteEventArgs对象

将订阅事件 TemperatureTropHaute 的类 Souscripteur 如下:


using System;

namespace Chap6 {
    public class Souscripteur {
        // 名称
        public string Nom { get; set; }

        // 事件 TemperatureTropHaute 的管理员
        public void EvtTemperatureTropHaute(object source, TemperatureTropHauteEventArgs e) {
            // 操作员控制台显示
            Console.WriteLine("Souscripteur [{0}] : la source [{1}] a signalé une température trop haute : [{2}]", Nom, ((Emetteur)source).Nom, e.Temperature);
        }
    }
}
  • 第 6 行:每个订阅者都通过一个名称进行标识。
  • 第 9-12 行:将与事件 TemperatureTropHaute 关联的方法。 该方法具有 delegate EventHandler<TEventArgs> 类型的签名,这是事件处理程序必须具备的。 该方法在控制台上显示:显示该消息的订阅者名称、报告该事件的发布者名称,以及触发该事件的温度。
  • 对象 Emetteur 并未在类 Souscripteur 中订阅事件 TemperatureTropHaute。该订阅将由一个外部类完成。

程序 [Program.cs] 将所有这些元素相互关联:


using System;
namespace Chap6 {
    class Program {
        static void Main(string[] args) {
            // 创建事件发送器
            Emetteur e1 = new Emetteur() { Nom = "e" };
            // 创建包含 2 个订阅者的数组
            Souscripteur[] souscripteurs = new Souscripteur[2];
            for (int i = 0; i < souscripteurs.Length; i++) {
                // 创建订阅者
                souscripteurs[i] = new Souscripteur() { Nom = "s" + i };
                // 将其订阅来自 e1 的事件 TemperatureTropHaute
                e1.TemperatureTropHaute += souscripteurs[i].EvtTemperatureTropHaute;
            }
            // 通过键盘读取温度
            decimal temperature;
            Console.Write("Température (rien pour arrêter) : ");
            string saisie = Console.ReadLine().Trim();
            // 只要输入的行不为空
            while (saisie != "") {
                // 输入内容是否为十进制数?
                if (decimal.TryParse(saisie, out temperature)) {
                    // 温度正确 - 将其记录
                    e1.Temperature = temperature;
                } else {
                    // 报告错误
                    Console.WriteLine("Température incorrecte");
                }
                // 重新输入
                Console.Write("Température (rien pour arrêter) : ");
                saisie = Console.ReadLine().Trim();
            }//while
        }
    }
}
  • 第 6 行:创建发布者
  • 第 8-14 行:创建两个订阅者,并将其订阅发布者 TemperatureTropHaute 的事件。
  • 第 20-32 行:通过键盘输入温度的循环
  • 第 24 行:如果输入的温度正确,则将其传输至对象 Emetteur e1;若温度高于 19 °C,该对象将触发事件 TemperatureTropHaute

执行结果如下:

1
2
3
4
Température (rien pour arrêter) : 17
Température (rien pour arrêter) : 21
Souscripteur [s0] : la source [e] a signalé une température trop haute : [21]
Souscripteur [s1] : la source [e] a signalé une température trop haute : [21]