Skip to content

Latest commit

 

History

History
1211 lines (940 loc) · 39.2 KB

File metadata and controls

1211 lines (940 loc) · 39.2 KB

二、事件

对事件的支持被认为是 C# 中最激动人心的特性之一。

以下是事件的一些基本特征。我建议您在使用事件编码之前,反复阅读这些要点。

  • 事件的支柱是委托,所以在使用事件之前了解委托是很重要的。

  • 使用事件时,一段代码可以向另一段代码发送通知。

  • 事件通常在 GUI 应用中使用。例如,当您单击一个按钮或选择一个单选按钮时,您可能会注意到 UI 布局中一些有趣的变化。

  • 在发布者-订阅者模型中,一个对象引发一个通知(事件),一个或多个对象侦听这些事件。引发事件的对象称为发送者(或发布者或广播者),接收事件的对象称为接收者(或订阅者)。发送者不关心接收者如何解释事件。它可能不关心谁在注册以接收或取消注册以停止接收事件或通知。你可以把这和脸书或者推特联系起来。如果您关注某人,您可以在此人更新个人资料时收到通知。如果您不想收到通知,您可以随时取消订阅。简而言之,订户可以决定何时开始收听事件或何时停止收听事件。(用编程术语来说,就是什么时候注册事件,什么时候注销事件)。

  • 英寸 NET 中,事件被实现为多播委托。

  • 发布者包含委托。订阅者在发布者的委托上使用+=进行注册,在该委托上使用-=进行注销。所以,当我们将+=或-=应用于一个事件时,有一个特殊的含义(换句话说,它们不是赋值的快捷键)。

  • 订户彼此不通信。因此,您可以构建一个松散耦合的系统。这通常是事件驱动架构的关键目标。

  • 在 GUI 应用中,Visual Studio IDE 可以让您在处理事件时更加轻松。(我相信,既然这些概念是 C# 的核心,不如从基础开始学。)

  • 那个。NET framework 提供了一个支持标准事件设计模式的泛型委托,如下所示:

    public delegate void EventHandler<TEventArgs>(object sendersource, TEventArgs e), where TEventArgs : EventArgs;.

    我还没有讨论泛型,所以你现在可以跳过这一点。但是有趣的是,为了支持向后兼容性,在。NET framework 遵循非泛型自定义委托模式。

  • 下面是一个事件声明的示例:

    public event EventHandler MyIntChanged;

    这只是表明MyIntChanged是事件的名称,而EventHandler是相应的代表。

  • 修饰符不需要是公共的。你可以为你的事件选择非公开的修饰语,比如privateprotectedinternal等等。在这种情况下,你也可以使用关键字staticvirtualoverrideabstractsealednew

演示 1

现在您已经准备好编码了。在声明事件之前,您需要一个委托。在示例中,您会看到下面的代码行。

public event EventHandler MyIntChanged;

但是您看不到委托声明,因为我使用了预定义的EventHandler委托。

现在让我们关注我们的实现。有两类:SenderReceiverSender扮演广播员的角色;当您更改myInt实例值时,它会引发MyIntChanged事件。Receiver类扮演消费者的角色。它有一个方法叫做GetNotificationFromSender。要从发件人处获得通知,请注意下面的代码行。

// Receiver is registering for a notification from sender
sender.MyIntChanged += receiver.GetNotificationFromSender;

这里的sender是一个Sender类对象,receiver是一个Receiver类对象。最终,接收者不再对从发送者那里获得进一步的通知感兴趣,并使用下面的代码取消订阅事件。

// Unregistering now
sender.MyIntChanged -= receiver.GetNotificationFromSender;

值得注意的是,发送者可以向自己发送通知。为了演示这一点,在最后几行of Main中,您会看到下面的代码。

// Sender will receive its own notification now onwards
sender.MyIntChanged += sender.GetNotificationItself;
using System;

namespace EventEx1
{
    class Sender
    {
        private int myInt;
        public int MyInt
        {
            get
            {
                return myInt;
            }
            set
            {
                myInt = value;
             //Whenever we set a new value, the event will fire.
                OnMyIntChanged();
            }
        }
        //EventHandler is a predefined delegate which is used to //handle simple events.
        //It has the following signature:
        //delegate void System.EventHandler(object sender,System.EventArgs e)
        //where the sender tells who is sending the event and
        //EventArgs is used to store information about the event.
        public event EventHandler MyIntChanged;
        public void OnMyIntChanged()
        {
            if(MyIntChanged!=null )
            {
                MyIntChanged(this, EventArgs.Empty);
            }
        }

        public void GetNotificationItself(Object sender, System.EventArgs e)
        {
            Console.WriteLine("Sender himself send a notification: I have changed myInt value  to {0} ", myInt);
        }
    }
    class Receiver
    {
        public void GetNotificationFromSender(Object sender, System.EventArgs e)
        {
         Console.WriteLine("Receiver receives a notification: Sender recently has changed the myInt value . ");
        }
    }
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("***Exploring events.***");
            Sender sender = new Sender();
            Receiver receiver = new Receiver();
            //Receiver is registering for a notification from sender
            sender.MyIntChanged += receiver.GetNotificationFromSender;

            sender.MyInt = 1;
            sender.MyInt = 2;
            //Unregistering now
            sender.MyIntChanged -= receiver.GetNotificationFromSender;
            //No notification sent for the receiver now.
            sender.MyInt = 3;
            //Sender will receive its own notification now onwards.
            sender.MyIntChanged += sender.GetNotificationItself;
            sender.MyInt = 4;

            Console.ReadKey();

        }
    }
}

