基于Windows的应用程序也是基于消息的。这说明,应用程序是通过Windows来通信的,Windows又是使用预定义的消息与应用程序通信的。这些消息是包含各种信息的结构,应用程序和Windows使用这些信息决定下一步的操作。在MFC等库或Visual Basic等开发环境推出之前,开发人员必须处理Windows发送给应用程序的消息。Visual Basic和今天的.NET把这些传送来的消息封装在事件中。如果需要响应某个消息,就应处理对应的事件。一个常见的例子是用户单击了窗体中的按钮后,Windows就会给按钮消息处理程序(有时称为Windows过程或WndProc)发送一个WM_MOUSECLICK消息。对于.NET开发人员来说,这就是按钮的Click事件。
在开发基于对象的应用程序时,需要使用另一种对象通信方式。在一个对象中发生了有趣的事情时,就需要通知其他对象发生了什么变化。这里又要用到事件。就像.NET Framework把Windows消息封装在事件中那样,也可以把事件用作对象之间的通信介质。
委托就用作应用程序接收到消息时封装事件的方式。在上一节介绍委托时,仅讨论了理解事件如何工作所需要的内容。但Microsoft设计C#事件的目的是让用户无需理解底层的委托,就可以使用它们。所以下面开始从客户软件的角度讨论事件,主要考虑的是需要编写什么代码来接收事件通知,而无需担心后台上究竟发生了什么,从中可以看出事件的处理十分简单。之后,编写一个生成事件的示例,介绍事件和委托之间的关系。
本节的内容对C++开发人员最有用,因为C++没有与事件类似的概念。另一方面,C#事件与Visual Basic事件非常类似,但C#中的语法和底层的实现有所不同。
注意:
这里的术语“事件”有两种不同的含义。第一,表示发生了某件有趣的事情;第二,表示C#语言中已定义的一个对象,即处理通知过程的对象。在使用第二个含义时,我们常常把事件表示为C#事件,或者在其含义很容易从上下文中看出时,就表示为事件。
7.2.1 从接收器的角度讨论事件
事件接收器是指在发生某些事情时被通知的任何应用程序、对象或组件。当然,有事件接收器,就有事件发送器。发送器的作用是引发事件。发送器可以是应用程序中的另一个对象或程序集,在系统事件中,例如鼠标单击或键盘按键,发送器就是.NET运行库。注意,事件的发送器并不知道接收器是谁。这就使事件非常有用。
现在,在事件接收器的某个地方有一个方法,它负责处理事件。在每次发生已注册的事件时,就执行这个事件处理程序。此时就要使用委托了。由于发送器对接收器一无所知,所以无法设置两者之间的引用类型,而是使用委托作为中介。发送器定义接收器要使用的委托,接收器将事件处理程序注册到事件中。连接事件处理程序的过程称为封装事件。封装Click事件的简单例子有助于说明这个过程。
首先创建一个简单的Windows窗体应用程序,把一个按钮控件从工具箱拖放到窗体上。在属性窗口中把按钮重命名为buttonOne。在代码编辑器中把下面的代码添加到Form1构造函数中:
public Form1()
{
InitializeComponent();
buttonOne.Click += new EventHandler(Button_Click);
}
在Visual Studio中,注意在输入+=运算符之后,就只需按下Tab键两次,编辑器就会完成剩余的输入工作。在大多数情况下这很不错。但在这个例子中,不使用默认的处理程序名,所以应自己输入文本。
这将告诉运行库,在引发buttonOne的Click事件时,应执行Button_Click方法。EventHandler是事件用于把处理程序(Button_Click)赋予事件(Click)的委托。注意使用+=运算符把这个新方法添加到委托列表中。这类似于本章前面介绍的多播示例。也就是说,可以为事件添加多个事件处理程序。由于这是一个多播委托,所以要遵循添加多个方法的所有规则,但是不能保证调用方法的顺序。下面在窗体上再添加一个按钮,把它重命名为buttonTwo。把buttonTwo的Click事件也连接到同一个Button_Click方法上,如下所示:
buttonOne.Click += new EventHandler(Button_Click);
buttonTwo.Click += new EventHandler(Button_Click);
利用委托推断,可以编写下面的代码。编译器会生成与前面相同的代码。
buttonOne.Click += Button_Click;
buttonTwo.Click += Button_Click;
EventHandler委托已在.NET Framework中定义了。它位于System命名空间,所有在.NET Framework中定义的事件都使用它。如前所述,委托要求添加到委托列表中的所有方法都必须有相同的签名。显然事件委托也有这个要求。下面是Button_Click方法的定义:
Private void Button_Click(object sender, Eventargs e)
{
}
这个方法有几个重要的地方。首先,它总是返回void。事件处理程序不能有返回值。其次是参数。只要使用EventHandler委托,参数就应是object和EventArgs。第一个参数是引发事件的对象,在这个例子中是buttonOne或buttonTwo,这取决于被单击的按钮。把一个引用发送给引发事件的对象,就可以把同一个事件处理程序赋予多个对象。例如,可以为几个按钮定义一个按钮单击处理程序,接着根据sender参数确定单击了哪个按钮。
第二个参数EventArgs是包含有关事件的其他有用信息的对象。这个参数可以是任意类型,只要它派生自EventArgs即可。MouseDown事件使用MouseDownEventArgs,它包含所使用按钮的属性、指针的X和Y坐标,以及与事件相关的其他信息。注意,其命名模式是在类型的后面加上EventArgs。本章的后面将介绍如何创建和使用基于EventArgs的定制对象。
方法的命名也应注意。按照约定,事件处理程序应遵循“object_event”的命名约定。object就是引发事件的对象,而event就是被引发的事件。从可读性来看,应遵循这个命名约定。
本例最后在处理程序中添加了一些代码,以完成一些工作。记住有两个按钮使用同一个处理程序。所以首先必须确定是哪个按钮引发了事件,接着调用应执行的操作。在本例中,只是在窗体的一个标签控件上输出一些文本。把一个标签控件从工具箱拖放到窗体上,并将其命名为labelInfo,然后在Button_Click方法中编写如下代码:
if(((Button)sender).Name == "buttonOne")
labelInfo.Text = "Button One was pressed";
else
labelInfo.Text = "Button Two was pressed";
注意,由于sender参数作为对象发送,所以必须把它的数据类型转换为引发事件的对象类型,在本例中就是Button。本例使用Name属性确定是哪个按钮引发了对象,也可以使用其他属性。例如Tag属性就可以处理这种情形,因为它可以包含任何内容。为了了解事件委托的多播功能,给buttonTwo的Click事件添加另一个方法。窗体的构造函数如下所示:
buttonOne.Click += new EventHandler(Button_Click);
buttonTwo.Click += new EventHandler(Button_Click);
buttonTwo.Click += new EventHandler(button2_Click);
如果让Visual Studio创建存根(stub),就会在源文件的末尾得到如下方法。但是,必须添加对MessageBox.Show()函数的调用:
Private void button2_Click(object sender, EventArgs e)
{
MessageBox.Show("This only happens in Button 2 click event");
}
如果使用l表达式,就不需要Button_Click方法和Button2_Click方法了。事件的代码如下:
buttonOne.Click += (sender, e) = > labelInfo.Text = "Button One was pressed";
buttonTwo.Click += (sender, e) = > labelInfo.Text = "Button Two was pressed";
buttonTwo.Click += (sender, e) = >
{
MessageBox.Show("This only happens in Button 2 click event");
};
在运行这个例子时,单击buttonOne会改变标签上的文本。单击buttonTwo不仅会改变文本,还会显示消息框。注意,不能保证标签文本在消息框显示之前改变,所以不要在处理程序中编写具有依赖性的代码。
我们已经学习了许多概念,但要在接收器中编写的代码量是很小的。记住,编写事件接收器常常比编写事件发送器要频繁得多。至少在Windows用户界面上,Microsoft已经编写了所有需要的事件发送器(它们都在.NET基类中,在Windows.Forms命名空间中)。
7.2.2 生成事件
接收事件并响应它们仅是事件的一个方面。为了使事件真正发挥作用,还需要在代码中生成和引发事件。下面的例子将介绍如何创建、引发、接收和取消事件。
这个例子包含一个窗体,它会引发另一个类正在监听的事件。在引发事件后,接收对象就确定是否执行一个过程,如果该过程未能继续,就取消事件。本例的目标是确定当前时间的秒数是大于30还是小于30。如果秒数小于30,就用一个表示当前时间的字符串设置一个属性;如果秒数大于30,就取消事件,把时间字符串设置为空。
用于生成事件的窗体包含一个按钮和一个标签。下载的示例代码把按钮命名为buttonRaise,标签命名为labelInfo。在创建窗体,添加两个控件后,就可以创建事件和相应的委托了。在窗体类的类声明部分,添加如下代码:
public delegate void ActionEventHandler(object sender, ActionCancelEventArgs ev);
public static event ActionEventHandler Action;
这两行代码的作用是什么?首先,我们声明了一种新的委托类型ActionEventHandler。必须创建一种新委托,而不使用.NET Framework预定义的委托,其原因是后面要使用定制的EventArgs类。方法签名必须与委托匹配。有了一个要使用的委托后,下一行代码就定义事件。在本例中定义了Action事件,定义事件的语法要求指定与事件相关的委托。还可以使用在.NET Framework中定义的委托。从EventArgs类中派生出了近100个类,应该可以找到一个自己能使用的类。但本例使用的是定制的EventArgs类,所以必须创建一个与之匹配的新委托类型。
在一行代码中定义事件是C#中的一个缩写方式,它可以定义添加和删除处理程序的方法,声明委托的一个变量。除了编写一行代码之外,还可以用下面的代码达到相同的效果。声明一个事件类型的变量以及添加和删除事件处理程序的方法。在定义添加和删除事件处理程序的方法时,其语法非常类似于属性。变量值的定义也类似于添加和删除事件处理程序。
private static ActionEventHandler action;
public static event ActionEventHandler Action
{
add
{
action += value;
}
remove
{
action -= value;
}
}
提示:
如果不仅仅需要添加和删除事件处理程序,就可以使用定义事件的较长记号。例如,要为多个线程访问添加同步功能。WPF控件就使用这种较长的记号给事件添加起泡和通道功能。事件的起泡和通道功能详见第34章。
基于EventArgs的新类ActionCancelEventHandler实际上派生自CancelEventArgs,而CancelEventArgs派生自EventArgs。CancelEventArgs添加了Cancel属性,该属性是一个布尔值,它通知sender对象,接收器希望取消或停止事件的处理。在ActionEventHandler类中,还添加了Message属性,这是一个字符串属性,包含事件处理状态的文本信息。下面是ActionCancelEventHandler类的代码:
public class ActionCancelEventArgs : System.ComponentModel.CancelEventArgs
{
public ActionCancelEventArgs() : this(false) {}
public ActionCancelEventArgs(bool cancel) : this(false, String.Empty) {}
public ActionCancelEventArgs(bool cancel, string message) : base(cancel)
{
this.message = message;
}
public string Message{ get; set;}
}
可以看出,所有基于EventArgs的类都负责在发送器和接收器之间来回传送事件的信息。在大多数情况下,EventArgs类中使用的信息都由事件处理程序中的接收器对象使用。但是,有时事件处理程序可以把信息添加到EventArgs类中,使之可用于发送器。这就是本例使用EventArgs类的方式。注意在EventArgs类中有两个可用的构造函数。这种额外的灵活性增加了该类的可用性。
目前声明了一个事件,定义了一个委托,并创建了EventArgs类。下一步需要引发事件。真正需要做的是用正确的参数调用事件,如本例所示:
ActionCancelEventArgs e = new ActionCancelEventArgs();
Action(this, e);
这非常简单。创建新的ActionCancelEventArgs类,并把它作为一个参数传递给事件。但是,这有一个小问题。如果事件不会在任何地方使用,该怎么办?如果还没有为事件定义处理程序,该怎么办?Action事件实际上是空的。如果试图引发该事件,就会得到一个空引用异常。如果要派生一个新的窗体类,并使用该窗体,把Action事件定义为基事件,则只要引发了Action事件,就必须执行其他一些操作。目前,我们必须在派生的窗体中激活另一个事件处理程序,这样才能访问它。为了使这个过程容易一些,并捕获空引用错误,就必须创建一个方法OnEventName,其中EventName是事件名。在这个例子中,有一个OnAction方法,下面是OnAction方法的完整代码:
protected void OnAction(object sender, ActionCancelEventArgs ev)
{
if(Action != null)
{
Action(sender, ev);
}
}
代码并不多,但完成了需要的工作。把该方法声明为protected,就只有派生类可以访问它。事件在引发之前还会进行空引用测试。如果派生一个包含该方法和事件的新类,就必须重写OnAction方法,然后连接事件。为此,必须在重写代码中调用base.OnAction()。否则就不会引发该事件。在整个.NET Framework中都用这个命名约定,并在.NET SDK文档中对这一命名规则进行了说明。
注意传送给OnAction方法的两个参数。它们看起来很熟悉,因为它们与需要传送给事件的参数相同。如果事件需要从另一个对象中引发,而不是从定义方法的对象中引发,就需要把访问修饰符设置为internal或public,而不能设置为protected。有时让类只包含事件声明,这些事件从其他类中调用是有意义的。仍可以创建OnEventName方法,但此时它们是静态方法。
目前,我们已经引发了事件,还需要一些代码来处理它。在项目中创建一个新类BusEntity。本项目的目的是检查当前时间的秒数,如果它小于30,就把一个字符串值设置为时间;如果它大于30,就把字符串设置为::,并取消事件。下面是代码:
using System;
using System.IO;
using System.ComponentModel;
namespace Wrox.ProCSharp.Delegates
{
public class BusEntity
{
string time = String.Empty;
public BusEntity()
{
Form1.Action += new Form1.ActionEventHandler(Form1_Action);
}
private void Form1_Action(object sender, ActionCancelEventArgs e)
{
e.Cancel = !DoActions();
if(e.Cancel)
e.Message = "Wasn’t the right time.";
}
private bool DoActions()
{
bool retVal = false;
DateTime tm = DateTime.Now;
if(tm.Second < 30)
{
time = "The time is " + DateTime.Now.ToLongTimeString();
retVal = true;
}
else
time = "";
return retVal;
}
public string TimeString
{
get {return time;}
}
}
}
在构造函数中声明了Form1.Action事件的处理程序。注意其语法非常类似于前面Click事件的语法。由于声明事件使用的模式都是相同的,所以语法也应保持一致。还要注意如何获取Action事件的引用,而无需在BusEntity类中引用Form1。在Form1类中,将Action事件声明为静态,这并不是必需的,但这样更易于创建处理程序。我们可以把事件声明为public,但接着需要引用Form1的一个实例。
在构造函数中编写事件时,调用添加到委托列表中的方法Form1_Action,并遵循命名标准。在处理程序中,需要确定是否取消事件。DoActions方法根据前面描述的时间条件返回一个布尔值,并把_time字符串设置为正确的值。
之后,把DoActions的返回值赋给ActionCancelEventArgs的Cancel属性。EventArgs类一般仅在事件发送器和接收器之间来回传递值。如果取消了事件(ev.Cancel = true),Message属性就设置为一个字符串值,以说明事件为什么被取消。
如果再次查看buttonRaise_Click事件处理程序的代码,就可以看出Cancel属性的使用方式:
private void buttonRaise_Click(object sender, EventArgs e)
{
ActionCancelEventArgs cancelEvent = new ActionCancelEventArgs();
OnAction(this, cancelEvent);
if(cancelEvent.Cancel)
labelInfo.Text = cancelEvent.Message;
else
labelInfo.Text = busEntity.TimeString;
}
注意,创建了ActionCancelEventArgs对象。接着引发了事件Action,并传递了新建的ActionCancel EventArgs对象。在调用OnAction方法,引发事件时,BusEntity对象中Action事件处理程序的代码就会执行。如果还有其他对象注册了事件Action,它们也会执行。记住,如果其他对象也处理这个事件,它们就会看到同一个ActionCancelEventArgs对象。如果需要确定是哪个对象取消了事件,而且有多个对象取消了事件,就需要在ActionCancel EventArgs类中包含某种基于列表的数据结构。
在与事件委托一起注册的处理程序执行完毕后,就可以查询ActionCancelEventArgs对象,确定它是否被取消了。如果是,labelInfo就包含Message属性值;如果事件没有被取消,labelInfo就会显示当前时间。
本节这基本上说明了如何利用事件和事件中基于EventArgs的对象,在应用程序中传递信息。