对事件的支持被认为是 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
是相应的代表。 -
修饰符不需要是公共的。你可以为你的事件选择非公开的修饰语,比如
private
、protected
、internal
等等。在这种情况下,你也可以使用关键字static
、virtual
、override
、abstract
、sealed
和new
。
现在您已经准备好编码了。在声明事件之前,您需要一个委托。在示例中,您会看到下面的代码行。
public event EventHandler MyIntChanged;
但是您看不到委托声明,因为我使用了预定义的EventHandler
委托。
现在让我们关注我们的实现。有两类:Sender
和Receiver
。Sender
扮演广播员的角色;当您更改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
的方法。
为了使更改与前面的示例保持一致,让我们按照以下步骤操作。
-
创建代理人。按照惯例,选择带
EventHandler
后缀的代理名称;大概如下:delegate void MyIntChangedEventHandler(Object sender, EventArgs eventArgs);
-
定义您的活动。按照惯例,您可以去掉代理名称的后缀
EventHandler
并设置您的事件名称。public event MyIntChangedEventHandler MyIntChanged;
-
引发事件。让我们在 Sender 类中使用下面的方法。一般情况下,不做方法
public
,建议你做方法protected virtual
。protected virtual void OnMyIntChanged() { if (MyIntChanged != null) { MyIntChanged(this, EventArgs.Empty); } }
-
处理事件。让我们使用一个
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 . "); } }
现在进行完整的演示。
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 中分析这样一个案例。
在这个演示中,我遵循了这些步骤。
-
创建
EventArgs
的子类。这个类有一个JobNo property
来设置jobNo
实例变量的值。 -
修改
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); }}
-
在这次演示中,我保持了相同的步骤。
这是完整的演示。
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);
}
}
现在如果你执行这个程序,你会得到同样的输出。这怎么可能?编译器的工作方式类似于您声明事件时的方式。让我们回到事件的基本原理。
事件是一种特殊的多播委托,您只能从包含该事件的类中调用它。接收者可以订阅该事件,并使用其中的方法处理该事件。因此,接收者在订阅事件时传递方法引用。因此,此方法通过事件访问器添加到委托的订阅列表中。这些事件访问器类似于属性访问器,只是它们被命名为add
和remove
。
通常,您不需要提供自定义事件访问器。但是当您定义它们时,您是在指示 C# 编译器不要为您生成默认的字段和访问器。
在撰写本文时,基于。NET 框架目标 c# 7.3;鉴于。NET 核心应用面向 C# 8.0。如果您在。NET Framework(我们将其重命名为EventEx3DotNetFramework
)并研究 IL 代码,您会注意到 IL 代码中出现了add_<EventName>
和remove_<EventName>
。图 2-1 是 IL 代码的部分截图。
图 2-1
IL 代码的部分截图
我们来做一个完整的演示,如下。
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;
}
}
注意,这些事件访问器类似于属性访问器,除了它们被命名为add
和remove
。这里你在你的委托周围使用了一个类似属性的包装。因此,只有包含类可以直接调用委托;外人不能这么做。这促进了更好的安全性和对代码的控制。
接口可以包含事件。当您实现接口方法或属性时,您需要遵循相同的规则。以下示例显示了这样的实现。
在这个例子中,IMyInterface
有一个MyIntChanged
事件。我使用了Sender
和Receiver
,它们与前面的例子相同。唯一不同的是,这一次,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 当接口事件同名时,我的类如何实现多个接口?
是的,这种情况很有意思。当您的类实现多个具有公共名称事件的接口时,您需要遵循显式接口实现技术。但是有一个重要的限制,即在这种情况下,您需要提供添加和移除事件访问器。通常,编译器可以提供这些访问器,但在这种情况下,它不能。下一节提供了完整的演示。
为了简单起见,这个例子与前面的例子一致。我们假设现在你有两个接口:IBeforeInterface
和IAfterInterface
。进一步假设每个包含一个名为MyIntChanged.
的事件
Sender
类实现了这些接口。现在你有两个接收者:ReceiverBefore
和ReceiverAfter
。当myInt
改变时,这些接收者类想要得到通知。在这个例子中,ReceiverBefore
对象在myInt
改变之前得到通知,而ReceiverAfter
对象在myInt
改变之后得到通知。
在演示 4 中,您看到了如何实现事件访问器。这里遵循相同的机制。这一次,我遵循了微软的建议,所以您可以看到锁在事件访问器中的使用。
完成下面的完整演示。
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。
微软表示,对于一个抽象事件,你不会得到编译器生成的add
和remove
事件访问器块。所以,你的派生类需要提供自己的实现。让我们简化一下,稍微修改一下演示 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。
在这个演示中,我创建了一个简单的 UI 应用来演示一个简单的事件处理机制。做这件事的步骤如下。
-
创建 Windows 窗体应用。
-
From the Toolbox, drag a button onto the form. Let’s name it Test. Figure 2-2 shows what it may look like.
图 2-2
放置在 Form1 上的测试按钮
-
Select the button. Open the Properties window and click the Events button. Name the Click event
TestBtnClickHandler
(see Figure 2-3).图 2-3
将点击事件名称设置为
TestBtnClickHandler
-
双击测试按钮。这将打开 Form1.cs 文件,您可以在其中为事件处理程序编写以下代码。
private void TestBtnClickHandler(object sender, EventArgs e) { MessageBox.Show("Hello Reader."); }
运行您的应用并单击 Test 按钮。您会看到如图 2-4 所示的输出。(为了更好的截图,我在 Form1 上拖动了消息框窗口。)
图 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 所示的屏幕。
图 2-5
由于缺少事件侦听器和正确的空检查,发生了 NullReferenceException
在引发事件之前,空检查非常重要。但是你可以假设在一个真实的应用中,如果你需要做一些空值检查,这会让你的代码变得笨拙。在这种情况下,您可以使用从 C# 6.0 开始就有的功能。您可以使用空条件操作符来避免突然的意外。
我使用这个操作符提供了另一个代码段。(我保留了带注释的死代码,以便您可以同时比较两个代码段)。
//if (MyIntChanged != null)
//{
// MyIntChanged(this, EventArgs.Empty);
//}
//Alternate code
MyIntChanged?.Invoke(this, EventArgs.Empty);
这都是关于事件的。现在让我们继续第 3 章,在这里你将学习使用 C# 中另一个强大的特性:lambda 表达式。
本章讨论了以下关键问题。
-
什么是事件?如何使用内置的事件支持?
-
如何编写自定义事件?
-
如何将数据传递给事件参数?
-
如何使用事件访问器?它们为什么有用?
-
如何使用不同的界面事件?
-
如何将不同的修饰语和关键词应用到一个事件中?
-
如何在简单的 UI 应用中实现事件处理机制?