Skip to content

8. 用户事件

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

8.1. 预定义的委托对象

我们在上一章中已经接触过对象委托的概念,但当时只是略过不谈。在探讨表单组件的事件处理程序如何声明时,我们曾遇到过类似以下的代码:


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

其中 buttonAfficher 是一个 [Button] 组件。该类定义了如下 Click 事件:

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

访问委托 EventHandler 的定义如下:

 

Visit 委托的 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);

实际上,委托类型的变量是一组指向该委托所对应方法的引用列表。要将新方法 M 添加到上面的变量 evtHandler 中,我们使用以下语法:

evtHandler+=new EvtHandler(M);

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

说明:


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

将一个事件处理程序添加到 buttonAfficher.Click 事件方法列表中。当组件 buttonAfficher 发生 Click 事件时,VB 将执行:

            buttonAfficher.Click(source, evt);

其中:

  • source 是事件背后的对象
  • 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);

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

类型为 Operation 的变量 op 将注册一组与原型 Operation 对应的函数:

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

将方法 fi 注册到变量 op 中,可通过 op=new Operation(fi) 或简写为 op=fi 实现。要将方法 fj 添加到已注册函数列表中,我们写 op+= fj。要移除方法 fk,我们写 op-=fk。如果在本例中,我们写 n=op(n1,n2),则 op 中存储的方法集合将使用参数 n1n2 执行。 最终获得的结果 n 将是最后执行的方法所返回的值。无法获取所有方法产生的结果。因此,若将方法列表存储在委托函数中,这些函数通常会返回 void 类型的结果。

请看以下示例:


using System;
namespace Chap6 {
    class Class1 {
         // function prototype definition
         // accepts 2 integers as parameters and returns an integer
        public delegate int Opération(int n1, int n2);
 
         // two instance methods corresponding to the prototype
        public int Ajouter(int n1, int n2) {
            Console.WriteLine("Ajouter(" + n1 + "," + n2 + ")");
            return n1 + n2;
         }//add
 
        public int Soustraire(int n1, int n2) {
            Console.WriteLine("Soustraire(" + n1 + "," + n2 + ")");
            return n1 - n2;
         }//subtract
 
         // a static method corresponding to the prototype
        public static int Augmenter(int n1, int n2) {
            Console.WriteLine("Augmenter(" + n1 + "," + n2 + ")");
            return n1 + 2 * n2;
         }//increase
 
        static void Main(string[] args) {
 
             // define an operation object to store functions
             // we register the static function increase
            Opération op = Augmenter;
             // the delegate is executed
            int n = op(4, 7);
            Console.WriteLine("n=" + n);
 
             // creation of a c1 object of type class1
            Class1 c1 = new Class1();
             // we register c1's add method in the delegate
            op = c1.Ajouter;
             // execution of delegated object
            n = op(2, 3);
            Console.WriteLine("n=" + n);
             // the subtract method of c1 is registered in the delegate
            op = c1.Soustraire;
            n = op(2, 3);
            Console.WriteLine("n=" + n);
             //registration of two functions in the delegate
            op = c1.Ajouter;
            op += c1.Soustraire;
             // execution of delegated object
            op(0, 0);
             // remove a function from the delegate
            op -= c1.Soustraire;
             // the delegate is executed
            op(1, 1);
        }
    }
}
  • 第 3 行:定义了一个名为 Class1 的类。
  • 第 6 行:定义委托 Operation:一个方法原型,接受两个 int 类型的参数并返回一个 int 类型
  • 第 9-12 行:将实例方法 Add 添加到委托 Operation 的签名中。
  • 第 14-17 行:将实例方法 subtract 添加到委托 Operation 的签名中。
  • 第 20-23 行:将方法 Ancrease 添加到委托 Operation 的签名中。
  • 第 25 行:执行了 Main 方法
  • 第 20 行:变量 op 的类型为 delegate Operation。它将包含一组具有 delegate Operation 类型签名的方法它被赋予了第一个方法引用,即静态方法 Class1.Augmenter 的引用。
  • 第 31 行:执行委托 opop 引用的所有方法都将被执行。它们将使用传递给委托 op 的参数进行执行在此处,仅会执行静态方法 Class1.Augmenter
  • 第 35 行:创建 Class1 类的实例 c1
  • 第 37 行:将实例方法 c1.Ajouter 赋值给委托 opAugmenter 是静态方法,而 Add 是实例方法。我们想说明这并不重要。
  • 第 39 行:执行委托 opAdd 方法将使用传递给委托 op 的参数进行执行。
  • 第 42 行:对实例方法 Subtract 执行相同操作。
  • 第 46-47 行:委托 op 中的 Add Subtract 方法。
  • 第 49 行:执行委托 opAdd Subtract 都将使用传递给委托 op 的参数被执行。
  • 第 51 行:从委托操作中移除 Subtract 方法。
  • 第 53 行:执行委托操作:剩余的 Add 方法将被执行。