输出

以下是运行该程序的输出。

***Exploring events.***
Receiver receives a notification: Sender recently has changed the myInt value.
Receiver receives a notification: Sender recently has changed the myInt value.
Sender himself send a notification: I have changed myInt value  to 4

分析

最初,我使用MyInt属性更改了发送者的myInt值。当我将该值更改为 1 或 2 时,Receiver 对象(receiver)收到了通知,因为它订阅了该事件。然后receiver退订了。因此,当我将值改为 3 时,receiver没有任何通知。然后sender订阅事件通知。结果,当我将值改为 4 时,sender收到了通知。

Note

在现实应用中,一旦你订阅了一个事件,你也应该在离开前退订该事件;否则,您可能会看到内存泄漏的影响。

问答环节

2.1 我可以在特定事件上使用任何方法吗?

不。它应该与委托签名相匹配。例如,让我们假设Receiver类有另一个名为UnRelatedMethod的方法,如下所示。

public void UnRelatedMethod()
{
   Console.WriteLine(" An unrelated method. ");
}

在演示 1 中,如果您通过使用语句用MyIntChanged附加了这个方法

sender.MyIntChanged += receiver.UnRelatedMethod;//Error

您将得到以下编译时错误:

CS0123    No overload for 'UnRelatedMethod' matches delegate 'EventHandler'

创建自定义事件

在演示 1 中,您看到了一个内置的委托,但是在许多情况下,您可能需要自己的事件来处理特定的场景。让我们来练习一个关于自定义事件的程序。为了使这个例子简单明了,我们假设发送者不需要给自己发送任何通知。所以,现在Sender类中没有类似GetNotificationItself的方法。

为了使更改与前面的示例保持一致,让我们按照以下步骤操作。

  1. 创建代理人。按照惯例,选择带EventHandler后缀的代理名称;大概如下:

    delegate void MyIntChangedEventHandler(Object sender, EventArgs eventArgs);
  2. 定义您的活动。按照惯例,您可以去掉代理名称的后缀EventHandler并设置您的事件名称。

    public event MyIntChangedEventHandler MyIntChanged;
  3. 引发事件。让我们在 Sender 类中使用下面的方法。一般情况下,不做方法public,建议你做方法protected virtual

    protected virtual void OnMyIntChanged()
    {
        if (MyIntChanged != null)
        {
            MyIntChanged(this, EventArgs.Empty);
        }
    }
  4. 处理事件。让我们使用一个Receiver类,它有下面的方法来处理被引发的事件。让我们保持与演示 1 中的相同。

    class Receiver
    {
        public void GetNotificationFromSender(Object sender, System.EventArgs e)
        {
            Console.WriteLine("Receiver receives a notification: Sender recently has changed the myInt value . ");
        }
    }

演示 2

现在进行完整的演示。

using System;

namespace EventsEx2
{
    //Step 1-Create a delegate.
    //You can pick an name (this name will be your event name)
    //which has the suffix EventHandler.For example, in the following case
    //'MyIntChanged' is the event name which has the suffix 'EventHandler'

    delegate void MyIntChangedEventHandler(Object sender, EventArgs eventArgs);

