类或对象可以通过事件向其他类或对象通知发生的事情。引发事件的类称为“发行者”,接收(或处理)事件的类称为“订户”。
事件具有以下特点:
-
发行者决定何时引发事件,订户决定执行什么操作来响应该事件。
-
一个事件可以有多个订户。 一个订户也可以处理来自多个发行者的多个事件。
-
没有订户的事件永远不会引发。
-
事件通常用于响应用户操作,例如,图形用户界面中单击按钮或选择菜单操作。
-
如果一个事件有多个订户,当引发该事件时,会同步调用多个事件处理程序。 对异步调用事件,可参考使用异步方式调用同步方法。
-
在 .NET Framework 类库中,事件全部基于 EventHandler 委托和 EventArgs 基类。
订阅和取消订阅事件
使用 Visual Studio IDE 订阅事件
-
打开属性窗口:通过View窗口打开,或右键单击要为其创建事件处理程序的窗体或控件,选择“属性”。
-
在“属性”窗口的顶部,单击“事件”图标。
-
双击要创建的事件,例如 Load 事件。
此时可以在代码中看到Visual C# 创建的一个空的事件处理方法。可以自己手动在其中添加事件处理代码。下面的代码声明了一个事件处理方法,在 Form 类引发 Load 事件时调用。
private void Form1_Load(object sender, System.EventArgs e) { // Add your form load event handling code here. }
VS会同时在项目的 Form1.Designer.cs 文件中的 InitializeComponent 方法中为组件添加订阅该事件所需的代码行。 该代码行类似于:
this.Load += new System.EventHandler(this.Form1_Load);
以编程方式订阅事件
-
首先定义一个事件处理方法,其签名与该事件的委托签名匹配。 例如,如果事件是基于 EventHandler 委托类型的,则事件处理方法可以如下表示:
void HandleCustomEvent(object sender, CustomEventArgs a) { // Do something useful here. }
-
使用 (+=) 运算符为事件添加事件处理程序。如下例所示,假设 publisher 对象拥有一个 RaiseCustomEvent 事件。 请注意,订户类需要引用发行者类才能订阅其事件。
publisher.RaiseCustomEvent += HandleCustomEvent;
上面的语法是 C# 2.0 中的新语法。 该语法完全等效于使用 new 关键字显式创建封装委托的 C# 1.0 语法:
publisher.RaiseCustomEvent += new CustomEventHandler(HandleCustomEvent);
还可以通过 lambda 表达式添加事件处理程序:
public Form1() { InitializeComponent(); // Use a lambda expression to define an event handler. this.Click += (s,e) => { MessageBox.Show( ((MouseEventArgs)e).Location.ToString());}; }
使用匿名方法订阅事件
-
如果某个事件只用一次,则可以使用 (+=) 运算符将匿名方法添加到此事件。 在下例中,publisher 对象拥有一个名为 RaiseCustomEvent 的事件,并且还定义了一个 CustomEventArgs 类以承载某些类型的专用事件信息。
publisher.RaiseCustomEvent += delegate(object o, CustomEventArgs e) { string s = o.ToString() + " " + e.ToString(); Console.WriteLine(s); };
注意:如果使用匿名函数订阅事件,事件的取消订阅过程将比较麻烦。 在这种情况下若要取消订阅,必须返回到该事件的订阅代码,将该匿名方法存储在委托变量中,然后将此委托添加到该事件中。 一般来说,如果需要在后面的代码中取消订阅某事件,则建议不要使用匿名函数订阅此事件。
取消订阅
要阻止引发事件时调用某事件处理程序,则需要取消订阅该事件。为防止资源泄露,在释放订户对象之前也需要取消订阅事件。 在取消订阅事件之前,发布对象中该事件的多路广播委托会引用封装了订户的事件处理程序的委托。 只要发布对象保持该引用,垃圾回收功能就不会删除该订户对象。
取消订阅事件
-
使用减法赋值运算符 (-=) 取消订阅事件:
publisher.RaiseCustomEvent -= HandleCustomEvent;
所有订户都取消订阅事件后,发行者类中的事件实例将被设置为 null。
如何发布事件
下面演示如何正确的添加事件。.NET Framework类库中的所有事件都是基于 EventHandler委托,其定义如下:
public delegate void EventHandler(object sender, EventArgs e);
虽然事件定义可以基于任何有效的委托类型,甚至可以是可返回值的委托,但通常建议采用基于EventHandler的.NET框架模式。
采用EventHandler模式发布事件
-
(如果不需要与事件一起发送自定义数据,可跳到步骤 3a)在发行者类和订阅方类均可看见的范围中声明自定义数据类,添加自定义事件数据所需的成员。 如下,会返回一个简单字符串。
public class CustomEventArgs : EventArgs { public CustomEventArgs(string s) { msg = s; } private string msg; public string Message { get { return msg; } } }
-
(如果使用泛型版本的 EventHandler<TEventArgs>,跳过此步)在发布类中声明一个委托。 以 EventHandler 结尾为其命名。 第二个参数为自定义的 EventArgs 类型。
public delegate void CustomEventHandler(object sender, CustomEventArgs a);
-
使用下面任一步骤,在发布类中声明事件。
-
如果没有自定义 EventArgs 类,事件类型就是非泛型 EventHandler 委托。此时无需声明委托,因为它已在创建 C# 项目时包含的 System 命名空间中进行了声明。 将以下代码添加到发行者类中。
public event EventHandler RaiseCustomEvent;
-
如果使用的是非泛型版本的 EventHandler,并且自定义了 EventArgs 派生类,则在发布类中声明定义的事件,并且步骤 2中定义的 的委托作为类型。
public event CustomEventHandler RaiseCustomEvent;
-
如果使用的是泛型版本,则不需要自定义委托。在发行者类中,将事件类型指定为 EventHandler<CustomEventArgs>。
public event EventHandler<CustomEventArgs> RaiseCustomEvent;
-
示例
下面的示例使用自定义的EventArgs和EventHandler<TEventArgs>作为事件类型来演示上面的过程:
namespace DotNetEvents { using System; using System.Collections.Generic; // Define a class to hold custom event info public class CustomEventArgs : EventArgs { public CustomEventArgs(string s) { message = s; } private string message; public string Message { get { return message; } set { message = value; } } } // Class that publishes an event class Publisher { // Declare the event using EventHandler<T> public event EventHandler<CustomEventArgs> RaiseCustomEvent; public void DoSomething() { // Write some code that does something useful here // then raise the event. You can also raise an event // before you execute a block of code. OnRaiseCustomEvent(new CustomEventArgs("Did something")); } // Wrap event invocations inside a protected virtual method // to allow derived classes to override the event invocation behavior protected virtual void OnRaiseCustomEvent(CustomEventArgs e) { // Make a temporary copy of the event to avoid possibility of // a race condition if the last subscriber unsubscribes // immediately after the null check and before the event is raised. EventHandler<CustomEventArgs> handler = RaiseCustomEvent; // Event will be null if there are no subscribers if (handler != null) { // Format the string to send inside the CustomEventArgs parameter e.Message += String.Format(" at {0}", DateTime.Now.ToString()); // Use the () operator to raise the event. handler(this, e); } } } //Class that subscribes to an event class Subscriber { private string id; public Subscriber(string ID, Publisher pub) { id = ID; // Subscribe to the event using C# 2.0 syntax pub.RaiseCustomEvent += HandleCustomEvent; } // Define what actions to take when the event is raised. void HandleCustomEvent(object sender, CustomEventArgs e) { Console.WriteLine(id + " received this message: {0}", e.Message); } } class Program { static void Main(string[] args) { Publisher pub = new Publisher(); Subscriber sub1 = new Subscriber("sub1", pub); Subscriber sub2 = new Subscriber("sub2", pub); // Call the method that raises the event. pub.DoSomething(); // Keep the console window open Console.WriteLine("Press Enter to close this window."); Console.ReadLine(); } } }
派生类中引发基类事件
下面演示如何在基类中声明事件,在派生类中引发该事件。该模式广泛应用于 .NET Framework 类库中的 Windows 窗体类。
在创建基类时,应作如下考虑:
- 事件是特殊类型的委托,只可以从声明它们的类中调用。
- 派生类无法直接调用基类中声明的事件。
- 虽然有时需要事件只能由基类引发,但在大多数情形下,应该允许派生类调用基类事件。为此,可以在包含该事件的基类中创建一个受保护的调用方法。
- 通过调用或重写此调用方法,派生类便可以间接调用该事件。
注意:不要在基类中声明virtual事件,也不要在派生类中重写这些事件。C# 编译器无法正确处理这些事件,并且无法判断其派生的事件的用户是否订阅了基类事件。
示例
namespace BaseClassEvents { using System; using System.Collections.Generic; // Special EventArgs class to hold info about Shapes. public class ShapeEventArgs : EventArgs { private double newArea; public ShapeEventArgs(double d) { newArea = d; } public double NewArea { get { return newArea; } } } // Base class event publisher public abstract class Shape { protected double area; public double Area { get { return area; } set { area = value; } } // The event. Note that by using the generic EventHandler<T> event type // we do not need to declare a separate delegate type. public event EventHandler<ShapeEventArgs> ShapeChanged; public abstract void Draw(); //The event-invoking method that derived classes can override. protected virtual void OnShapeChanged(ShapeEventArgs e) { // Make a temporary copy of the event to avoid possibility of // a race condition if the last subscriber unsubscribes // immediately after the null check and before the event is raised. EventHandler<ShapeEventArgs> handler = ShapeChanged; if (handler != null) { handler(this, e); } } } public class Circle : Shape { private double radius; public Circle(double d) { radius = d; area = 3.14 * radius * radius; } public void Update(double d) { radius = d; area = 3.14 * radius * radius; OnShapeChanged(new ShapeEventArgs(area)); } protected override void OnShapeChanged(ShapeEventArgs e) { // Do any circle-specific processing here. // Call the base class event invocation method. base.OnShapeChanged(e); } public override void Draw() { Console.WriteLine("Drawing a circle"); } } public class Rectangle : Shape { private double length; private double width; public Rectangle(double length, double width) { this.length = length; this.width = width; area = length * width; } public void Update(double length, double width) { this.length = length; this.width = width; area = length * width; OnShapeChanged(new ShapeEventArgs(area)); } protected override void OnShapeChanged(ShapeEventArgs e) { // Do any rectangle-specific processing here. // Call the base class event invocation method. base.OnShapeChanged(e); } public override void Draw() { Console.WriteLine("Drawing a rectangle"); } } // Represents the surface on which the shapes are drawn // Subscribes to shape events so that it knows // when to redraw a shape. public class ShapeContainer { List<Shape> _list; public ShapeContainer() { _list = new List<Shape>(); } public void AddShape(Shape s) { _list.Add(s); // Subscribe to the base class event. s.ShapeChanged += HandleShapeChanged; } // ...Other methods to draw, resize, etc. private void HandleShapeChanged(object sender, ShapeEventArgs e) { Shape s = (Shape)sender; // Diagnostic message for demonstration purposes. Console.WriteLine("Received event. Shape area is now {0}", e.NewArea); // Redraw the shape here. s.Draw(); } } class Test { static void Main(string[] args) { //Create the event publishers and subscriber Circle c1 = new Circle(54); Rectangle r1 = new Rectangle(12, 9); ShapeContainer sc = new ShapeContainer(); // Add the shapes to the container. sc.AddShape(c1); sc.AddShape(r1); // Cause some events to be raised. c1.Update(57); r1.Update(7, 7); // Keep the console window open in debug mode. System.Console.WriteLine("Press any key to exit."); System.Console.ReadKey(); } } } /* Output: Received event. Shape area is now 10201.86 Drawing a circle Received event. Shape area is now 49 Drawing a rectangle */
实现接口事件
可用接口声明事件。下面演示如何在类中实现接口事件。基本上和实现任意接口方法或属性的方法一样。
实现接口事件
在类中声明事件,然后在合适的地方调用该事件。
namespace ImplementInterfaceEvents { public interface IDrawingObject { event EventHandler ShapeChanged; } public class MyEventArgs : EventArgs { // class members } public class Shape : IDrawingObject { public event EventHandler ShapeChanged; void ChangeShape() { // Do something here before the event… OnShapeChanged(new MyEventArgs(/*arguments*/)); // or do something here after the event. } protected virtual void OnShapeChanged(MyEventArgs e) { if(ShapeChanged != null) { ShapeChanged(this, e); } } } }
示例
下面演示一种特殊情况:你的类从两个以上的接口继承而来,而有两个接口正好有同名的事件。在这种情况下,你至少要为其中一个事件提供显式接口实现。 为事件编写显式接口实现时,必须添加 add 和 remove事件访问器。 这两个事件访问器通常由编译器提供,但此时编译器不能提供。
通过您提供自己的访问器,以便指定这两个事件在你的类中是否由同一事件表示。 例如,如果两事件应在不同时间引发,则可以分别实现每个事件。 在例中,订户通过将shape引用强制转换为 IShape 或 IDrawingObject,以确定接收哪个 OnDraw 事件。
namespace WrapTwoInterfaceEvents { using System; public interface IDrawingObject { // Raise this event before drawing // the object. event EventHandler OnDraw; } public interface IShape { // Raise this event after drawing // the shape. event EventHandler OnDraw; } // Base class event publisher inherits two // interfaces, each with an OnDraw event public class Shape : IDrawingObject, IShape { // Create an event for each interface event event EventHandler PreDrawEvent; event EventHandler PostDrawEvent; object objectLock = new Object(); // Explicit interface implementation required. // Associate IDrawingObject's event with // PreDrawEvent event EventHandler IDrawingObject.OnDraw { add { lock (objectLock) { PreDrawEvent += value; } } remove { lock (objectLock) { PreDrawEvent -= value; } } } // Explicit interface implementation required. // Associate IShape's event with // PostDrawEvent event EventHandler IShape.OnDraw { add { lock (objectLock) { PostDrawEvent += value; } } remove { lock (objectLock) { PostDrawEvent -= value; } } } // For the sake of simplicity this one method // implements both interfaces. public void Draw() { // Raise IDrawingObject's event before the object is drawn. EventHandler handler = PreDrawEvent; if (handler != null) { handler(this, new EventArgs()); } Console.WriteLine("Drawing a shape."); // RaiseIShape's event after the object is drawn. handler = PostDrawEvent; if (handler != null) { handler(this, new EventArgs()); } } } public class Subscriber1 { // References the shape object as an IDrawingObject public Subscriber1(Shape shape) { IDrawingObject d = (IDrawingObject)shape; d.OnDraw += new EventHandler(d_OnDraw); } void d_OnDraw(object sender, EventArgs e) { Console.WriteLine("Sub1 receives the IDrawingObject event."); } } // References the shape object as an IShape public class Subscriber2 { public Subscriber2(Shape shape) { IShape d = (IShape)shape; d.OnDraw += new EventHandler(d_OnDraw); } void d_OnDraw(object sender, EventArgs e) { Console.WriteLine("Sub2 receives the IShape event."); } } public class Program { static void Main(string[] args) { Shape shape = new Shape(); Subscriber1 sub = new Subscriber1(shape); Subscriber2 sub2 = new Subscriber2(shape); shape.Draw(); // Keep the console window open in debug mode. System.Console.WriteLine("Press any key to exit."); System.Console.ReadKey(); } } } /* Output: Sub1 receives the IDrawingObject event. Drawing a shape. Sub2 receives the IShape event. */
实现自定义事件访问器
事件是特殊类型的多路广播委托,它只能从声明它的类中调用。客户端代码通过事件引发时调用的方法的引用来订阅事件。 这些方法通过事件访问器添加到委托的调用列表中,事件访问器类似于属性访问器,不同之处在于事件访问器被命名为 add 和 remove。 在大多数情况下都不需要提供自定义的事件访问器。 如果您在代码中没有提供自定义的事件访问器,编译器会自动添加事件访问器。少数情况需要自定义行为,可参考实现接口部分。
示例
下面演示如何实现自定义的add和remove事件访问器。虽然访问器内的代码可以任意替换,不过建议在添加或移除事件处理方法之前先锁定该事件。
event EventHandler IDrawingObject.OnDraw { add { lock (PreDrawEvent) { PreDrawEvent += value; } } remove { lock (PreDrawEvent) { PreDrawEvent -= value; } } }