结果如下:

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. 委托还是接口?

委托和接口的概念看似相当相似,我们可能会好奇这两者究竟有何区别。让我们来看一个与我们之前研究过的例子相似的示例:


using System;
namespace Chap6 {
    class Program1 {
         // function prototype definition
         // accepts 2 integers as parameters and returns an integer
        public delegate int Opération(int n1, int n2);
 
         // two instance methods corresponding to the prototype
        public static int Ajouter(int n1, int n2) {
            Console.WriteLine("Ajouter(" + n1 + "," + n2 + ")");
            return n1 + n2;
         }//add
 
        public static int Soustraire(int n1, int n2) {
            Console.WriteLine("Soustraire(" + n1 + "," + n2 + ")");
            return n1 - n2;
         }//subtract
 
         // Executing a delegate
        public static int Execute(Opération op, int n1, int n2){
            return op(n1, n2);
        }
 
        static void Main(string[] args) {
             // delegate execution Add
            Console.WriteLine(Execute(Ajouter, 2, 3));
             // delegate execution Subtract
            Console.WriteLine(Execute(Soustraire, 2, 3));
             // executing a multicast delegate
            Opération op = Ajouter;
            op += Soustraire;
            Console.WriteLine(Execute(op, 2, 3));
             // remove a function from the delegate
            op -= Soustraire;
             // the delegate is executed
            Console.WriteLine(Execute(op, 2, 3));
        }
    }
}

第 20 行,Execute 方法期望接收一个对第 6 行定义的 Operation 委托类型对象的引用。这会切换到 Execute 方法的不同实现(第 26、28、32 和 36 行)。这种多态特性也可以通过 : 来实现:


using System;
 
namespace Chap6 {
 
     // interface IOperation
    public interface IOperation {
        int operation(int n1, int n2);
    }
 
     // class Add
    public class Ajouter : IOperation {
        public int operation(int n1, int n2) {
            Console.WriteLine("Ajouter(" + n1 + "," + n2 + ")");
            return n1 + n2;
        }
    }
 
     // class Subtract
    public class Soustraire : IOperation {
        public int operation(int n1, int n2) {
            Console.WriteLine("Soustraire(" + n1 + "," + n2 + ")");
            return n1 - n2;
        }
    }
 
     // test class
    public static class Program2 {
         // Executing the single method of the IOperation interface
        public static int Execute(IOperation op, int n1, int n2) {
            return op.operation(n1, n2);
        }
 
        public static void Main() {
             // delegate execution Add
            Console.WriteLine(Execute(new Ajouter(), 2, 3));
             // delegate execution Subtract
            Console.WriteLine(Execute(new Soustraire(), 2, 3));
        }
    }
}
  • 第 6-8 行:接口 [IOperation] 定义了一个 operation 方法。
  • 第 11-16 行和第 19-24 行:[Add] 和 [Subtract] 类实现了 [IOperation] 接口。
  • 第 29-31 行:方法 Execute 的第一个参数是接口类型 IOperation。方法 Execute 将依次接收 Add 类的实例和 Subtract 类的实例作为第一个参数。

前一个示例中委托的多态性。同时,这两个示例也展示了这两个概念之间的差异。

当接口仅包含一个方法时,委托接口可以互换使用

  • (前提是接口仅包含一个方法)。实际上,委托是单个方法的封装,而接口可以定义多个方法。
  • 如果未使用委托的多播特性。接口中并不存在多播的概念。

如果满足这两个条件,那么我们可以为 Execute 方法选择以下两种签名之一:


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

第二种方法使用委托,因此使用起来更加灵活。在第一种签名中,方法的第一个参数必须实现 IOperation 接口。这要求创建一个类来定义将作为 Execute 方法第一个参数传递的方法。而在第二种签名中,任何具有正确签名的现有方法均可使用,无需额外构建。

8.4. 事件管理

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

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

在此场景中,我们有:

  • 一个触发事件的类
  • 接收该事件通知的类。这些类被称为订阅了该事件。
  • 一个委托类型,用于定义将接收该事件通知的方法的签名

.NET 框架定义了:

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

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

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

.NET 框架提供了一个满足事件委托签名的泛型方法:

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

采用这种泛型委托后,类 C 中事件 X 的声明应遵循以下推荐方案:

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

