事件和委托
事件是对象发送的消息,以发信号通知操作的发生。操作可能是由用户交互(例如鼠标单击)引起的,也可能是由某些其他的程序逻辑触发的。引发事件的对象称为事件发送方。捕获事件并对其作出响应的对象叫做事件接收方。
在事件通信中,事件发送方类不知道哪个对象或方法将接收到(处理)它引发的事件。所需要的是在源和接收方之间存在一个媒介(或类似指针的机制)。.NET Framework 定义了一个特殊的类型 (Delegate),该类型提供函数指针的功能。
委托是可保存对方法的引用的类。与其他的类不同,委托类具有一个签名,并且它只能对与其签名匹配的方法进行引用。这样,委托就等效于一个类型安全函数指针或一个回调。虽然委托具有许多其他的用途,但这里只讨论委托的事件处理功能。一个委托声明足以定义一个委托类。声明提供委托的签名,公共语言运行库提供实现。下面的示例显示了事件委托声明。
public class WakeMeUp { // AlarmRang has the same signature as AlarmEventHandler. public void AlarmRang(object sender, AlarmEventArgs e) {...}; ... }
只有当事件生成事件数据时才需要自定义事件委托。许多事件,包括一些用户界面事件(例如鼠标单击)在内,都不生成事件数据。在这种情况下,类库中为无数据事件提供的事件委托 System.EventHandler 便足够了。其声明如下。
delegate void EventHandler(object sender, EventArgs e);
事件委托是多路广播的,这意味着它们可以对多个事件处理方法进行引用。有关详细信息,请参见 Delegate。委托考虑了事件处理中的灵活性和精确控制。通过维护事件的已注册事件处理程序列表,委托为引发事件的类担当事件发送器的角色。
如何:将事件处理程序方法连接到事件
若要使用在另一个类中定义的事件,必须定义和注册一个事件处理程序。事件处理程序必须具有与为事件声明的委托相同的方法签名。通过向事件添加事件处理程序可注册该处理程序。向事件添加事件处理程序后,每当该类引发该事件时都会调用该方法。
有关阐释引发和处理事件的完整示例,请参见如何:引发和使用事件。
为事件添加事件处理程序方法
- 定义一个具有与事件委托相同的签名的事件处理程序方法。
public class WakeMeUp { // AlarmRang has the same signature as AlarmEventHandler. public void AlarmRang(object sender, AlarmEventArgs e) {...}; ... }
- 使用对该事件处理程序方法的一个引用创建委托的一个实例。调用该委托实例时,该实例会接着调用该事件处理程序方法。
// Create an instance of WakeMeUp. WakeMeUp w = new WakeMeUp(); // Instantiate the event delegate. AlarmEventHandler alhandler = new AlarmEventHandler(w.AlarmRang);
- 将该委托实例添加到事件。引发该事件时,就会调用该委托实例及其关联的事件处理程序方法。
// Instantiate the event source. AlarmClock clock = new AlarmClock(); // Add the delegate instance to the event. clock.Alarm += alhandler;
使用事件
要在应用程序中使用事件,您必须提供一个事件处理程序(事件处理方法),该处理程序执行程序逻辑以响应事件并向事件源注册事件处理程序。我们将该过程叫做事件连结。Windows 窗体和 Web 窗体的可视设计器所提供的应用程序快速开发 (RAD) 工具简化(或者说隐藏)了事件连结的详细信息。
本主题介绍处理事件的常规模式。有关 .NET Framework 中事件模型的概述,请参见事件和委托。有关 Windows 窗体中事件模型的更多信息,请参见如何:在 Windows 窗体应用程序中使用事件。有关 Web 窗体中事件模型的更多信息,请参见如何:在 Web 窗体应用程序中使用事件。
事件模式
由于不同 RAD 工具提供不同级别的支持,所以 Windows 窗体和 Web 窗体中的事件连接详细信息是不同的。不过,两个方案遵循同一事件模式,该模式具有下列特征:
- 引发名为 EventName 事件的类具有以下成员:
public event EventNameEventHandler EventName;
- EventName 事件的事件委托是 EventNameEventHandler,它具有以下签名:
public delegate void EventNameEventHandler(object sender, EventNameEventArgs e);
要使用 EventName 事件,您的事件处理程序必须与事件委托具有相同签名:
void EventHandler(object sender, EventNameEventArgs e) {}
如果事件没有任何关联数据,则引发事件的类使用 System.EventHandler 作为委托,并将 System.EventArgs 作为事件数据。具有关联数据的事件使用从事件数据类型的 EventArgs 派生的类以及相应的事件委托类型。例如,如果您要处理 Windows 窗体应用程序中的 MouseUp 事件,则事件数据类是 MouseEventArgs,而事件委托是 MouseEventHandler。请注意,某些鼠标事件使用事件数据的公共类和公共事件委托,因此命名方案与上面所述的约定不完全匹配。对于鼠标事件,事件处理程序必须具有以下签名:
void Mouse_Moved(object sender, MouseEventArgs e){}
发送方和事件变量参数向事件处理程序提供有关鼠标事件的详细信息。发送方对象指示引发事件的对象。MouseEventArgs 参数提供有关引发事件的鼠标移动的详细信息。许多事件源提供有关事件的其他数据,且许多事件处理程序在处理事件时使用事件特定的数据。有关阐释如何引发和处理具有事件特定数据的事件的示例,请参见如何:引发和使用事件。
静态事件和动态事件
.NET Framework 允许订户为获得事件通知而进行静态或动态注册。静态事件处理程序在其所处理的事件所属类的整个生存期内有效。这是处理事件的最常用方法。动态事件处理程序在程序执行期间显式激活和停用,通常是为了响应某些条件程序逻辑。例如,如果只在特定条件下才需要事件通知,或者如果应用程序提供了多个事件处理程序,并由运行时条件来确定要使用哪个事件处理程序,则可以使用动态事件处理程序。
EventInfo.AddEventHandler 方法添加动态事件处理程序,而 EventInfo.RemoveEventHandler 方法停用这些事件处理程序。各种语言还提供各自的用于动态处理事件的功能。下面的示例定义一个TemperatureMonitor 类,每当温度达到预定义的阈值时该类将引发 TemperatureTheshold 事件。随后,在程序执行期间将激活和停用订阅此事件的事件处理程序。
using System; public class TemperatureEventArgs : EventArgs { private decimal oldTemp; private decimal newTemp; public decimal OldTemperature { get { return this.oldTemp; } } public decimal NewTemperature { get { return this.newTemp; } } public TemperatureEventArgs(decimal oldTemp, decimal newTemp) { this.oldTemp = oldTemp; this.newTemp = newTemp; } } public delegate void TemperatureEventHandler(object sender, TemperatureEventArgs ev); public class TemperatureMonitor { private decimal currentTemperature; private decimal threshholdTemperature; public event TemperatureEventHandler TemperatureThreshold; public TemperatureMonitor(decimal threshhold) { this.threshholdTemperature = threshhold; } public void SetTemperature(decimal newTemperature) { if ( (this.currentTemperature > this.threshholdTemperature && newTemperature <= this.threshholdTemperature) || (this.currentTemperature < this.threshholdTemperature && newTemperature >= this.threshholdTemperature) ) OnRaiseTemperatureEvent(newTemperature); this.currentTemperature = newTemperature; } public decimal GetTemperature() { return this.currentTemperature; } protected virtual void OnRaiseTemperatureEvent(decimal newTemperature) { // Raise the event if it has subscribers. if (TemperatureThreshold != null) TemperatureThreshold(this, new TemperatureEventArgs(this.currentTemperature, newTemperature)); } } public class Example { public static void Main() { Example ex = new Example(); ex.MonitorTemperatures(); } public void MonitorTemperatures() { TemperatureMonitor tempMon = new TemperatureMonitor(32); tempMon.SetTemperature(33); Console.WriteLine("Current temperature is {0} degrees Fahrenheit.", tempMon.GetTemperature()); tempMon.SetTemperature(32); Console.WriteLine("Current temperature is {0} degrees Fahrenheit.", tempMon.GetTemperature()); // Add event handler dynamically using C# syntax. tempMon.TemperatureThreshold += this.TempMonitor; tempMon.SetTemperature(33); Console.WriteLine("Current temperature is {0} degrees Fahrenheit.", tempMon.GetTemperature()); tempMon.SetTemperature(34); Console.WriteLine("Current temperature is {0} degrees Fahrenheit.", tempMon.GetTemperature()); tempMon.SetTemperature(32); Console.WriteLine("Current temperature is {0} degrees Fahrenheit.", tempMon.GetTemperature()); // Remove event handler dynamically using C# syntax. tempMon.TemperatureThreshold -= this.TempMonitor; tempMon.SetTemperature(31); Console.WriteLine("Current temperature is {0} degrees Fahrenheit.", tempMon.GetTemperature()); tempMon.SetTemperature(35); Console.WriteLine("Current temperature is {0} degrees Fahrenheit.", tempMon.GetTemperature()); } private void TempMonitor(object sender, TemperatureEventArgs e) { Console.WriteLine(" ***Warning: Temperature is changing from {0} to {1}.", e.OldTemperature, e.NewTemperature); } } // The example displays the following output: // Current temperature is 33 degrees Fahrenheit. // Current temperature is 32 degrees Fahrenheit. // Current temperature is 33 degrees Fahrenheit. // Current temperature is 34 degrees Fahrenheit. // ***Warning: Temperature is changing from 34 to 32. // Current temperature is 32 degrees Fahrenheit. // Current temperature is 31 degrees Fahrenheit. // Current temperature is 35 degrees Fahrenheit.
引发事件
事件功能是由三个互相联系的元素提供的:提供事件数据的类、事件委托和引发事件的类。.NET Framework 具有命名与事件相关的类和方法的约定。如果希望您的类引发一个名为 EventName 的事件,您需要以下元素:
- 包含事件数据的类,名为 EventNameEventArgs。此类必须是从 System.EventArgs 派生的。
- 事件的委托,名为 EventNameEventHandler。
- 引发事件的类。该类必须提供事件声明 (EventName) 和引发事件 (OnEventName) 的方法。
.NET Framework 类库或第三方类库中可能已经定义了事件数据类和事件委托类。在这种情况下,您就不需要定义这些类了。例如,如果您的事件不使用自定义数据,您可以使用 System.EventArgs 作为事件数据并使用System.EventHandler 作为委托。
使用 event 关键字在类中定义事件成员。当编译器在类中遇到 event 关键字时,它会创建一个私有成员,例如:
private EventNameHandler eh = null;
编译器还会创建两个公共方法,即 add_EventName 和 remove_EventName。这些方法是事件挂钩,它们允许委托与事件委托 eh 合并或从该事件委托中移除。这些详细信息对程序员是隐藏的。
定义事件实现后,您必须确定引发事件的时间。通过在定义事件的类或派生类中调用受保护的 OnEventName 方法来引发事件。OnEventName 方法通过调用委托,传入所有事件特定的数据来引发事件。事件的委托方法可以执行事件操作或处理事件特定的数据。
受保护的 OnEventName 方法也允许派生类重写事件,而不必向其附加委托。派生类必须始终调用基类的 OnEventName 方法以确保注册的委托接收到事件。
如果希望处理另一个类中引发的事件,请向事件中添加委托方法。如果您不熟悉 .NET Framework 中事件的委托模型,请参见事件和委托。
如何:在类中实现事件
下面的过程说明如何在类中实现事件。第一个过程实现没有关联数据的事件,它将 System.EventArgs 类和 System.EventHandler 类用作事件数据和委托处理程序。第二个过程实现具有自定义数据的事件,它为事件数据和事件委托处理程序定义自定义类。
有关阐释引发和处理事件的完整示例,请参见如何:引发和使用事件。
实现不具有事件特定的数据的事件
- 在类中定义公共事件成员。将事件成员的类型设置为 System.EventHandler 委托。
public class Countdown { ... public event EventHandler CountdownCompleted; }
- 在引发事件的类中提供一个受保护的方法。对 OnEventName 方法进行命名。在该方法中引发该事件。
public class Countdown { ... public event EventHandler CountdownCompleted; protected virtual void OnCountdownCompleted(EventArgs e) { if (CountdownCompleted != null) CountdownCompleted(this, e); } }
- 在类中确定引发该事件的时间。调用 OnEventName 以引发该事件。
public class Countdown { ... public void Decrement { internalCounter = internalCounter - 1; if (internalCounter == 0) OnCountdownCompleted(new EventArgs()); } }
实现具有事件特定的数据的事件
- 定义一个提供事件数据的类。对类 EventNameArgs 进行命名,从 System.EventArgs 派生该类,然后添加所有事件特定的成员。
public class AlarmEventArgs : EventArgs { private readonly int nrings = 0; private readonly bool snoozePressed = false; //Constructor. public AlarmEventArgs(bool snoozePressed, int nrings) { this.snoozePressed = snoozePressed; this.nrings = nrings; } //Properties. public string AlarmText { ... } public int NumRings { ... } public bool SnoozePressed{ ... } }
- 声明事件的委托。对委托 EventNameEventHandler 进行命名。
public delegate void AlarmEventHandler(object sender, AlarmEventArgs e);
- 在类中定义名为 EventName 的公共事件成员。将事件成员的类型设置为事件委托类型。
public class AlarmClock { ... public event AlarmEventHandler Alarm; }
- 在引发事件的类中定义一个受保护的方法。对 OnEventName 方法进行命名。在该方法中引发该事件。
public class AlarmClock { ... public event AlarmHandler Alarm; protected virtual void OnAlarm(AlarmEventArgs e) { if (Alarm != null) Alarm(this, e); } }
- 在类中确定引发该事件的时间。调用 OnEventName 以引发该事件并使用 EventNameEventArgs 传入事件特定的数据。
Public Class AlarmClock { ... public void Start() { ... System.Threading.Thread.Sleep(300); AlarmEventArgs e = new AlarmEventArgs(false, 0); OnAlarm(e); } }
如何:引发和使用事件
下面的示例程序阐释如何在一个类中引发一个事件,然后在另一个类中处理该事件。 AlarmClock 类定义公共事件 Alarm,并提供引发该事件的方法。 AlarmEventArgs 类派生自 EventArgs,并定义 Alarm 事件特定的数据。 WakeMeUp 类定义处理 Alarm 事件的 AlarmRang 方法。 AlarmDriver 类一起使用类,将使用 WakeMeUp 的 AlarmRang 方法设置为处理 AlarmClock 的 Alarm 事件。
该示例程序使用事件和委托和引发事件中详细说明的概念。
// EventSample.cs. // namespace EventSample { using System; using System.ComponentModel; // Class that contains the data for // the alarm event. Derives from System.EventArgs. // public class AlarmEventArgs : EventArgs { private readonly bool snoozePressed ; private readonly int nrings; //Constructor. // public AlarmEventArgs(bool snoozePressed, int nrings) { this.snoozePressed = snoozePressed; this.nrings = nrings; } // The NumRings property returns the number of rings // that the alarm clock has sounded when the alarm event // is generated. // public int NumRings { get { return nrings;} } // The SnoozePressed property indicates whether the snooze // button is pressed on the alarm when the alarm event is generated. // public bool SnoozePressed { get {return snoozePressed;} } // The AlarmText property that contains the wake-up message. // public string AlarmText { get { if (snoozePressed) { return ("Wake Up!!! Snooze time is over."); } else { return ("Wake Up!"); } } } } // Delegate declaration. // public delegate void AlarmEventHandler(object sender, AlarmEventArgs e); // The Alarm class that raises the alarm event. // public class AlarmClock { private bool snoozePressed = false; private int nrings = 0; private bool stop = false; // The Stop property indicates whether the // alarm should be turned off. // public bool Stop { get {return stop;} set {stop = value;} } // The SnoozePressed property indicates whether the snooze // button is pressed on the alarm when the alarm event is generated. // public bool SnoozePressed { get {return snoozePressed;} set {snoozePressed = value;} } // The event member that is of type AlarmEventHandler. // public event AlarmEventHandler Alarm; // The protected OnAlarm method raises the event by invoking // the delegates. The sender is always this, the current instance // of the class. // protected virtual void OnAlarm(AlarmEventArgs e) { AlarmEventHandler handler = Alarm; if (handler != null) { // Invokes the delegates. handler(this, e); } } // This alarm clock does not have // a user interface. // To simulate the alarm mechanism it has a loop // that raises the alarm event at every iteration // with a time delay of 300 milliseconds, // if snooze is not pressed. If snooze is pressed, // the time delay is 1000 milliseconds. // public void Start() { for (;;) { nrings++; if (stop) { break; } else if (snoozePressed) { System.Threading.Thread.Sleep(1000); { AlarmEventArgs e = new AlarmEventArgs(snoozePressed, nrings); OnAlarm(e); } } else { System.Threading.Thread.Sleep(300); AlarmEventArgs e = new AlarmEventArgs(snoozePressed, nrings); OnAlarm(e); } } } } // The WakeMeUp class has a method AlarmRang that handles the // alarm event. // public class WakeMeUp { public void AlarmRang(object sender, AlarmEventArgs e) { Console.WriteLine(e.AlarmText +"\n"); if (!(e.SnoozePressed)) { if (e.NumRings % 10 == 0) { Console.WriteLine(" Let alarm ring? Enter Y"); Console.WriteLine(" Press Snooze? Enter N"); Console.WriteLine(" Stop Alarm? Enter Q"); String input = Console.ReadLine(); if (input.Equals("Y") ||input.Equals("y")) return; else if (input.Equals("N") || input.Equals("n")) { ((AlarmClock)sender).SnoozePressed = true; return; } else { ((AlarmClock)sender).Stop = true; return; } } } else { Console.WriteLine(" Let alarm ring? Enter Y"); Console.WriteLine(" Stop Alarm? Enter Q"); String input = Console.ReadLine(); if (input.Equals("Y") || input.Equals("y")) return; else { ((AlarmClock)sender).Stop = true; return; } } } } // The driver class that hooks up the event handling method of // WakeMeUp to the alarm event of an Alarm object using a delegate. // In a forms-based application, the driver class is the // form. // public class AlarmDriver { public static void Main (string[] args) { // Instantiates the event receiver. WakeMeUp w= new WakeMeUp(); // Instantiates the event source. AlarmClock clock = new AlarmClock(); // Wires the AlarmRang method to the Alarm event. clock.Alarm += new AlarmEventHandler(w.AlarmRang); clock.Start(); } } }
引发多个事件
如果您的类引发多个事件,并且您按引发事件中的说明对这些事件进行编程,编译器将为每个事件委托实例生成一个字段。如果事件的数目很大,则一个委托一个字段的存储成本可能无法接受。对于这些情况,.NET Framework 提供一个称为事件属性的构造(Visual Basic 2005 中的自定义事件),此构造可以和(您选择的)另一数据结构一起用于存储事件委托。
事件属性由带事件访问器的事件声明组成。事件访问器是您定义的方法,用以允许事件委托实例添加到存储数据结构或从存储数据结构移除。请注意,事件属性要比事件字段慢,这是因为必须先检索每个事件委托,然后才能调用它。这是内存和速度之间的折中方案。如果您的类定义了许多不常引发的事件,那么您可能要实现事件属性。Windows 窗体控件和 ASP.NET 服务器控件使用事件属性而不是事件字段。
如何:使用事件属性处理多个事件
要使用事件属性(Visual Basic 2005 中的自定义事件),请在引发事件的类中定义事件属性,然后在处理事件的类中设置事件属性的委托。要在一个类中实现多个事件属性,该类必须在内部存储和维护为每个事件定义的委托。一种典型方法是实现通过事件键进行索引的委托集合。
若要存储每个事件的委托,可以使用 EventHandlerList 类或实现您自己的集合。集合类必须提供用于基于事件键设置、访问和检索事件处理程序委托的方法。例如,可以使用 Hashtable 类或从 DictionaryBase 类派生一个自定义类。不需要在类以外公开委托集合的实现详细信息。
类中的每个事件属性定义一个 add 访问器方法和一个 remove 访问器方法。事件属性的 add 访问器将输入委托实例添加到委托集合。事件属性的 remove 访问器从委托集合中移除输入委托实例。事件属性访问器使用事件属性的预定义键在委托集合中添加和从委托集合中移除实例。
使用事件属性处理多个事件
- 在引发事件的类中定义一个委托集合。
- 定义每个事件的键。
- 在引发事件的类中定义事件属性。
- 使用委托集合实现事件属性的 add 访问器方法和 remove 访问器方法。
- 使用公共事件属性可在处理事件的类中添加和移除事件处理程序委托。
// The class SampleControl defines two event properties, MouseUp and MouseDown. class SampleControl: Component { // : // Define other control methods and properties. // : // Define the delegate collection. protected EventHandlerList listEventDelegates = new EventHandlerList(); // Define a unique key for each event. static readonly object mouseDownEventKey = new object(); static readonly object mouseUpEventKey = new object(); // Define the MouseDown event property. public event MouseEventHandler MouseDown { // Add the input delegate to the collection. add { listEventDelegates.AddHandler(mouseDownEventKey, value); } // Remove the input delegate from the collection. remove { listEventDelegates.RemoveHandler(mouseDownEventKey, value); } } // Define the MouseUp event property. public event MouseEventHandler MouseUp { // Add the input delegate to the collection. add { listEventDelegates.AddHandler(mouseUpEventKey, value); } // Remove the input delegate from the collection. remove { listEventDelegates.RemoveHandler(mouseUpEventKey, value); } } }