    //Create a Sender or Publisher for the event.
    class Sender
    {
        //Step-2: Create the event based on your delegate.
        public event MyIntChangedEventHandler MyIntChanged;

        private int myInt;
        public int MyInt
        {
            get
            {
                return myInt;
            }
            set
            {
                myInt = value;
             //Raise the event.
             //Whenever we set a new value, the event will fire.
                OnMyIntChanged();
            }
        }

        /*
        Step-3.
        In the standard practise, the method name is the event name with a prefix 'On'.For example, MyIntChanged(event name) is prefixed with 'On' here.
        Also, in normal practises, instead of making the method 'public',
        you make the method 'protected virtual'.
        */
        protected virtual void OnMyIntChanged()
        {
            if (MyIntChanged != null)
            {
                MyIntChanged(this, EventArgs.Empty);
            }
        }
    }
    //Step-4: Create a Receiver or Subscriber for the event.
    class Receiver
    {
        public void GetNotificationFromSender(Object sender, System.EventArgs e)
        {
            Console.WriteLine("Receiver receives a notification: Sender recently has changed the myInt value . ");
        }
    }
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("***Exploring a custom event.***");
            Sender sender = new Sender();
            Receiver receiver = new Receiver();
            //Receiver is registering for a notification from sender
            sender.MyIntChanged += receiver.GetNotificationFromSender;

            sender.MyInt = 1;
            sender.MyInt = 2;
            //Unregistering now
            sender.MyIntChanged -= receiver.GetNotificationFromSender;
            //No notification sent for the receiver now.
            sender.MyInt = 3;
            Console.ReadKey();
        }
    }
}

输出

以下是运行该程序的输出。

***Exploring a custom event.***
Receiver receives a notification: Sender recently has changed the myInt value .
Receiver receives a notification: Sender recently has changed the myInt value .

分析

您可以看到,通过使用MyInt属性,我正在更改myInt的值。当该值设置为 1 或 2 时,接收方会收到通知,但是当myInt值更改为 3 时,接收方没有收到通知,因为事件通知被取消订阅。

将数据传递给事件参数

让我们再来看看OnMyIntChanged方法。在前两个演示中,我在方法中使用了下面一行代码。

  MyIntChanged(this, EventArgs.Empty);

我没有在事件参数中传递任何东西。但是在现实编程中,你可能需要传递一些有意义的东西。让我们在演示 3 中分析这样一个案例。

演示 3

在这个演示中,我遵循了这些步骤。

  1. 创建EventArgs的子类。这个类有一个JobNo property来设置jobNo实例变量的值。

  2. 修改OnMyIntChanged方法,用事件封装预期数据(在本例中是job number)。现在这个方法看起来如下:

    protected virtual void OnMyIntChanged()
    {
        if (MyIntChanged != null)
        {
          // Combine your data with the event argument
          JobNoEventArgs jobNoEventArgs = new JobNoEventArgs();
          jobNoEventArgs.JobNo = myInt;
          MyIntChanged(this, jobNoEventArgs);
        }}
  3. 在这次演示中,我保持了相同的步骤。

这是完整的演示。

using System;

namespace EventsEx3
{
    // Create a subclass of System.EventArgs
    class JobNoEventArgs : EventArgs
    {
        int jobNo = 0;
        public int JobNo
        {
            get { return jobNo; }
            set { jobNo = value; }
        }
    }

    // Create a delegate.
    delegate void MyIntChangedEventHandler(Object sender, JobNoEventArgs eventArgs);
    // Create a Sender or Publisher for the event.
    class Sender
    {
        // Create the event based on your delegate.
        public event MyIntChangedEventHandler MyIntChanged;

        private int myInt;
        public int MyInt
        {
            get
            {
                return myInt;
            }
            set
            {
                myInt = value;
                // Raise the event.
                // Whenever you set a new value, the event will fire.
                OnMyIntChanged();
            }
        }

