处理和引发事件

.NET 中的事件基于委托模型。 委托模型遵循观察者设计模式,使订阅者能够向提供方注册并接收相关通知。 事件发送方推送事件发生的通知,事件接收器接收该通知并定义对它的响应。 本文介绍委托模型的主要组件、如何在应用程序中使用事件以及如何在你的代码中实现事件。

事件

事件是由对象发送的用于发出操作信号的消息。 该操作可能是由用户交互引起,例如单击按钮;也可能是由某些其他程序的逻辑导致,例如更改属性值。 引发事件的对象称为“事件发送方”。 事件发送方不知道哪个对象或方法将接收(处理)它引发的事件。 事件通常是事件发送方的成员,例如 Click 事件是 Button 类的成员,PropertyChanged 事件是实现 INotifyPropertyChanged 接口的类的成员。

若要定义一个事件,可以在事件类签名中使用 event(在 C# 中)或 Event(在 Visual Basic 中)关键字,并指定事件的委托类型。 委托在下一节中介绍。

通常,为了引发事件,您可以在 C# 中添加一个标记为 protectedvirtual 或在 Visual Basic 中标记为 ProtectedOverridable 的方法。 将此方法命名为 OnEventName;例如,OnDataReceived。 此方法应接受一个指定事件数据对象(EventArgs 类型或派生类型)的参数。 您提供此方法以允许派生类重写引发事件的逻辑。 派生类应始终调用基类的 OnEventName 方法,以确保注册的委托接收事件。

下面的示例显示如何声明名为 ThresholdReached 事件。 该事件与 EventHandler 委托相关联并且由 OnThresholdReached 方法引发。

class Counter
{
    public event EventHandler ThresholdReached;

    protected virtual void OnThresholdReached(EventArgs e)
    {
        EventHandler handler = ThresholdReached;
        handler?.Invoke(this, e);
    }

    // provide remaining implementation for the class
}
Public Class Counter
    Public Event ThresholdReached As EventHandler

    Protected Overridable Sub OnThresholdReached(e As EventArgs)
        RaiseEvent ThresholdReached(Me, e)
    End Sub

    ' provide remaining implementation for the class
End Class

委托

委托是一种保存对方法的引用的类型。 委托是通过显示所引用方法的返回类型和参数的签名来声明的,并可以仅保存与其签名匹配的方法的引用。 因此,委托等同于类型安全函数指针或回叫。 委托声明足以定义委托类。

委托在 .NET 中有许多用途。 在事件上下文中,委托是事件源和处理事件的代码之间的媒介(或类似指针的机制)。 如上一节的例子所示,可以通过在事件声明中包括委托类型来将委托和事件相关联。 有关委托的详细信息,请参阅 Delegate 类。

.NET 提供了 EventHandlerEventHandler<TEventArgs> 委托来支持大部分事件场景。 使用 EventHandler 委托处理不包含事件数据的所有事件。 使用 EventHandler<TEventArgs> 委托处理包含事件相关数据的事件。 这些委托没有返回类型值,并且接受两个参数(事件源的对象和事件数据的对象)。

委托是多播,这意味着它们可以保存对多个事件处理方法的引用。 有关详细信息,请参阅 Delegate 参考页。 委托提供了事件处理中的灵活性和精确控制。 委托人通过维护事件的已注册事件处理程序列表来充当引发事件的类的事件调度程序。

EventHandlerEventHandler<TEventArgs> 委托不可用的场景下,您可以定义一个委托。 要求你定义委托的场景非常少见的,例如,当你必须处理无法识别泛型的代码时。 在声明中使用 delegate(在 C# 中)和 Delegate(在 Visual Basic 中)关键字标记委托。 下面的示例说明如何声明 ThresholdReachedEventHandler 委托。

public delegate void ThresholdReachedEventHandler(object sender, ThresholdReachedEventArgs e);
Public Delegate Sub ThresholdReachedEventHandler(sender As Object, e As ThresholdReachedEventArgs)

事件数据

与事件相关的数据可以通过事件数据类提供。 .NET 提供了许多事件数据类,用户可以在自己的应用程序中使用它们。 例如,SerialDataReceivedEventArgs 类是 SerialPort.DataReceived 事件的事件数据类。 .NET 遵循所有事件数据类以 EventArgs 结尾的命名模式。 您通过查看事件的委托来确定哪个事件数据类与事件相关联。 例如,SerialDataReceivedEventHandler 委托包含 SerialDataReceivedEventArgs 类作为它的一个参数。

EventArgs 类是所有事件数据类的基类型。 当一个事件没有任何与其相关联的数据时,您也会用到 EventArgs 类。 当您创建一个事件仅用来通知其他类出问题了,不需要传递任何数据时,请包括 EventArgs 类作为委托中的第二个参数。 当没有数据提供时,您可以传递 EventArgs.Empty 值。 EventHandler 委托包括 EventArgs 类作为一个参数。

当您想创建一个自定义的事件数据类时,请创建一个派生自 EventArgs 的类,然后提供所需的所有成员,来传递与该事件相关的数据。 通常,应使用与 .NET 相同的命名模式,并且事件数据类名称应以 EventArgs 结尾。

下面的示例演示了一个名为 ThresholdReachedEventArgs 的事件数据类。 它包含特定于引发事件的属性。

public class ThresholdReachedEventArgs : EventArgs
{
    public int Threshold { get; set; }
    public DateTime TimeReached { get; set; }
}
Public Class ThresholdReachedEventArgs
    Inherits EventArgs

    Public Property Threshold As Integer
    Public Property TimeReached As DateTime
End Class

事件处理程序

若要响应事件,您需要在事件接收器中定义一个事件处理程序方法。 此方法必须与您正处理的事件的委托签名匹配。 在事件处理程序中,当事件引发时会执行所需操作,例如在用户单击按钮之后收集用户输入。 若当事件发生时收到通知,您的事件处理程序方法必须订阅该事件。

下面的示例演示与 c_ThresholdReached 委托的签名匹配的 EventHandler 事件处理程序方法。 该方法订阅 ThresholdReached 事件。

class Program
{
    static void Main()
    {
        var c = new Counter();
        c.ThresholdReached += c_ThresholdReached;

        // provide remaining implementation for the class
    }

    static void c_ThresholdReached(object sender, EventArgs e)
    {
        Console.WriteLine("The threshold was reached.");
    }
}
Module Module1

    Sub Main()
        Dim c As New Counter()
        AddHandler c.ThresholdReached, AddressOf c_ThresholdReached

        ' provide remaining implementation for the class
    End Sub

    Sub c_ThresholdReached(sender As Object, e As EventArgs)
        Console.WriteLine("The threshold was reached.")
    End Sub
End Module

静态和动态事件处理程序

借助 .NET,订阅者可以进行静态或动态注册以获得事件通知。 对于其事件由静态事件处理程序进行处理的类,静态事件处理程序对其整个生命周期有效。 通常为响应某些条件程序逻辑,会在程序执行期间显式激活和停用动态事件处理程序。 例如,如果仅在特定条件下需要事件通知,或如果应用程序提供多个事件处理程序且由运行时条件定义要使用的适当事件处理程序,则可以使用动态事件处理程序。 上一节中的示例演示如何动态添加事件处理程序。 有关详细信息,请查看 Visual Basic 中的事件和 C# 中的事件

引发多个事件

如果您的类引发多个事件,编译器会为每一个事件委托实例生成一个字段。 如果事件数量很大,则可能无法接受按一个委托计算一个字段的存储成本。 对于这些情况,.NET 提供一个事件属性,可以将其与选择的另一数据结构一起用于存储事件委托。

事件属性由事件声明和事件访问器组成。 事件访问器是您定义的方法,用来从存储数据结构添加和移除事件委托实例。 请注意,事件属性要比事件字段慢,这是因为必须先检索每个事件委托,然后才能调用它。 需在内存和速度之间进行权衡。 如果类定义许多不常引发的事件,那么需要实现事件属性。 有关详细信息,请参阅如何:使用事件属性处理多个事件

Title 描述
如何:抛出和使用事件 包含引发和使用事件的示例。
如何:使用事件属性处理多个事件 演示如何使用事件属性处理多个事件。
观察程序设计模式 描述允许订阅者向提供方注册和接收通知的设计模式。

请参阅