用于将事件 X “发布”给订阅者。

请看以下示例:

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

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


using System;
 
namespace Chap6 {
    public class TemperatureTropHauteEventArgs:EventArgs {
         // temperature during evt
        public decimal Temperature { get; set; }
         // manufacturers
        public TemperatureTropHauteEventArgs() {
        }
        public TemperatureTropHauteEventArgs(decimal temperature) {
            Temperature = temperature;
        }
    }
}
  • 第 6 行:由 TemperatureTropHauteEventArgs 封装的信息是触发 TemperatureTropHaute 事件的温度。

Transmitter 类定义如下:


using System;
 
namespace Chap6 {
    public class Emetteur {
        static decimal SEUIL = 19;
 
         // observed temperature
        private decimal temperature;
         // name of source
        public string Nom { get; set; }
         // event reported
        public event EventHandler<TemperatureTropHauteEventArgs> TemperatureTropHaute;
 
         // read/write temperature
        public decimal Temperature {
            get {
                return temperature;
            }
            set {
                temperature = value;
                if (temperature > SEUIL) {
                     // subscribers are notified of the event
                    OnTemperatureTropHaute(new TemperatureTropHauteEventArgs(temperature));
                }
            }
        }
 
         // reporting an event
        protected virtual void OnTemperatureTropHaute(TemperatureTropHauteEventArgs evt) {
             // issue of event TemperatureTropHaute to subscribers
            TemperatureTropHaute(this, evt);
        }
    }
}
  • 第 5 行:温度阈值,超过该阈值时将发布 TemperatureTropHaute 事件。
  • 第 10 行:发送者有一个用于识别的名称
  • 第 12 行:TemperatureTropHaute 事件。
  • 第 15-26 行:get 方法用于获取温度,set 方法用于记录温度。当待记录的温度超过第 5 行设定的阈值时,set 方法将发布 TemperatureTropHaute 事件。它通过传递一个记录了超过阈值温度的 TemperatureTropHauteEventArgs 对象,使用第 29 行定义的 OnTemperatureTropHauteHandler 发布该事件。
  • 第 29-32 行:发布 TemperatureTropHaute 事件,将事件发送者本身作为第一个参数,并将接收到的 TemperatureTropHauteEventArgs 对象作为参数。

将订阅 TemperatureTropHaute 事件的 Underwriter 类如下所示:


using System;
 
namespace Chap6 {
    public class Souscripteur {
         // name
        public string Nom { get; set; }
 
         // event manager TemperatureTropHaute
        public void EvtTemperatureTropHaute(object source, TemperatureTropHauteEventArgs e) {
             // operator console display
            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 事件关联的方法。该方法的签名是 EventHandler<TEventArgs> 类型,这是事件管理器必须具备的类型。该方法会在控制台显示:显示该消息的订阅者名称、报告该事件的发送者名称,以及触发该事件的温度。
  • Transmitter 对象对 TemperatureTropHaute 事件的订阅并非在 Underwriter 类中完成,而是由一个外部类来处理。

[Program.cs] 程序将所有这些元素整合在一起:


using System;
namespace Chap6 {
    class Program {
        static void Main(string[] args) {
             // creation of an evts transmitter
            Emetteur e1 = new Emetteur() { Nom = "e" };
             // creation of a table of 2 subscribers
            Souscripteur[] souscripteurs = new Souscripteur[2];
            for (int i = 0; i < souscripteurs.Length; i++) {
                 // creation subscriber
                souscripteurs[i] = new Souscripteur() { Nom = "s" + i };
                 // we subscribe him to e1's TemperatureTropHaute event
                e1.TemperatureTropHaute += souscripteurs[i].EvtTemperatureTropHaute;
            }
             // temperatures are read from the keyboard
            decimal temperature;
            Console.Write("Température (rien pour arrêter) : ");
            string saisie = Console.ReadLine().Trim();
             // as long as the line entered is not empty
            while (saisie != "") {
                 // is the input a decimal number?
                if (decimal.TryParse(saisie, out temperature)) {
                     // correct temperature - recorded
                    e1.Temperature = temperature;
                } else {
                    // on signale l'erreur
                    Console.WriteLine("Température incorrecte");
                }
                 // new entry
                Console.Write("Température (rien pour arrêter) : ");
                saisie = Console.ReadLine().Trim();
             }//while
        }
    }
}
  • 第 6 行:创建发送器
  • 第8-14行:创建两个订阅器,用于订阅发布器的 TemperatureTropHaute 事件。
  • 第20-32行:键盘温度输入循环
  • 第 24 行:如果输入的温度正确,则将其传输至 Transmitter 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]