        /*
In the standard practise, the method name is the event name with a prefix 'On'.For example, MyIntChanged(event name) is prefixed with 'On' here.Also, in normal practises, instead of making the method 'public',you make the method 'protected virtual'.
       */
        protected virtual void OnMyIntChanged()
        {
            if (MyIntChanged != null)
            {   // Combine your data with the event argument
                JobNoEventArgs jobNoEventArgs = new JobNoEventArgs();
                jobNoEventArgs.JobNo = myInt;
                MyIntChanged(this, jobNoEventArgs);
            }
        }
    }
    // Create a Receiver or Subscriber for the event.
    class Receiver
    {
        public void GetNotificationFromSender(Object sender, JobNoEventArgs e)
        {
            Console.WriteLine("Receiver receives a notification: Sender recently has changed the myInt value to {0}.",e.JobNo);
        }
    }
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("***Passing data in the event argument.***");
            Sender sender = new Sender();
            Receiver receiver = new Receiver();
            // Receiver is registering for a notification from sender
            sender.MyIntChanged += receiver.GetNotificationFromSender;

            sender.MyInt = 1;
            sender.MyInt = 2;
            // Unregistering now
            sender.MyIntChanged -= receiver.GetNotificationFromSender;
            // No notification sent for the receiver now.
            sender.MyInt = 3;
            Console.ReadKey();
        }
    }
}

输出

以下是运行该程序的输出。

***Passing data in the event argument.***
Receiver receives a notification: Sender recently has changed the myInt value to 1.
Receiver receives a notification: Sender recently has changed the myInt value to 2.

使用事件访问器

让我们对演示 3 做一些有趣的修改。而不是使用

public event MyIntChangedEventHandler MyIntChanged;

使用下面的代码段。

private MyIntChangedEventHandler myIntChanged;
public event MyIntChangedEventHandler MyIntChanged
{
    add
    {
        myIntChanged += value;
        }
        remove
        {
        myIntChanged -= value;
    }
}

为了适应这种变化,让我们如下更新OnMyIntChanged方法。

protected virtual void OnMyIntChanged()
{
    if (myIntChanged != null)
    {
        // Combine your data with the event argument
        JobNoEventArgs jobNoEventArgs = new JobNoEventArgs();
        jobNoEventArgs.JobNo = myInt;
        myIntChanged(this, jobNoEventArgs);
    }
}

现在如果你执行这个程序,你会得到同样的输出。这怎么可能?编译器的工作方式类似于您声明事件时的方式。让我们回到事件的基本原理。

事件是一种特殊的多播委托,您只能从包含该事件的类中调用它。接收者可以订阅该事件,并使用其中的方法处理该事件。因此,接收者在订阅事件时传递方法引用。因此,此方法通过事件访问器添加到委托的订阅列表中。这些事件访问器类似于属性访问器,只是它们被命名为addremove

通常,您不需要提供自定义事件访问器。但是当您定义它们时,您是在指示 C# 编译器不要为您生成默认的字段和访问器。

在撰写本文时,基于。NET 框架目标 c# 7.3;鉴于。NET 核心应用面向 C# 8.0。如果您在。NET Framework(我们将其重命名为EventEx3DotNetFramework)并研究 IL 代码,您会注意到 IL 代码中出现了add_<EventName>remove_<EventName>。图 2-1 是 IL 代码的部分截图。

img/494901_1_En_2_Fig1_HTML.jpg

图 2-1

IL 代码的部分截图

演示 4

我们来做一个完整的演示,如下。

using System;

namespace EventsEx4
{
    //Create a subclass of System.EventArgs
    class JobNoEventArgs : EventArgs
    {
        int jobNo = 0;
        public int JobNo
        {
            get { return jobNo; }
            set { jobNo = value; }
        }
    }

    // Create a delegate.
    delegate void MyIntChangedEventHandler(Object sender, JobNoEventArgs eventArgs);

    // Create a Sender or Publisher for the event.
    class Sender
    {
        // Create the event based on your delegate.
        #region equivalent code
        // public event MyIntChangedEventHandler MyIntChanged;
        private MyIntChangedEventHandler myIntChanged;
        public event MyIntChangedEventHandler MyIntChanged
        {
            add
            {
                Console.WriteLine("***Inside add accessor.Entry point.***");
                myIntChanged += value;
            }
            remove
            {
                myIntChanged -= value;
                Console.WriteLine("***Inside remove accessor.Exit point.***");
            }
        }
        #endregion

