第7章 委托和事件
2011年01月07日
回调(callback)函数是Windows编程的一个重要部分。如果您具备C或C++编程背景,应该就曾在许多Windows API中使用过回调。Visual Basic添加了AddressOf关键字后,开发人员就可以利用以前一度受到限制的API了。回调函数实际上是方法调用的指针,也称为函数指针,是一个非常强大的编程特性。.NET以委托的形式实现了函数指针的概念。它们的特殊之处是,与C函数指针不同,.NET委托是类型安全的。这说明,C中的函数指针只不过是一个指向存储单元的指针,我们无法说出这个指针实际指向什么,像参数和返回类型等就更无从知晓了。如本章所述,.NET把委托作为一种类型安全的操作。本章后面将学习.NET如何将委托用作实现事件的方式。
本章的主要内容如下:
● 委托
● 匿名方法
● 表达式
● 事件 当要把方法传送给其他方法时,需要使用委托。要了解它们的含义,可以看看下面的代码: 我们习惯于把数据作为参数传递给方法,如上面的例子所示。所以,给方法传送另一个方法听起来有点奇怪。而有时某个方法执行的操作并不是针对数据进行的,而是要对另一个方法进行操作,这就比较复杂了。在编译时我们不知道第二个方法是什么,这个信息只能在运行时得到,所以需要把第二个方法作为参数传递给第一个方法,这听起来很令人迷惑,下面用几个示例来说明:
● 启动线程-- 在C#中,可以告诉计算机并行运行某些新的执行序列。这种序列就称为线程,在基类System.Threading.Thread的一个实例上使用方法Start(),就可以开始执行一个线程。如果要告诉计算机开始一个新的执行序列,就必须说明要在哪里执行该序列。必须为计算机提供开始执行的方法的细节,即Thread类的构造函数必须带有一个参数,该参数定义了要由线程调用的方法。
● 通用库类-- 有许多库包含执行各种标准任务的代码。这些库通常可以自我包含。这样在编写库时,就会知道任务该如何执行。但是有时在任务中还包含子任务,只有使用该库的客户机代码才知道如何执行这些子任务。例如编写一个类,它带有一个对象数组,并把它们按升序排列。但是,排序的部分过程会涉及到重复使用数组中的两个对象,比较它们,看看哪一个应放在前面。如果要编写的类必须能给任何对象数组排序,就无法提前告诉计算机应如何比较对象。处理类中对象数组的客户机代码也必须告诉类如何比较要排序的对象。换言之,客户机代码必须给类传递某个可以进行这种比较的合适方法的细节。
● 事件-- 一般是通知代码发生了什么事件。GUI编程主要是处理事件。在发生事件时,运行库需要知道应执行哪个方法。这就需要把处理事件的方法传送为委托的一个参数。这些将在本章后面讨论。
在C和C++中,只能提取函数的地址,并传送为一个参数。C是没有类型安全性的。可以把任何函数传送给需要函数指针的方法。这种直接的方法会导致一些问题,例如类型的安全性,在进行面向对象编程时,方法很少是孤立存在的,在调用前,通常需要与类实例相关联。而这种方法并没有考虑到这个问题。所以.NET Framework在语法上不允许使用这种直接的方法。如果要传递方法,就必须把方法的细节封装在一种新类型的对象中,即委托。委托只是一种特殊的对象类型,其特殊之处在于,我们以前定义的所有对象都包含数据,而委托包含的只是方法的地址。 在C#中使用一个类时,分两个阶段。首先需要定义这个类,即告诉编译器这个类由什么字段和方法组成。然后(除非只使用静态方法)实例化类的一个对象。使用委托时,也需要经过这两个步骤。首先定义要使用的委托,对于委托,定义它就是告诉编译器这种类型的委托代表了哪种类型的方法,然后创建该委托的一个或多个实例。编译器在后台将创建表示该委托的一个类。
定义委托的语法如下: 在这个示例中,定义了一个委托IntMethodInvoker,并指定该委托的每个实例都包含一个方法的细节,该方法带有一个int参数,并返回void。理解委托的一个要点是它们的类型安全性非常高。在定义委托时,必须给出它所代表的方法签名和返回类型等全部细节。
提示:
理解委托的一种好方式是把委托当作给方法签名和返回类型指定名称。
假定要定义一个委托TwoLongsOp,该委托代表的方法有两个long型参数,返回类型为double。可以编写如下代码: 或者定义一个委托,它代表的方法不带参数,返回一个string型的值,则可以编写如下代码: 其语法类似于方法的定义,但没有方法体,定义的前面要加上关键字delegate。因为定义委托基本上是定义一个新类,所以可以在定义类的任何地方定义委托,既可以在另一个类的内部定义,也可以在任何类的外部定义,还可以在命名空间中把委托定义为顶层对象。根据定义的可见性,可以在委托定义上添加一般的访问修饰符:public、private、protected等: 注意:
实际上,"定义一个委托"是指"定义一个新类"。委托实现为派生自基类System. Multicast Delegate的类,System.MulticastDelegate又派生自基类System.Delegate。C#编译器知道这个类,会使用其委托语法,因此我们不需要了解这个类的具体执行情况,这是C#与基类共同合作,使编程更易完成的另一个示例。
定义好委托后,就可以创建它的一个实例,以存储特定方法的细节。
注意:
此处,在术语方面有一个问题。类有两个不同的术语:"类"表示较广义的定义,"对象"表示类的实例。但委托只有一个术语。在创建委托的实例时,所创建的委托的实例仍称为委托。必须从上下文中确定委托的确切含义。 下面的代码段说明了如何使用委托。这是在int上调用ToString()方法的一种相当冗长的方式: private delegate string GetAString();
static void Main()
{
int x = 40;
GetAString firstStringMethod = new GetAString(x.ToString);
Console.WriteLine("String is {0}" + firstStringMethod());
// With firstStringMethod initialized to x.ToString(),
// the above statement is equivalent to saying
// Console.WriteLine("String is {0}" + x.ToString()); } 在这段代码中,实例化了类型为GetAString的一个委托,并对它进行初始化,使它引用整型变量x的ToString()方法。在C#中,委托在语法上总是带有一个参数的构造函数,这个参数就是委托引用的方法。这个方法必须匹配最初定义委托时的签名。所以在这个示例中,如果用不带参数、返回一个字符串的方法来初始化firstStringMethod变量,就会产生一个编译错误。注意,int.ToString()是一个实例方法(不是静态方法),所以需要指定实例(x)和方法名来正确初始化委托。 下一行代码使用这个委托来显示字符串。在任何代码中,都应提供委托实例的名称,后面的括号中应包含调用该委托中的方法时使用的参数。所以在上面的代码中,Console.WriteLine()语句完全等价于注释语句中的代码行。
实际上,给委托实例提供括号与调用委托类的Invoke()方法完全相同。firstStringMethod是委托类型的一个变量,所以C#编译器会用firstStringMethod.Invoke()代替firstStringMethod()。 C# 2.0使用委托推断扩展了委托的语法。为了减少输入量,只要需要委托实例,就可以只传送地址的名称。这称为委托推断。只要编译器可以把委托实例解析为特定的类型,这个C#特性就是有效的。下面的示例用GetAString委托的一个新实例初始化了GetAString类型的变量firstStringMethod: 只要用变量x把方法名传送给变量firstStringMethod,就可以编写出作用相同的代码: C#编译器创建的代码是一样的。编译器会用firstStringMethod检测需要的委托类型,因此创建GetAString委托类型的一个实例,用对象x把方法的地址传送给构造函数。
注意:
不能调用x.ToString()方法,把它传送给委托变量。调用x.ToString()方法会返回一个不能赋予委托变量的字符串对象。只能把方法的地址赋予委托变量。
委托推断可以在需要委托实例的任何地方使用。委托推断也可以用于事件,因为事件基于委托(参见本章后面的内容)。
委托的一个特征是它们的类型是安全的,可以确保被调用的方法签名是正确的。但有趣的是,它们不关心在什么类型的对象上调用该方法,甚至不考虑该方法是静态方法,还是实例方法。
提示:
给定委托的实例可以表示任何类型的任何对象上的实例方法或静态方法-- 只要方法的签名匹配于委托的签名即可。
为了说明这一点,我们扩展上面的代码,让它使用firstStringMethod委托在另一个对象上调用其他两个方法,其中一个是实例方法,另一个是静态方法。为此,再次使用本章前面定义的Currency结构。Currency结构有自己的ToString()重载方法和一个与GetCurrencyUnit()的签名相同的静态方法,这样,就可以用同一个委托变量调用这些方法了: struct Currency
{
public uint Dollars;
public ushort Cents; public Currency(uint dollars, ushort cents)
{
this.Dollars = dollars;
this.Cents = cents;
}
public override string ToString()
{
return string.Format("${0}.{1,-2:00}", Dollars,Cents);
}
public static string GetCurrencyUnit()
{
return "Dollar";
}
public static explicit operator Currency (float value)
{
checked
{
uint dollars =(uint)value;
ushort cents =(ushort)((value-dollars)*100);
return new Currency(dollars,cents);
}
}
public static implicit operator float (Currency value)
{
return value.Dollars + (value.Cents/100.0f);
}
public static implicit operator Currency (uint value)
{
return new Currency(value, 0);
}
public static implicit operator uint (Currency value)
{
return value.Dollars;
}
} 下面就可以使用GetAString 实例,代码如下所示: private delegate string GetAString(); static void Main()
{
int x = 40;
GetAString firstStringMethod = x.ToString;
Console.WriteLine("String is {0}" + firstStringMethod()); Currency balance = new Currency(34, 50); // firstStringMethod references an instance method
firstStringMethod = balance.ToString;
Console.WriteLine("String is {0}" + firstStringMethod());
// firstStringMethod references a static method
firstStringMethod = new GetAString(Currency.GetCurrencyUnit);
Console.WriteLine("String is {0}" + firstStringMethod());
} 这段代码说明了如何通过委托来调用方法,然后重新给委托指定在类的不同实例上执行的不同方法,甚至可以指定静态方法,或者在类的不同类型的实例上执行的方法,只要每个方法的签名匹配委托定义即可。
运行应用程序,会得到委托引用的不同方法的结果: 但是,我们还没有说明把一个委托传递给另一个方法的具体过程,也没有给出任何有用的结果。调用int和Currency对象的ToString()的方法要比使用委托直观得多!在真正领会到委托的用处前,需要用一个相当复杂的示例来说明委托的本质。下面就是两个委托的示例。第一个示例仅使用委托来调用两个不同的操作,说明了如何把委托传递给方法,如何使用委托数组,但这仍没有很好地说明:没有委托,就不能完成很多工作。第二个示例就复杂得多了,它有一个类BubbleSorter,执行一个方法,按照升序排列一个对象数组,这个类没有委托是很难编写出来的。 在这个示例中,定义一个类MathsOperations,它有两个静态方法,对double类型的值执行两个操作,然后使用该委托调用这些方法。这个数学类如下所示: 下面调用这些方法: using System; namespace Wrox.ProCSharp.Delegates
{
delegate double DoubleOp(double x); class Program
{
static void Main()
{
DoubleOp [] operations =
{
MathsOperations.MultiplyByTwo,
MathsOperations.Square
}; for (int i=0 ; i删除方法调用。
注意:
根据后面的内容,多播委托是一个派生于System.MulticastDelegate的类,System. MulticastDelegate又派生于基类System.Delegate。System.MulticastDelegate的其他成员允许把多个方法调用链接在一起,成为一个列表。
为了说明多播委托的用法,下面把SimpleDelegate示例改写为一个新示例MulticastDelegate。现在需要把委托表示为返回void的方法,就应重写MathOperations类中的方法,让它们显示其结果,而不是返回它们: class MathOperations
{
public static void MultiplyByTwo(double value)
{
double result = value*2;
Console.WriteLine(
"Multiplying by 2: {0} gives {1}", value, result);
} public static void Square(double value)
{
double result = value*value;
Console.WriteLine("Squaring: {0} gives {1}", value, result);
}
} 为了适应这个改变,也必须重写ProcessAndDisplayNumber: 下面测试多播委托,其代码如下: 现在,每次调用ProcessAndDisplayNumber时,都会显示一个信息,说明它已经被调用。然后,下面的语句会按顺序调用action委托实例中的每个方法: 运行这段代码,得到如下所示的结果: MulticastDelegate ProcessAndDisplayNumber called with value = 2
Multiplying by 2: 2 gives 4
Squaring: 2 gives 4 ProcessAndDisplayNumber called with value = 7.94
Multiplying by 2: 7.94 gives 15.88
Squaring: 7.94 gives 63.0436 ProcessAndDisplayNumber called with value = 1.414
Multiplying by 2: 1.414 gives 2.828
Squaring: 1.414 gives 1.999396 如果使用多播委托,就应注意对同一个委托调用方法链的顺序并未正式定义,因此应避免编写依赖于以特定顺序调用方法的代码。
通过一个委托调用多个方法还有一个大问题。多播委托包含一个逐个调用的委托集合。如果通过委托调用的一个方法抛出了异常,整个迭代就会停止。下面是MulticastIteration示例。其中定义了一个简单的委托DemoDelegate,它没有参数,返回void。这个委托调用方法One()和Two(),这两个方法满足委托的参数和返回类型要求。注意方法One()抛出了一个异常: 在Main()方法中,创建了委托d1,它引用方法One(),接着把Two()方法的地址添加到同一个委托中。调用d1委托,就可以调用这两个方法。异常在try/catch块中捕获: 委托只调用了第一个方法。第一个方法抛出了异常,所以委托的迭代会停止,不再调用Two()方法。当调用方法的顺序没有指定时,结果会有所不同。 注意:
错误和异常详见第14章。
在这种情况下,为了避免这个问题,应手动迭代方法列表。Delegate类定义了方法GetInvocationList(),它返回一个Delegate对象数组。现在可以使用这个委托调用与委托直接相关的方法,捕获异常,并继续下一次迭代。 修改了代码后运行应用程序,会看到在捕获了异常后,将继续迭代下一个方法。 到目前为止,要想使委托工作,方法必须已经存在(即委托是用方法的签名定义的)。但使用委托还有另外一种方式:即通过匿名方法。匿名方法是用作委托参数的一个代码块。
用匿名方法定义委托的语法与前面的定义并没有区别。但在实例化委托时,就有区别了。下面是一个非常简单的控制台应用程序,说明了如何使用匿名方法: using System; namespace Wrox.ProCSharp.Delegates
{
class Program
{
delegate string DelegateTest(string val); static void Main()
{
string mid = ", middle part,"; delegateTest anonDel = delegate(string param)
{
param += mid;
param += " and this was added to the string.";
return param;
}; Console.WriteLine(anonDel("Start of string")); }
}
} 委托DelegateTest在类Program中定义,它带一个字符串参数。有区别的是Main方法。在定义anonDel时,不是传送已知的方法名,而是使用一个简单的代码块:它前面是关键字delegate,后面是一个参数: 可以看出,该代码块使用方法级的字符串变量mid,该变量是在匿名方法的外部定义的,并添加到要传送的参数中。接着代码返回该字符串值。在调用委托时,把一个字符串传送为参数,将返回的字符串输出到控制台上。
匿名方法的优点是减少了要编写的代码。不必定义仅由委托使用的方法。在为事件定义委托时,这是非常显然的。(本章后面探讨事件。)这有助于降低代码的复杂性,尤其是定义了好几个事件时,代码会显得比较简单。使用匿名方法时,代码执行得不太快。编译器仍定义了一个方法,该方法只有一个自动指定的名称,我们不需要知道这个名称。
在使用匿名方法时,必须遵循两个规则。在匿名方法中不能使用跳转语句跳到该匿名方法的外部,反之亦然:匿名方法外部的跳转语句不能跳到该匿名方法的内部。
在匿名方法内部不能访问不安全的代码。另外,也不能访问在匿名方法外部使用的ref和out参数。但可以使用在匿名方法外部定义的其他变量。
如果需要用匿名方法多次编写同一个功能,就不要使用匿名方法。而编写一个指定的方法比较好,因为该方法只需编写一次,以后可通过名称引用它。 C# 3.0为匿名方法提供了一个新的语法:l表达式。l表达式可以用于委托类型。前面使用匿名方法的例子可以改为使用表达式: using System; namespace Wrox.ProCSharp.Delegates
{
class Program
{
delegate string DelegateTest(string val); static void Main()
{
string mid = ", middle part,"; DelegateTest anonDel = param = >
{
param += mid;
param += " and this was added to the string.";
return param;
}; Console.WriteLine(anonDel("Start of string"));
}
}
} 运算符=>的左边列出了匿名方法需要的参数。这有几种编写方式。例如,如果需要在示例代码中把一个字符串参数定义为委托类型,一种方式是在括号中定义类型和变量名: 在表达式中,不需要给声明添加变量类型,因为编译器知道该类型: 如果只有一个参数,就可以删除括号: 表达式的右边列出了实现代码。在示例程序中,实现代码放在花括号中,类似于前面的匿名方法: 如果实现代码只有一行,也可以删除花括号和return语句,因为编译器会自动添加该语句。
例如,在下面的委托中,需要一个int参数,返回一个bool值: 可以声明一个委托变量,并指定一个表达式。在表达式中,左边定义了变量x。这个变量的类型自动设置为int,因为这是通过委托定义的。实现代码返回比较x>5的布尔结果。如果x大于5,就返回true,否则返回false。 可以把这个表达式传送给需要谓词参数的方法: 这里列出了相同的表达式,但把变量x的类型定义为int,没有使用变量类型推断功能,还在实现代码中添加了return语句: 如果使用以前的语法,可以通过匿名方法完成相同的功能: 通过所有这些改变,C#编译器就创建出了相同的IL代码。
修改前面的SimpleDelegate示例,使用表达式,可以删除类MathOperations。Main()方法现在如下所示: static void Main()
{
DoubleOp multByTwo = val = > val * 2;
DoubleOp square = val = > val * val; DoubleOp [] operations = {multByTwo, square}; for (int i=0 ; i 删除了类。
提示:
表达式可以用于委托是类型的任意地方。类型是Expression或Expression时,也可以使用表达式。此时编译器会创建一个表达式树,详见第11章。 委托调用的方法不需要与委托声明定义的类型相同。因此可能出现协变和抗变。
1. 返回类型协变
方法的返回类型可以派生于委托定义的类型。在下面的示例中,委托MyDelegate定义为返回DelegateReturn类型。赋予委托实例d1的方法返回DelegateReturn2类型,DelegateReturn2派生自DelegateReturn,因此满足了委托的需求。这称为返回类型协变。 2. 参数类型抗变
术语"参数类型抗变"表示,委托定义的参数可能不同于委托调用的方法。这里是返回类型不同,因为方法使用的参数类型可能派生自委托定义的类型。在下面的示例代码中,委托使用的参数类型是DelegateParam2,而赋予委托实例d2的方法使用的参数类型是DelegateParam,
DelegateParam是DelegateParam2的基类。 基于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#事件,或者在其含义很容易从上下文中看出时,就表示为事件。 事件接收器是指在发生某些事情时被通知的任何应用程序、对象或组件。当然,有事件接收器,就有事件发送器。发送器的作用是引发事件。发送器可以是应用程序中的另一个对象或程序集,在系统事件中,例如鼠标单击或键盘按键,发送器就是.NET运行库。注意,事件的发送器并不知道接收器是谁。这就使事件非常有用。
现在,在事件接收器的某个地方有一个方法,它负责处理事件。在每次发生已注册的事件时,就执行这个事件处理程序。此时就要使用委托了。由于发送器对接收器一无所知,所以无法设置两者之间的引用类型,而是使用委托作为中介。发送器定义接收器要使用的委托,接收器将事件处理程序注册到事件中。连接事件处理程序的过程称为封装事件。封装Click事件的简单例子有助于说明这个过程。
首先创建一个简单的Windows窗体应用程序,把一个按钮控件从工具箱拖放到窗体上。在属性窗口中把按钮重命名为buttonOne。在代码编辑器中把下面的代码添加到Form1构造函数中: 在Visual Studio中,注意在输入+=运算符之后,就只需按下Tab键两次,编辑器就会完成剩余的输入工作。在大多数情况下这很不错。但在这个例子中,不使用默认的处理程序名,所以应自己输入文本。
这将告诉运行库,在引发buttonOne的Click事件时,应执行Button_Click方法。EventHandler是事件用于把处理程序(Button_Click)赋予事件(Click)的委托。注意使用+=运算符把这个新方法添加到委托列表中。这类似于本章前面介绍的多播示例。也就是说,可以为事件添加多个事件处理程序。由于这是一个多播委托,所以要遵循添加多个方法的所有规则,但是不能保证调用方法的顺序。下面在窗体上再添加一个按钮,把它重命名为buttonTwo。把buttonTwo的Click事件也连接到同一个Button_Click方法上,如下所示: 利用委托推断,可以编写下面的代码。编译器会生成与前面相同的代码。 EventHandler委托已在.NET Framework中定义了。它位于System命名空间,所有在.NET Framework中定义的事件都使用它。如前所述,委托要求添加到委托列表中的所有方法都必须有相同的签名。显然事件委托也有这个要求。下面是Button_Click方法的定义: 这个方法有几个重要的地方。首先,它总是返回void。事件处理程序不能有返回值。其次是参数。只要使用EventHandler委托,参数就应是object和EventArgs。第一个参数是引发事件的对象,在这个例子中是buttonOne或buttonTwo,这取决于被单击的按钮。把一个引用发送给引发事件的对象,就可以把同一个事件处理程序赋予多个对象。例如,可以为几个按钮定义一个按钮单击处理程序,接着根据sender参数确定单击了哪个按钮。
第二个参数EventArgs是包含有关事件的其他有用信息的对象。这个参数可以是任意类型,只要它派生自EventArgs即可。MouseDown事件使用MouseDownEventArgs,它包含所使用按钮的属性、指针的X和Y坐标,以及与事件相关的其他信息。注意,其命名模式是在类型的后面加上EventArgs。本章的后面将介绍如何创建和使用基于EventArgs的定制对象。
方法的命名也应注意。按照约定,事件处理程序应遵循"object_event"的命名约定。object就是引发事件的对象,而event就是被引发的事件。从可读性来看,应遵循这个命名约定。
本例最后在处理程序中添加了一些代码,以完成一些工作。记住有两个按钮使用同一个处理程序。所以首先必须确定是哪个按钮引发了事件,接着调用应执行的操作。在本例中,只是在窗体的一个标签控件上输出一些文本。把一个标签控件从工具箱拖放到窗体上,并将其命名为labelInfo,然后在Button_Click方法中编写如下代码: 注意,由于sender参数作为对象发送,所以必须把它的数据类型转换为引发事件的对象类型,在本例中就是Button。本例使用Name属性确定是哪个按钮引发了对象,也可以使用其他属性。例如Tag属性就可以处理这种情形,因为它可以包含任何内容。为了了解事件委托的多播功能,给buttonTwo的Click事件添加另一个方法。窗体的构造函数如下所示: 如果让Visual Studio创建存根(stub),就会在源文件的末尾得到如下方法。但是,必须添加对MessageBox.Show()函数的调用: 如果使用表达式,就不需要Button_Click方法和Button2_Click方法了。事件的代码如下: 在运行这个例子时,单击buttonOne会改变标签上的文本。单击buttonTwo不仅会改变文本,还会显示消息框。注意,不能保证标签文本在消息框显示之前改变,所以不要在处理程序中编写具有依赖性的代码。
我们已经学习了许多概念,但要在接收器中编写的代码量是很小的。记住,编写事件接收器常常比编写事件发送器要频繁得多。至少在Windows用户界面上,Microsoft已经编写了所有需要的事件发送器(它们都在.NET基类中,在Windows.Forms命名空间中)。 接收事件并响应它们仅是事件的一个方面。为了使事件真正发挥作用,还需要在代码中生成和引发事件。下面的例子将介绍如何创建、引发、接收和取消事件。
这个例子包含一个窗体,它会引发另一个类正在监听的事件。在引发事件后,接收对象就确定是否执行一个过程,如果该过程未能继续,就取消事件。本例的目标是确定当前时间的秒数是大于30还是小于30。如果秒数小于30,就用一个表示当前时间的字符串设置一个属性;如果秒数大于30,就取消事件,把时间字符串设置为空。
用于生成事件的窗体包含一个按钮和一个标签。下载的示例代码把按钮命名为buttonRaise,标签命名为labelInfo。在创建窗体,添加两个控件后,就可以创建事件和相应的委托了。在窗体类的类声明部分,添加如下代码: 这两行代码的作用是什么?首先,我们声明了一种新的委托类型ActionEventHandler。必须创建一种新委托,而不使用.NET Framework预定义的委托,其原因是后面要使用定制的EventArgs类。方法签名必须与委托匹配。有了一个要使用的委托后,下一行代码就定义事件。在本例中定义了Action事件,定义事件的语法要求指定与事件相关的委托。还可以使用在.NET Framework中定义的委托。从EventArgs类中派生出了近100个类,应该可以找到一个自己能使用的类。但本例使用的是定制的EventArgs类,所以必须创建一个与之匹配的新委托类型。
在一行代码中定义事件是C#中的一个缩写方式,它可以定义添加和删除处理程序的方法,声明委托的一个变量。除了编写一行代码之外,还可以用下面的代码达到相同的效果。声明一个事件类型的变量以及添加和删除事件处理程序的方法。在定义添加和删除事件处理程序的方法时,其语法非常类似于属性。变量值的定义也类似于添加和删除事件处理程序。 提示:
如果不仅仅需要添加和删除事件处理程序,就可以使用定义事件的较长记号。例如,要为多个线程访问添加同步功能。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类,并把它作为一个参数传递给事件。但是,这有一个小问题。如果事件不会在任何地方使用,该怎么办?如果还没有为事件定义处理程序,该怎么办?Action事件实际上是空的。如果试图引发该事件,就会得到一个空引用异常。如果要派生一个新的窗体类,并使用该窗体,把Action事件定义为基事件,则只要引发了Action事件,就必须执行其他一些操作。目前,我们必须在派生的窗体中激活另一个事件处理程序,这样才能访问它。为了使这个过程容易一些,并捕获空引用错误,就必须创建一个方法OnEventName,其中EventName是事件名。在这个例子中,有一个OnAction方法,下面是OnAction方法的完整代码: 代码并不多,但完成了需要的工作。把该方法声明为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 Windows Forms应用程序。事件是.NET开发人员监视应用程序执行时出现的各种Windows消息的方式,否则就必须监视WndProc,捕获WM_MOUSEDOWN消息,而不是获取按钮的鼠标Click事件。
在设计大型应用程序时,使用委托和事件可以减少依赖性和层的关联,并能开发出具有更高复用性的组件。
发表评论
-
白纸~~~神舞
2014-02-21 23:55 27716世纪金刚亥母造像, 体态妖娆灵动, 纹饰处 ... -
[互联网江湖]我当程序员的那些狗日日子
2014-02-21 23:54 341[互联网江湖]我当程序员的那些狗日日子 更多 ... -
解决Visual C++ 2008 Feature Pack Release安装失败的方法 ―― 在VS2008简体中文版上安装TR1
2012-01-20 01:59 1465解决Visual C++ 2008 Feature Pack ... -
[华为U8120沃达丰V845]官方刷机包的刷机方法
2012-01-20 01:59 2595[华为U8120沃达丰V845]官方刷机包的刷机方法 201 ... -
NET安全系列之三:用户与角色的概念/基于角色的安全
2012-01-20 01:59 687NET安全系列之三:用户与角色的概念/基于角色的安全 201 ... -
silverlight 自定义转换器
2012-01-20 01:59 750silverlight 自定义转换器 2011年01月10日 ... -
Python 3.2.2 RC1发布
2012-01-19 09:47 549Python 3.2.2 RC1发布 2011年08月16日 ... -
在Windows上安装Python+MySQL 的常见问题及解决方法
2012-01-19 09:47 1172在Windows上安装Python+MySQL 的常见问题及解 ... -
用Python捕获snmp trap
2012-01-19 09:46 2293用Python捕获snmp trap 2011年10月11日 ... -
一些python的三方库
2012-01-19 09:46 1222一些python的三方库 2010年06月14日 ... -
Qt(4.7)入门_Windows环境安装
2012-01-17 02:20 887Qt(4.7)入门_Windows环境安装 2012年01月 ... -
sysytem32下的文件及其作用的文章
2012-01-17 02:20 492sysytem32下的文件及其作用的文章 2011年11月2 ... -
关于eclipse配置android开发配置流程
2012-01-17 02:20 799关于eclipse配置android开发配置流程 2012年 ... -
关于Android4.0 emulator-arm.exe-应用程序错误,内存不能为"read"问题解决方法
2012-01-17 02:20 874关于Android4.0 emulator-arm.exe-应 ...
相关推荐
第7章 委托和事件 第8章 字符串和正则表达式 第9章 泛型 第10章 线程化 第11章 文件和流 第12章 异常处理 第13章 数组和集合 第14章 语言集成查询(LINQ) 第15章 程序集和版本化 第Ⅱ部分 使用C#开发应用程序 第16章 ...
第7章 委托和事件 第8章 字符串和正则表达式 第9章 泛型 第10章 线程化 第11章 文件和流 第12章 异常处理 第13章 数组和集合 第14章 语言集成查询(LINQ) 第15章 程序集和版本化 第Ⅱ部分 使用C#开发应用程序 第16章 ...
7第七章接受委托、审计计划与风险评估程序.pptx
第七章接受委托、审计计划与风险评估程序.pptx
针对C#高级编程考试重点复习用的文档第1章 .NET体系结构内容小结第2章 托管执行环境的介绍第3章 使用组件第4章 部署与版本控制第5章 对象和类型第7章 数组和集合第8章 委托和事件第9章 内存管理第10章 文件与I/O流...
第7章 字符串、数组和集合 第8章 委托和事件 第9章 内存和资源管理 第10章 数据流和文件 第11章 Internet访问 第12章 序列化 第13章 远程处理和XML Web Service 第14章 线程和异步编程 第15章 托管代码与非托管...
第7章 字符串、数组和集合 第8章 委托和事件 第9章 内存和资源管理 第10章 数据流和文件 第11章 Internet访问 第12章 序列化 第13章 远程处理和XML Web Service 第14章 线程和异步编程 第15章 托管代码与非托管...
第7章 字符串、数组和集合 第8章 委托和事件 第9章 内存和资源管理 第10章 数据流和文件 第11章 Internet访问 第12章 序列化 第13章 远程处理和XML Web Service 第14章 线程和异步编程 第15章 托管代码与非托管...
中文版,[Csharp高级编程(第6版)]7章委托和事件、8章字符串和正则表达式、9章集合.pdf