事件(C# 编程指南)
Visual Studio 2010
·
类 或对象可以通过事件向其他类或对象通知发生的相关事情。 发送(或引发)事件的类称为 “ 发行者 ” ,接收(或处理)事件的类称为 “ 订户 ” 。在典型的 C# Windows 窗体或 Web 应用程序中,可订阅由控件(如按钮和列表框)引发的事件。 可使用 Visual C# 集成开发环境 (IDE) 来浏览控件发布的事件,选择要处理的事件。 IDE 会自动添加空事件处理程序方法和订阅事件的代码。 有关更多信息,请参见 如何:订阅和取消订阅事件(C# 编程指南)。
事件具有以下特点:
· 发行者确定何时引发事件,订户确定执行何种操作来响应该事件。
· 一个事件可以有多个订户。 一个订户可处理来自多个发行者的多个事件。
· 没有订户的事件永远也不会引发。
· 事件通常用于通知用户操作,例如,图形用户界面中的按钮单击或菜单选择操作。
· 如果一个事件有多个订户,当引发该事件时,会同步调用多个事件处理程序。 要异步调用事件,请参见使用异步方式调用同步方法。
· 在 .NET Framework 类库中,事件是基于 EventHandler 委托和 EventArgs 基类的。
有关更多信息,请参见:
· 如何:发布符合 .NET Framework 准则的事件(C# 编程指南)
· 如何:订阅和取消订阅事件(C# 编程指南)
· 如何:在派生类中引发基类事件(C# 编程指南)
· 如何:实现接口事件(C# 编程指南)
· 事件设计
如何:发布符合 .NET Framework 准则的事件(C# 编程指南)
/*采用 EventHandler 模式发布事件
1.(如果不需要与事件一起发送自定义数据,请跳过此步骤,进入步骤 3a。)在发行者类和订阅方类均可看见的范围中声明自定义数据的类。 然后添加保留您的自定义事件数据所需的成员。 在此示例中,会返回一个简单字符串。
复制
public class CustomEventArgs : EventArgs
{
public CustomEventArgs(string s)
{
msg = s;
}
private string msg;
public string Message
{
get { return msg; }
}
}
2.(如果您使用的是 EventHandler<TEventArgs> 的泛型版本,请跳过此步骤。)在发布类中声明一个委托。 为它指定以 EventHandler 结尾的名称。 第二个参数指定自定义 EventArgs 类型。
复制
public delegate void CustomEventHandler(object sender, CustomEventArgs a);
3.使用以下任一步骤,在发布类中声明事件。
a.如果没有自定义 EventArgs 类,事件类型就是非泛型 EventHandler 委托。 无需声明委托,因为它已在创建 C# 项目时包含的 System 命名空间中进行了声明。 将以下代码添加到发行者类中。
复制
public event EventHandler RaiseCustomEvent;
b.如果使用的是 EventHandler 的非泛型版本,并且您有一个由 EventArgs 派生的自定义类,请在发布类中声明您的事件,并且将来自步骤 2 的委托用作类型。
复制
public event CustomEventHandler RaiseCustomEvent;
c.如果使用的是泛型版本,则不需要自定义委托。 相反,在发行者类中,您应将事件类型指定为 EventHandler<CustomEventArgs>,将尖括号中的内容替换为自己的类的名称。
复制
public event EventHandler<CustomEventArgs> RaiseCustomEvent;
示例
--------------------------------------------------------------------------------
下面的示例通过将自定义 EventArgs 类和 EventHandler<TEventArgs> 用作事件类型来演示上述步骤。 */
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace DotNetEvents
{
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();
}
}
}
}
如何:订阅和取消订阅事件(C# 编程指南)
如果您想编写引发事件时调用的自定义代码,则可以订阅由其他类发布的事件。 例如,可以订阅某个按钮的 click 事件,以使应用程序在用户单击该按钮时执行一些有用的操作。
使用 Visual Studio IDE 订阅事件
1. 如果看不到“属性”窗口,请在“设计”视图中,右击要为其创建事件处理程序的窗体或控件,然后选择“属性”。
2. 在“属性”窗口的顶部,单击“事件”图标。
3. 双击要创建的事件,例如 Load 事件。
Visual C# 会创建一个空事件处理程序方法,并将其添加到您的代码中。 或者,您也可以在“代码”视图中手动添加代码。 例如,下面的代码行声明了一个在 Form 类引发 Load 事件时调用的事件处理程序方法。
private void Form1_Load(object sender, System.EventArgs e)
{
// Add your form load event handling code here.
}
还会在项目的 Form1.Designer.cs 文件的 InitializeComponent 方法中自动生成订阅该事件所需的代码行。 该代码行类似于:
this.Load += new System.EventHandler(this.Form1_Load);
以编程方式订阅事件
1. 定义一个事件处理程序方法,其签名与该事件的委托签名匹配。 例如,如果事件基于 EventHandler 委托类型,则下面的代码表示方法存根:
void HandleCustomEvent(object sender, CustomEventArgs a)
{
// Do something useful here.
}
2. 使用加法赋值运算符 (+=) 来为事件附加事件处理程序。 在下面的示例中,假设名为 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());};
}
有关更多信息,请参见 如何:在 LINQ 外部使用 Lambda 表达式(C# 编程指南)。
使用匿名方法订阅事件
· 如果以后不必取消订阅某个事件,则可以使用加法赋值运算符 (+=) 将匿名方法附加到此事件。 在下面的示例中,假设名为 publisher 的对象拥有一个名为 RaiseCustomEvent 的事件,并且还定义了一个 CustomEventArgs 类以承载某些类型的专用事件信息。 请注意,订户类需要引用 publisher 才能订阅其事件。
publisher.RaiseCustomEvent += delegate(object o, CustomEventArgs e)
{
string s = o.ToString() + " " + e.ToString();
Console.WriteLine(s);
};
请务必注意,如果使用匿名函数订阅事件,事件的取消订阅过程将比较麻烦。 这种情况下若要取消订阅,必须返回到该事件的订阅代码,将该匿名方法存储在委托变量中,然后将此委托添加到该事件中。 一般来说,如果必须在后面的代码中取消订阅某个事件,则建议您不要使用匿名函数订阅此事件。 有关匿名函数的更多信息,请参见匿名函数(C# 编程指南)。
要防止在引发事件时调用事件处理程序,请取消订阅该事件。 要防止资源泄露,应在释放订户对象之前取消订阅事件。 在取消订阅事件之前,在发布对象中作为该事件的基础的多路广播委托会引用封装了订户的事件处理程序的委托。 只要发布对象保持该引用,垃圾回收功能就不会删除订户对象。
取消订阅事件
· 使用减法赋值运算符 (-=) 取消订阅事件:
publisher.RaiseCustomEvent -= HandleCustomEvent;
所有订户都取消订阅事件后,发行者类中的事件实例将设置为 null。
如何:在派生类中引发基类事件(C# 编程指南)
Visual Studio 2010
·
以下简单示例演示了在基类中声明可从派生类引发的事件的标准方法。 此模式广泛应用于 .NET Framework 类库中的 Windows 窗体类。在创建可用作其他类的基类的类时,应考虑如下事实:事件是特殊类型的委托,只可以从声明它们的类中调用。 派生类无法直接调用基类中声明的事件。 尽管有时需要事件仅由基类引发,但在大多数情形下,应该允许派生类调用基类事件。 为此,您可以在包含该事件的基类中创建一个受保护的调用方法。 通过调用或重写此调用方法,派生类便可以间接调用该事件。
注意 |
不要在基类中声明虚拟事件,也不要在派生类中重写这些事件。 在 Microsoft Visual Studio 2010 中 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
*/
如何:实现接口事件(C# 编程指南)
Visual Studio 2010
·
接口 可声明 事件 。 下面的示例演示如何在类中实现接口事件。 实现接口事件的规则与实现任何接口方法或属性的规则基本相同。在类中实现接口事件
· 在类中声明事件,然后在适当的区域调用该事件。
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 事件访问器。 这两个事件访问器通常由编译器提供,但在这种情况下编译器不能提供。
您可以提供自己的访问器,以便指定这两个事件是由您的类中的同一事件表示,还是由不同事件表示。 例如,根据接口规范,如果事件应在不同时间引发,则可以将每个事件与类中的一个单独实现关联。 在下面的示例中,订户将形状引用强制转换为 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.
*/
事件设计
事件是操作发生时允许执行特定于应用程序的代码的机制。 事件要么在相关联的操作发生前发生(事前事件),要么在操作发生后发生(事后事件)。 例如,当用户单击窗口中的按钮时,将引发一个事后事件,以允许执行特定于应用程序的方法。 事件处理程序委托会绑定到系统引发事件时要执行的方法。 事件处理程序会添加到事件中,以便当事件引发时,事件处理程序能够调用它的方法。 事件可以具有特定于事件的数据,例如,按下鼠标事件可以包含有关屏幕光标位置的数据。
事件处理方法的签名与事件处理程序委托的签名是相同的。 事件处理程序签名遵循下面的约定:
· 返回类型为 Void。
· 第一个参数命名为 sender,是 Object 类型。 它是引发事件的对象。
· 第二个参数命名为 e,是 EventArgs 类型或 EventArgs 的派生类。它是特定于事件的数据。
· 该方法有且仅有两个参数。
有关事件的更多信息,请参见 处理和引发事件。
对于事件,要使用术语“引发”,而不要使用“激发”或者“触发”。
使用 System.EventHandler<T>,而不要手动创建用作事件处理程序的新委托。
此准则主要适用于新的功能区域。 如果您是在已经使用非泛型事件处理程序的区域中扩展功能,则可以继续使用非泛型事件处理程序,以保持设计一致。
如果您的库针对的是不支持泛型的 .NET Framework 版本,则无法遵循此准则。
考虑使用 System.EventArgs 的派生类作为事件参数,除非您完全确定事件决不会需要向事件处理方法传递任何数据(这种情况下可以直接使用 System.EventArgs 类型)。
如果您定义的事件采用 EventArgs 实例而不是您定义的派生类,则不能够在以后的版本中向该事件添加数据。 出于上述原因,建议创建一个空的 EventArgs 派生类。 这使您能够在以后的版本中在不引入重大更改的情况下向事件添加数据。
使用受保护的虚方法来引发每个事件。 这只适用于未密封类的非静态事件,而不适用于结构、密封类或静态事件。
遵循此准则可使派生类能够通过重写受保护的方法来处理基类事件。 受保护的 virtual(在 Visual Basic 中是 Overridable)方法的名称应该是为事件名加上 On 前缀而得到的名称。 例如,名为“TimeChanged”的事件的受保护的虚方法被命名为“OnTimeChanged”。
重要事项 |
重写受保护的虚方法的派生类无需调用基类实现。 即使没有调用基类的实现,基类也必须继续正常工作。 |
使用一个参数,该参数已类型化为引发事件的受保护方法的事件参数类。 该参数应命名为 e。
FontDialog 类提供下面的方法,该方法引发 Apply 事件:
protected virtual void OnApply(EventArgs e);
当引发非静态事件时,不要将 null(在 Visual Basic 中为 Nothing)作为 sender 参数进行传递。
对于静态事件,sender 参数应该为 null(在 Visual Basic 中是 Nothing)。
当引发事件时,不要将 null(在 Visual Basic 中为 Nothing)作为事件数据参数进行传递。
如果没有事件数据,则传递 Empty,而不要传递 null。
事件处理方法中会发生任意代码执行,对此一定要做好准备。
考虑将引发事件的代码放置在 try-catch 块中,以防止由事件处理程序引发的未经处理的异常所导致的程序终止。
考虑引发最终用户可以取消的事件。 这仅适用于事前事件。
如果您正在设计可取消的事件,请使用 CancelEventArgs(而非 EventArgs)作为事件数据对象 e 的基类。