        private int myInt;
        public int MyInt
        {
            get
            {
                return myInt;
            }
            set
            {
                myInt = value;
                // Raise the event.
                // Whenever we set a new value, the event will fire.
                OnMyIntChanged();
            }
        }
        protected virtual void OnMyIntChanged()
        {
            // if (MyIntChanged != null)
            if (myIntChanged != null)
            {
                // Combine your data with the event argument
                JobNoEventArgs jobNoEventArgs = new JobNoEventArgs();
                jobNoEventArgs.JobNo = myInt;
                // MyIntChanged(this, jobNoEventArgs);
                myIntChanged(this, jobNoEventArgs);
            }
        }
    }
    // Create a Receiver or Subscriber for the event.
    class Receiver
    {
        public void GetNotificationFromSender(Object sender, JobNoEventArgs e)
        {
            Console.WriteLine("Receiver receives a notification: Sender recently has changed the myInt value to {0}.", e.JobNo);
        }
    }
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("***Using event accessors.***");
            Sender sender = new Sender();
            Receiver receiver = new Receiver();
            // Receiver is registering for a notification from sender
            sender.MyIntChanged += receiver.GetNotificationFromSender;

            sender.MyInt = 1;
            sender.MyInt = 2;
            // Unregistering now
            sender.MyIntChanged -= receiver.GetNotificationFromSender;
            // No notification sent for the receiver now.
            sender.MyInt = 3;
            Console.ReadKey();
        }
    }
}

输出

以下是运行该程序的输出。

***Using event accessors.***
***Inside add accessor.Entry point.***
Receiver receives a notification: Sender recently has changed the myInt value to 1.
Receiver receives a notification: Sender recently has changed the myInt value to 2.
***Inside remove accessor.Exit point.***

分析

当您使用事件访问器时,请记住一个重要的建议:实现锁定机制。例如,当您编写以下代码段时,可以改进演示 4。

public object lockObject = new object();
private MyIntChangedEventHandler myIntChanged;
public event MyIntChangedEventHandler MyIntChanged
{
    add
    {
        lock (lockObject)
        {
            Console.WriteLine("***Inside add accessor.Entry point.***");
               myIntChanged += value;
          }
      }
      remove
      {
          lock (lockObject)
          {
              myIntChanged -= value;
              Console.WriteLine("***Inside remove accessor.Exit point.***");
          }
      }
 }

问答环节

2.2 使用用户定义的事件访问器的主要好处是什么?

让我们仔细看看下面这段代码。

   private  MyIntChangedEventHandler myIntChanged;
   public event MyIntChangedEventHandler MyIntChanged
   {
       add
       {
           myIntChanged += value;
       }
       remove
       {
          myIntChanged -= value;
       }
   }

注意,这些事件访问器类似于属性访问器,除了它们被命名为addremove。这里你在你的委托周围使用了一个类似属性的包装。因此,只有包含类可以直接调用委托;外人不能这么做。这促进了更好的安全性和对代码的控制。

处理界面事件

接口可以包含事件。当您实现接口方法或属性时,您需要遵循相同的规则。以下示例显示了这样的实现。

演示 5

在这个例子中,IMyInterface有一个MyIntChanged事件。我使用了SenderReceiver,它们与前面的例子相同。唯一不同的是,这一次,Sender类实现了IMyInterface接口。

using System;

namespace EventEx5
{
    interface IMyInterface
    {
        // An interface event
        event EventHandler MyIntChanged;
    }
    class Sender : IMyInterface
    {
        // Declare the event here and raise from your intended location
        public event EventHandler MyIntChanged;
        private int myInt;
        public int MyInt
        {
            get
            {
                return myInt;
            }
            set
            {
                // Setting a new value prior to raise the event.
                myInt = value;
                OnMyIntChanged();
            }
        }

        protected virtual void OnMyIntChanged()
        {
            if (MyIntChanged != null)
            {
                MyIntChanged(this, EventArgs.Empty);
            }
        }
    }
    class Receiver
    {
        public void GetNotificationFromSender(Object sender, System.EventArgs e)
        {
            Console.WriteLine("Receiver receives a notification: Sender recently has changed the myInt value . ");
        }
    }
    class Program
    {
        static void Main(string[] args)

        {
            Console.WriteLine("***Exploring an event with an interface.***");
            Sender sender = new Sender();
            Receiver receiver = new Receiver();
            // Receiver is registering for a notification from sender
            sender.MyIntChanged += receiver.GetNotificationFromSender;

            sender.MyInt = 1;
            sender.MyInt = 2;
            // Unregistering now
            sender.MyIntChanged -= receiver.GetNotificationFromSender;
            // No notification sent for the receiver now.
            sender.MyInt = 3;

            Console.ReadKey();
        }
    }
}

输出

以下是运行该程序的输出。

***Exploring an event with an interface.***
Receiver receives a notification: Sender recently has changed the myInt value .
Receiver receives a notification: Sender recently has changed the myInt value .

问答环节

2.3 当接口事件同名时,我的类如何实现多个接口?

是的,这种情况很有意思。当您的类实现多个具有公共名称事件的接口时,您需要遵循显式接口实现技术。但是有一个重要的限制,即在这种情况下,您需要提供添加和移除事件访问器。通常,编译器可以提供这些访问器,但在这种情况下,它不能。下一节提供了完整的演示。

处理显式接口事件

为了简单起见,这个例子与前面的例子一致。我们假设现在你有两个接口:IBeforeInterfaceIAfterInterface。进一步假设每个包含一个名为MyIntChanged.的事件

Sender类实现了这些接口。现在你有两个接收者:ReceiverBeforeReceiverAfter。当myInt改变时,这些接收者类想要得到通知。在这个例子中,ReceiverBefore对象在myInt改变之前得到通知,而ReceiverAfter对象在myInt改变之后得到通知。

在演示 4 中,您看到了如何实现事件访问器。这里遵循相同的机制。这一次,我遵循了微软的建议,所以您可以看到锁在事件访问器中的使用。

演示 6

完成下面的完整演示。

using System;

namespace EventEx6
{
    interface IBeforeInterface
    {
        public event EventHandler MyIntChanged;
    }
    interface IAfterInterface
    {
        public event EventHandler MyIntChanged;
    }
    class Sender : IBeforeInterface, IAfterInterface
    {
        // Creating two separate events for two interface events
        public event EventHandler BeforeMyIntChanged;
        public event EventHandler AfterMyIntChanged;
        // Microsoft recommends this, i.e. to use a lock inside accessors
        object objectLock = new Object();

        private int myInt;
        public int MyInt
        {
            get
            {
                return myInt;
            }
            set
            {
                // Fire an event before we make a change to myInt.
                OnMyIntChangedBefore();
                Console.WriteLine("Making a change to myInt from {0} to {1}.",myInt,value);
                myInt = value;
                // Fire an event after we make a change to myInt.
                OnMyIntChangedAfter();
            }
        }
        // Explicit interface implementation required.
        // Associate IBeforeInterface's event with
        // BeforeMyIntChanged
        event EventHandler IBeforeInterface.MyIntChanged
        {
            add
            {
                lock (objectLock)
                {
                    BeforeMyIntChanged += value;
                }
            }

            remove
            {
                lock (objectLock)
                {
                    BeforeMyIntChanged -= value;
                }
            }
        }
        // Explicit interface implementation required.
        // Associate IAfterInterface's event with
        // AfterMyIntChanged

        event EventHandler IAfterInterface.MyIntChanged
        {
            add
            {
                lock (objectLock)
                {
                    AfterMyIntChanged += value;
                }
            }

            remove
            {
                lock (objectLock)
                {
                    AfterMyIntChanged -= value;
                }
            }
        }
        // This method uses BeforeMyIntChanged event
        protected virtual void OnMyIntChangedBefore()
        {
            if (BeforeMyIntChanged != null)
            {
                BeforeMyIntChanged(this, EventArgs.Empty);
            }
        }
        // This method uses AfterMyIntChanged event
        protected virtual void OnMyIntChangedAfter()
        {
            if (AfterMyIntChanged != null)
            {
                AfterMyIntChanged(this, EventArgs.Empty);
            }
        }
    }
    // First receiver: ReceiverBefore class
    class ReceiverBefore
    {
        public void GetNotificationFromSender(Object sender, System.EventArgs e)
        {
            Console.WriteLine("ReceiverBefore receives : Sender is about to change the myInt value . ");
        }
    }
    // Second receiver: ReceiverAfter class
    class ReceiverAfter
    {
        public void GetNotificationFromSender(Object sender, System.EventArgs e)
        {
            Console.WriteLine("ReceiverAfter receives : Sender recently has changed the myInt value . ");
        }
    }
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("***Handling explicit interface events.***");
            Sender sender = new Sender();
            ReceiverBefore receiverBefore = new ReceiverBefore();
            ReceiverAfter receiverAfter = new ReceiverAfter();
            // Receiver's are registering for getting //notifications from Sender
            sender.BeforeMyIntChanged += receiverBefore.GetNotificationFromSender;
            sender.AfterMyIntChanged += receiverAfter.GetNotificationFromSender;

            sender.MyInt = 1;
            Console.WriteLine("");
            sender.MyInt = 2;
            // Unregistering now
            sender.BeforeMyIntChanged -= receiverBefore.GetNotificationFromSender;
            sender.AfterMyIntChanged -= receiverAfter.GetNotificationFromSender;
            Console.WriteLine("");
            // No notification sent for the receivers now.
            sender.MyInt = 3;

            Console.ReadKey();
        }
    }
}

输出

以下是运行该程序的输出。

***Handling explicit interface events.***
ReceiverBefore receives : Sender is about to change the myInt value .
Making a change to myInt from 0 to 1.
ReceiverAfter receives : Sender recently has changed the myInt value .

ReceiverBefore receives : Sender is about to change the myInt value .
Making a change to myInt from 1 to 2.
ReceiverAfter receives : Sender recently has changed the myInt value .

Making a change to myInt from 2 to 3.

问答环节

2.4 代理是事件的支柱,一般来说,当我们为事件编写代码以及注册和注销这些事件时,我们遵循观察者设计模式。这是正确的吗?

是的。

在这一章的开始,你说当我写一个关于某个事件的程序时,我也可以使用“new”关键字。能举个例子吗?

我基本上用的是简写形式。例如,在演示 1 中,当我注册事件时,您会看到下面一行代码。

sender.MyIntChanged += receiver.GetNotificationFromSender;

现在,如果您回忆一下在第 1 章的委托上下文中使用的缩写形式,您可以编写等价的代码,如下所示。

sender.MyIntChanged += new EventHandler(receiver.GetNotificationFromSender);

除此之外,考虑另一种情况,发送者类包含一个密封的事件。如果您有 Sender 的派生类,则它不能使用事件。相反,派生类可以使用“new”关键字来指示它没有重写基类事件。

你能举一个抽象事件的例子吗?

见演示 7。

演示 7

微软表示,对于一个抽象事件,你不会得到编译器生成的addremove事件访问器块。所以,你的派生类需要提供自己的实现。让我们简化一下,稍微修改一下演示 1。像演示 2 一样,让我们假设在这个例子中,发送者不需要向自己发送通知。在这个演示中,Sender类中没有GetNotificationItself方法。

现在我们来关注关键部分。Sender 类包含一个抽象事件,如下所示。

public abstract event EventHandler MyIntChanged;

由于该类包含一个抽象事件,因此该类本身也变得抽象。

我现在将介绍另一个名为ConcreteSender的类,它派生自 Sender。它重写事件并完成事件调用过程。

下面是ConcreteSender的实现。

    class ConcreteSender : Sender
    {
        public override event EventHandler MyIntChanged;
        protected override void OnMyIntChanged()
        {
            if (MyIntChanged != null)
            {
                MyIntChanged(this, EventArgs.Empty);
            }
        }
    }

现在我们来看一下完整的程序和输出。

using System;

namespace EventsEx7
{
    abstract class Sender
    {
        private int myInt;
        public int MyInt
        {
            get
            {
                return myInt;
            }
            set
            {
                myInt = value;
               // Whenever we set a new value, the event will fire.
                OnMyIntChanged();
            }
        }
        // Abstract event.The containing class becomes abstract for this.
        public abstract event EventHandler MyIntChanged;
        protected virtual void OnMyIntChanged()
        {
            Console.WriteLine("Sender.OnMyIntChanged");
        }
    }
    class ConcreteSender : Sender
    {
        public override event EventHandler MyIntChanged;
        protected override void OnMyIntChanged()
        {
            if (MyIntChanged != null)
            {
                MyIntChanged(this, EventArgs.Empty);
            }
        }
    }

class Receiver
{
    public void GetNotificationFromSender(Object sender, System.EventArgs e)
    {
        Console.WriteLine("Receiver receives a notification: Sender recently has changed the myInt value . ");
    }

}
class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine("***Exploring an abstract event.***");
        Sender sender = new ConcreteSender();
        Receiver receiver = new Receiver();
        // Receiver is registering for a notification from sender
        sender.MyIntChanged += receiver.GetNotificationFromSender;
        sender.MyInt = 1;
        sender.MyInt = 2;
        // Unregistering now
        sender.MyIntChanged -= receiver.GetNotificationFromSender;
        // No notification sent for the receiver now.
        sender.MyInt = 3;

        Console.ReadKey();
    }
}}

输出

以下是运行该程序的输出。

***Exploring an abstract event.***
Receiver receives a notification: Sender recently has changed the myInt value .
Receiver receives a notification: Sender recently has changed the myInt value .

问答环节

2.7 我知道 EventHandler 是一个预定义的代表。但是在很多地方,我看到人们在广义上使用术语 事件处理程序 。有什么特别的含义与之相关吗?

简单地说,事件处理程序是一个过程,当一个特定的事件发生时,你决定做什么。例如,当用户点击 GUI 应用中的按钮时。请注意,您的事件可以有多个处理程序,同时,处理事件的方法也可以动态变化。在本章中,你看到了事件是如何工作的,特别是 Receiver 类是如何处理事件的。但是如果您使用像 Visual Studio 中的 Windows 窗体设计器这样的现成构造,就可以非常容易地编写事件代码。

有一个如何在 GUI 应用中添加事件处理程序的例子会很有帮助。

让我们看看演示 8。

演示 8

在这个演示中,我创建了一个简单的 UI 应用来演示一个简单的事件处理机制。做这件事的步骤如下。

  1. 创建 Windows 窗体应用。

  2. From the Toolbox, drag a button onto the form. Let’s name it Test. Figure 2-2 shows what it may look like.

    img/494901_1_En_2_Fig2_HTML.jpg

    图 2-2

    放置在 Form1 上的测试按钮

  3. Select the button. Open the Properties window and click the Events button. Name the Click event TestBtnClickHandler (see Figure 2-3).

    img/494901_1_En_2_Fig3_HTML.jpg

    图 2-3

    将点击事件名称设置为TestBtnClickHandler

  4. 双击测试按钮。这将打开 Form1.cs 文件,您可以在其中为事件处理程序编写以下代码。

    private void TestBtnClickHandler(object sender, EventArgs e)
    {
     MessageBox.Show("Hello Reader.");
    }

输出

运行您的应用并单击 Test 按钮。您会看到如图 2-4 所示的输出。(为了更好的截图,我在 Form1 上拖动了消息框窗口。)

img/494901_1_En_2_Fig4_HTML.jpg

图 2-4

单击“测试”按钮时,从 Visual Studio 输出屏幕截图

Note

演示 8 于年执行。但在. NET Framework 中没有。NET 核心。在撰写本文时,可视化设计器被标记为的“预览功能”。NET 核心应用,它受到了很多问题的困扰(更多信息,请访问 https://github.com/dotnet/winforms/blob/master/Documentation/designer-releases/0.1/knownissues.md )。在解决方案资源管理器中单击 Form1.cs 文件时,在. NET 核心应用中看不到 Form1.cs[Design]。

最后的话

在演示 2 中,您看到了下面的代码段。

if (MyIntChanged != null)
{
    MyIntChanged(this, EventArgs.Empty);
}

实际上,在所有示例中,在引发事件之前都会看到这种空检查。这很重要,因为如果事件没有监听器(或接收器),您可能会遇到一个名为NullReferenceException的异常。在这种情况下,Visual Studio 会向您显示如图 2-5 所示的屏幕。

img/494901_1_En_2_Fig5_HTML.jpg

图 2-5

由于缺少事件侦听器和正确的空检查,发生了 NullReferenceException

在引发事件之前,空检查非常重要。但是你可以假设在一个真实的应用中,如果你需要做一些空值检查,这会让你的代码变得笨拙。在这种情况下,您可以使用从 C# 6.0 开始就有的功能。您可以使用空条件操作符来避免突然的意外。

我使用这个操作符提供了另一个代码段。(我保留了带注释的死代码,以便您可以同时比较两个代码段)。

//if (MyIntChanged != null)
//{
//    MyIntChanged(this, EventArgs.Empty);
//}
//Alternate code
MyIntChanged?.Invoke(this, EventArgs.Empty);

这都是关于事件的。现在让我们继续第 3 章,在这里你将学习使用 C# 中另一个强大的特性:lambda 表达式。

摘要

本章讨论了以下关键问题。

  • 什么是事件?如何使用内置的事件支持?

  • 如何编写自定义事件?

  • 如何将数据传递给事件参数?

  • 如何使用事件访问器?它们为什么有用?

  • 如何使用不同的界面事件?

  • 如何将不同的修饰语和关键词应用到一个事件中?

  • 如何在简单的 UI 应用中实现事件处理机制?