1. Bin文件夹
Bin文件夹包含应用程序所需的,用于控件、组件或者需要引用的任何其他代码的可部署程序集。该目录中存在的任何.dll文 件将自动地链接到应用程序。如果在该文件夹中留有不用的或过期的文件,则可能出现“二义性引用(ambiguous reference)”异常的风险。换句话说,如果两个不同的程序集定义相同的类(相同的命名空间和名称),则ASP.NET运行库不能决定应该使用哪一 个程序集,从而抛出一个异常。在开发时,当我们重新命名一个项目或一个程序集的名称时,这是常见的错误。为了避免这种错误,一定不要在该文件夹中保留任何 不必要的程序集,或者至少要从配置文件的<assemblies>节中删除如下代码行:
<add assembly="*" />
在表1.10所列的所有文件夹中,只有Bin可以被ASP.NET 1.x应用程序识别。该文件夹是强制性的。
2. App_Browser文件夹
该可选的文件夹包含.browser文件。.browser文件描述浏览器(不管是移动设备浏览器,还是台式机浏览器)的特 征和功能。ASP.NET在安装路径下的Config\Browser文件夹中安装了大量.browser文件,这些文件供所有应用程序共享。我们只是把 当前应用程序特有的浏览器文件放在App_Browser文件夹下。.browser文件的内容即时动态地进行编译,以便向ASP.NET运行库提供最新 的浏览器信息。
让我们简单谈谈拥有一个自定义的.browser文件可能有帮助的场景。设想应用程序使用了一个在某个浏览器下不能有效呈现的控件。在指定的浏览器中显示宿主页面时,可以编写一个.browser文件,迫使ASP.NET使用一个不同的适配器来生成该控件。
<browsers>
<browser id="browserID">
<controlAdapters>
<adapter controlType="Samples.CustomControl"
adapterType="Samples.Adapters.CustomControlAdapter" />
</controlAdapters>
</browser>
</browsers>
假设browserID与ASP.NET识别的标准浏览器之一相匹配,则上文所示的.browser文件指示在指定的浏览器下使用CustomControlAdapter呈现CustomControl。
3. App_Code文件夹
App_Code文件夹正好在Web应用程序根目录下,其存储所有应当作为应用程序的一部分动态编译的类文件。这些类文件自 动链接到应用程序,而不需要在页面中添加任何显式指令或声明来创建依赖性。App_Code文件夹中放置的类文件可以包含任何可识别的ASP.NET组件 ——自定义控件、辅助类、build提供程序、业务类、自定义提供程序、HTTP处理程序等。
注意 在开发时,对App_Code文件夹的更改会导致整个应用程序重新编译。对于大型项目,这可能不受欢迎,而且很耗时。为此,鼓励大家将代码进行模块化处理 到不同的类库中,按逻辑上相关的类集合进行组织。应用程序专用的辅助类大多应当放置在App_Code文件夹中。
App_Code文件夹中存放的所有类文件应当使用相同的语言。如果类文件使用两种或多种语言编写,则必须创建特定语言的子目录,以包含用每种语言编写的类。一旦根据语言组织这些类文件,就要在web.config文件中为每个子目录添加一个设置:
<compilation>
<codeSubDirectories>
<add directoryName="VBFolder" />
</codeSubDirectories>
</compilation>
重要的是,特定语言的子目录应在web.config文件中注册,否则,不管它们属于哪个文件夹,App_Code文件夹下 的所有文件将被编译成一个单独的程序集。上述配置脚本描述了这么一种情况,即所有的C#文件都放在App_Code文件夹的根目录下,而把几个 Visual Basic .NET类文件移入VBFolder目录中。如果<codeSubDirectories>节中提到的目录不存在,则会收到一个编译错误提 示。
App_Code根文件夹中的文件被编译成App_Code_xxx.dll程序集,其中xxx是随机生成的字符序列。一个 给定子目录中的文件将被编译成一个名为App_SubCode_xxx_yyy.dll的动态创建的程序集,其中xxx指示子目录的名称,而yyy是一个 随机字符序列。只有在应用程序根目录中的web.config文件中进行了设置,<codeSubDirectories>节才有效。
在App_Code目录或任何其他子目录中放置一个assemblyinfo.cs文件,可以创建一个强命名的程序集。显然,如果该文件夹包含Visual Basic .NET文件,那么将使用assemblyinfo.vb文件。程序集配置文件可以引用一个.snk文件来保存强名称的密钥。
注意 给一个程序集设置一个强名称,首先必须获得一个公开/私有密钥对。通过使用强名称(Strong Name)工具(sn.exe),可以获得这样一个密钥对。强名称工具是我们可以在.NET Framework的安装路径中发现的SDK binary之一。密钥对文件通常有一个.snk扩展名。可以将该文件保存到一个应用程序文件夹中,并在assemblyinfo.cs文件中引用它,如下所示:
[assembly: AssemblyKeyFileAttribute(@"yourKeyPair.snk")]
注意,Visual Basic .NET是在包含Visual Studio Solution的目录中寻找密钥文件,而C#编译器则在包含该binary的目录中寻找密钥文件。据此可知,用此属性调整我们使用的路径,或者把密钥文件放在合适的文件夹中。
在随后发生的任何重新生成中,程序集的名称将发生变化。同时,老的AppDomain请求一结束,就删除老的程序集。
App_Code文件夹并非只能包含类文件。特别是,它可以包含并能自动地处理代表数据架构的XSD文件。把一个XSD文件 添加到该文件夹中时,编译器将把它解析成一个有类型的DataSet类,并将它添加到应用程序作用域中。在ASP.NET 1.x中,这一工作由Visual Studio .NET向导,使用一个命令行实用程序(xsd.exe)完成的。
注意 使用web.config文件注册一个组件(例如,一个自定义的服务器控件或一个自定义的HTTP处理程序)时,通常要求指定包含该代码的程序集名称。如 果该组件定义在App_Code文件夹中,则应该用什么名称来指示程序集?在这种情况下,只是忽略程序集信息,并规定完整的类名即可。如果没有规定任何程 序集,则ASP.NET运行库将试图从任何已装载的程序集中装入该类,包括为App_Code文件夹动态创建的程序集。
4. App_Data文件夹
App_Data文件夹应该包含应用程序的本地数据存储。它通常以文件(诸如Microsoft Access或Microsoft SQL Server Express数据库、XML文件、文本文件以及应用程序支持的任何其他文件)形式包含数据存储。该文件夹内容不由ASP.NET处理。该文件夹是ASP.NET提供程序存储自身数据的默认位置。
注意 默认ASP.NET帐户被授予对文件夹的完全访问权限。如果碰巧要改变ASP.NET帐户,一定要确保新帐户被授予对该文件夹的读/写访问权。
5. App_GlobalResources文件夹
正如其他应用程序一样,ASP.NET应用程序也可以使用资源,而且通常应该使用资源。资源是隔离应用程序用户界面的可局部 化部分的一种有效方法。一般而言,资源是与程序相关的不可执行的文本。典型的资源有图像、图标、文本和附属文件,但是任何可序列化的对象也可以被看作资 源。应用程序资源存储在应用程序的外部,这样就能在不影响和重新编译应用程序本身的情况下重新编译和替换它们。
ASP.NET应用程序需要有一个主要程序集来保存应用程序默认的或中性的资源。此外,还要部署许多附属程序集,它们中各自 包含我们需要支持的某种文化的本地化资源。在ASP.NET 1.x中,编译一个程序集内的资源有点麻烦。需要手动地将基于XML的资源文件(那些带.res扩展名的资源)编译成.resources二进制文件。这 些文件既可以嵌入到一个.NET可执行文件中,也可以编译成附属程序集。使用资源文件生成器实用程序resgen.exe,将文本和基于XML的资源文件 转变为.resource文件。资源文件名称遵循baseName.cultureName.resource命名约定,其中baseName通常是应用 程序的名称:
resgen.exe ProAspNet20.resx ProAspNet20.it.resources
创建.resource文件以后,应当把它嵌入到一个程序集中,甚至可以作为一个资源容器来使用。要把一个资源文件嵌入到一个附属程序集中,可以使用程序集连接器工具(al.exe)。在命令行上,指出程序集所使用的文化(如下面示例中的it,它代表意大利)和名称。
al /out:ProAspNet20.resources.dll /c:it /embed:ProAspNet20.it.resources
在编译附属程序集之后,它们将有相同的名称。将它们部署到不同的子目录中,分别按文化命名。
幸运的是,对于ASP.NET 2.0,附属程序集的时代已经一去不复返了。更准确地说,附属程序集仍然存在,但是由于App_GlobalResources保留文件夹,对开发人员来说它们已经成为过去的事情。
该文件夹中的任何定位的.resx文件自动地被编译成附属程序 集。.resx文件的名称包含文化信息,以帮助ASP.NET运行库环境的程序集生成。如下文件,resources.resx, resources.it.resx, resources.fr.resx,生成中性程序集以及适合于意大利(Italian)和法国(French)文化的附属程序集。如果没有要求特定文 化,则中性程序集是默认的文化资源。
App_GlobalResources文件夹中的资源文件是应用程序的全局资源,因而可以从任何页面中引用它。和ASP.NET 1.x相比资源读取结果也极大地简化了:
<asp:Label Runat="server" Text="<%$ Resources:ResxFile, MyResName %>" />
可以使用最新的称为Resources的$-表达式以声明的方式绑定全局资源(第5章将详细介绍$-表达式)。该表达式包括两个参数:.resx资源文件的名称(没有扩展名),以及要检索的资源的名称。以编程的方式访问资源,请使用如下代码:
HttpContext.GetGlobalResourceObject(resxFile, MyResName)
这两个参数都是字符串,并且与$-表达式中的参数具有相同的作用。此外,$-表达式Resources的实现在内部使用GetGlobalResourceObject。
6. App_LocalResources文件夹
App_LocalResources文件夹位于包含一些ASP.NET页面的文件夹下的一个子目录。该文件夹可以使用位于 目录结构中高一级目录中的页面命名的.resx文件进行填充。假定父文件夹包含test.aspx,则可以在App_LocalResources文件夹 中找到一些可用的资源文件如下:test.aspx.resx、test.aspx.it.resx和test.aspx.fr.resx。显然,上述文 件中存储的资源仅对test.aspx页面有影响,因而只能在链接的页面中看见它们(可以使用它们)。
如何访问一个页面特有的资源呢?对于编程访问,可使用如下代码:
HttpContext.GetLocalResourceObject("/ProAspNet20/ResPage.aspx",
"PageResource1.Title")
第1个参数指出页面虚拟路径;第2个参数是资源名称。对于声明式访问,使用meta:ResourceKey属性。例如,
<asp:Button Runat="server" meta:resourcekey="ButtonResource1" />
该声明将一个惟一的资源键与特定按钮实例关联。局部文件.resx包含prefix.name形式的条目,其中prefix 是资源键,而name是绑定控件上的属性名。为了赋予按钮一个本地化标题(Text属性),只要在资源文件中创建一个 ButtonResource1.Text条目即可。
局部和全局资源文件夹中存在的资源文件都被编译,以创建附属程序集的类。最后的结果是开发人员创建.resx文件,并测试该页面。而ASP.NET编译机制会完成其余工作。
7. App_Themes文件夹
App_Themes文件夹为ASP.NET控件定义主题。主题包含在App_Themes文件夹下的一个文件夹。根据定义,一个主题是一组带有样式信息的文件。主题文件夹中的文件内容被编译,以生成一个类,而该类被页面调用以编程的方式设置主题化控件的样式。
App_Themes文件夹列出应用程序的本地主题。 应用程序还可以继承如下文件夹中定义的全局主题:
%WINDOWS%\Microsoft.NET\Framework\[version]\ASP.NETClientFiles\Themes
从编译的角度看,全局主题和局部主题没有区别。如果一个给定名称的主题,既存在应用程序的本地主题,又存在服务器机器的全局主题,则本地主题优先适用。
8. App_WebReferences文件夹
在Visual Studio .NET 2003中,一个需要访问Web服务的ASP.NET应用程序,将通过“添加Web引用”对话框获得相应的.wsdl文件。Web服务的WSDL(Web Service Description Language)文档,对于从页面使用Web服务是不够的。ASP.NET页面最终是一个托管类,并且需要与另一个托管类通信。因此,Web服务被一个 代理类所包装。该代理类是由Visual Studio使用命令行工具wsdl.exe的服务创建的。该代理类尽量包含与Web服务商的Web方法一样多的方法,并且它结合了Web服务的公共接口 定义的任何自定义的数据类型。
这个操作不需要开发人员付出很大的代价。然而,开发人员显然要依赖于Visual Studio来生成代理类。如果能够直接把.wsdl文件放在应用程序的目录树的某个地方,并让ASP.NET处理其余的任务,这样不是更容易、更简单 吗?这正好是App_WebReferences文件夹要做的事情。
它识别那些用来描述所绑定的Web服务的.wsdl文件,并生成运行时代理类,以便ASP.NET页面能够以类型安全的方式 放置对Web服务的调用。App_WebReferences文件夹可以包含子文件夹。子文件夹的名称驱动最后所得到的代理类的命名空间,而WSDL文件 定义类名。例如,samples.wsdl文件和ProsAspNet20子文件夹将创建一个称为ProAspNet20.Samples的代理类。该动 态创建的程序集称为App_WebReferences.xxx.dll,其中xxx是一个随机的字符序列。
文件夹名称
文件类型
注 释
Bin
.dll
包含应用程序所需的任何预生成的程序集
App_Browsers
.browser
包含应用程序特有的浏览器定义文件,ASP.NET用它来识别各浏览器及确定它们的功能
App_Code
.cs、.vb、.xsd、自定义的文件类型
包含作为应用程序的一部分编译的类的源文件。当页面被请求时,ASP.NET编译该文件夹中的代码。该文件夹中的代码在应用程序中自动地被引用
App_Data
.mdb、.mdf、.xml
包含Microsoft Office Access和SQL Express文件以及XML文件或其他数据存储
App_GlobalResources
.resx
包含在本地化应用程序中以编程方式使用的资源文件
App_LocalResources
.resx
包含页面范围的资源文件
App_Themes
.skin、.CSS、.xsl、附属文件
包含一组定义ASP.NET页面和控件外观的文件
App_WebReferences
.wsdl
包含用以生成代理类的WSDL文件,以及与在应用程序中使用Web服务有关的其他文件
事件驱动模型
事件驱动模型是软件系统平台中的一个重要区域,现代软件系统大量地使用事件驱动的处理方法,尤其在用户界面方面。虽然如此,过去在软件开发语言中一直没有融入事件处理的因子,直到.net的出现,才将事件处理的工作负荷一部分的分派给编译器,从而稍微减轻开发者的负担。
下图显示事件模型的组成份子:
Subscriber需事先和publisher预订要接受其发布的某事件(下图a1),publisher在某事件发生以后,必需先生成该事件的相关数据对象(下图a2.1),然后通过方法调用来通知subscriber(下图a2.2),也就是用回调(callback)的方式来通知subscriber。当然在预订的时候,并不一定要由subscriber自身来预订,也可以由另一个对象来帮忙预订。其动态图形示意如下:
本文并不探讨异步的信息传送,也就是在整个事件的处理过程当中,publisher和subscriber 对象皆需要同时存在。如果对于离线(offline)的方式来处理事件有兴趣的话,请参阅
Java的JMS(Java Message Service)和.NET的LCE(Loosely Coupled Events)。
事件是什么? 那么,到底事件是什么?在软件系统中要如何表达一个事件?一个事件应该包括两个东西:识别事件的名称(event identity),和事件的相关的数据(event data)。例如,一个键盘按键被按下的事件可能叫KeyPressedEvent,事件数据则为该按键的代码。
先前提到发布事件是用调用方法的方式(回调),不过有一个问题,就是publisher无法事先知道subscriber的类型。在Java的编码模式当中,回调可以使用接口模式,也就是publisher必需事先定义好一个在发布事件中使用的接口,subscriber实现该接口中的方法,publisher则通过调用接口中的方法来完成发布事件的工作。如下图:
这样,在Java的编码模式中,一个事件的识别名称就是接口名称和其中的方法名称,而事件数据则自然是接口方法的参数了。Java对于这个接口的命名风格为XXXListener,顾名思义就是某事件的倾听者。例如:
public interface KeyListener extends EventListener {
public void keyTyped(KeyEvent e);
public void keyPressed(KeyEvent e);
public void keyReleased(KeyEvent e);
}
由于一个接口中可以包含多个方法,所以Java在设计事件的时候,是将一组相关联的事件放在一起,这样设计的优点是可以很好的将事件做分类,并且在publisher中如果要处理的事件较多的话,可以使用比较少的成员变量来记录subscribers。缺点是如果subscriber只对事件接口中的部分事件有兴趣,也必需要全盘实现该接口(所以在AWT里有java.awt.event.XXXAdapter抽象辅助类)。另一个缺点则是必需要为每一类事件定义一个接口类型,即使可能大部分的事件只有极少的方法。
微软在为
C#语言命名的时候,就刻意隐喻C#是从C/
C++为基础发展而得的面向对象程序语言,始祖绝不是Java,所以肯定要保留一些C/C++的语言机制。在C/C++里面对回调的设计方式就是用函数指针,想当然C#也希望直接使用类似函数调用的方式来做为事件发布的方法。如下图:
所以C#期望使用函数指针类型来作为事件的识别名称,然后用函数的参数来传递事件数据。我们先用一段C++代码来描绘这幅图画:
Event type definition:
// 定义KeyPressedCallback 为一个函数指针的类型,
// 该函数接受一个整数型参数,无返回值
typedef void (*KeyPressedCallback)(int keyCode);
Publisher:
class Publisher
{
public KeyPressedCallback KeyPressedSink = null;
...
void FireEvent(int KeyCode)
{
if (KeyPressedSink != null)
(*KeyPressedSink)(keyCode);//callback
}
}
Subscriber:
void KeyPressedHandler(int keyCode)
{
...
}
...
Publisher publisher = new Publisher();
//reGISter
publisher.KeyPressedSink = &KeyPressedHandler;
一个当代的纯面向对象程序语言,是肯定希望要把造成程序复杂和不易维护的指针给去除的。所以在C#语言机制当中,势必要创造新的元素来取代,于是delegate(委托)出现了。如下:
Event type definition:
// 定义KeyPressedDelegate 为一个类似函数指针的类型,
// 该函数接受一个整数型参数,无返回值
delegate void KeyPressedDelegate(int keyCode);
Publisher:
class Publisher
{
public KeyPressedDelegate KeyPressed = null;
...
void FireEvent(int KeyCode)
{
if (KeyPressed != null)
KeyPressed(keyCode);
}
}
Subscriber:
void KeyPressedHandler(int keyCode)
{
...
}
...
Publisher publisher = new Publisher();
//register
publisher.KeyPressed = KeyPressedHandler;
一开始,你可以把KeyPressedDelegate当成是与函数指针相类似的东西,通过它你可以引用一个实例方法或静态方法,就好像引用一个对象一样。然后可以通过这个delegate直接调用其引用的方法。但是下面你会看到delegate更扩大了其引用能力。
事件的预订和发布
Publisher必需能够接受多个subscribers的预订,所以在publisher当中必需维护预订者的列表以供将来发布事件使用。在
Java语言的模式中,并没有提供特别的东西来帮助这件事,可以自己用collection 类来做,例如可以使用ArrayList 对象来做记录。Java的预订方法名的风格为addXXXListener(),因为在publisher端必需把subscriber 对象“添加”到预订者列表后面,如下图:
对于subscriber来说,预订动作的内部处理是黑箱的,subscriber不用关心publisher是如何做预订记录的。参考以下代码片段:
class Publisher {
private ArrayList listenerList = new ArrayList();
public void addKeyListener(KeyListener l) {
listenerList.add(l);
}
public void fireKeyPressedEvent(int keyCode) {
Iterator iter = listenerList.iterator();
while (iter.hasNext()) {
KeyListener l = (KeyListener)iter.next();
l.keyPressed(keyCode);
}
}
}
当然这段代码只是简单的示意,如果要考虑多线程的安全问题,可能要在addKeyListener()前面加上synchronized;还有,有预订就必然相应的有“退订”(removeXXXListener()),在这里就不再把它写出来。
如果每个publisher都要这样重复撰写这样的代码的确很麻烦。所以在
.net中势必希望能够提供一种用来帮助publisher记录预订者,和发布事件的工具。按一般设计者的初步想法,一定是先提供一个辅助类来协助:
从语法上考虑简化,add/remove动作应该可以用
C++的operator=()、operator+=()和operator-=()来完成。像这样:
Publisher publisher = new Publisher();
...
publisher.KeyEventHandlerDelegate += KeyPressedHandler;
//等同于
//publisher.EventHandlerDelegate.add(KeyPressedHandler);
如果可以这样撰写的话,确实很简单。不过在强制性类型(strongly-typed)语言系统中,必需精确的定义add()方法参数中的delegate 类型,这样似乎无法写出一个可以公用的基础类。所以在.NET中,是结合delegate关键字,通过简单的语法,借助编译器来帮我们自动生成相关的代码。于是把delegate的能力再予以加强了:
Event type definition:
public delegate void KeyPressedDelegate(int keyCode);
Publisher:
class Publisher
{
public KeyPressedDelegate KeyPressed;
...
void FireKeyPressedEvent(int KeyCode)
{
if (KeyPressed != null)
//依次调用记录在KeyPressed中的所有方法
KeyPressed(keyCode);
}
}
Subscriber:
void OnKeyPressed(int keyCode)
{
...
}
void OnKeyPressed2(int keyCode)
{
...
}
...
Publisher publisher = new Publisher();
publisher.KeyPressed = OnKeyPressed; //预订
publisher.KeyPressed += OnKeyPressed2; //预订另一个
这样一个delegate不仅可以帮忙记录一个以上的subscribers,也可以简单的通过一行的调用语句来发布事件。其实编译器会为每一个delegate 生成一个相应的类来帮助处理这些工作,不过是作为一个只是编写应用系统的程序员是不必要去了解这些细节的,有兴趣的人可以去研究System.Delegate和System.MulticastDelegate 类。
上面看到的结果应该已经是比较满意的,但是仍有改善空间。首先,因为一个delegate成员是public的,任何人都可以任意的直接接触,有失面向对象世界中的信息封装和隐藏(information encapsulation and hiding)的原则。所以在
C#中又增加一个关键字“event”,用在放在声明一个delegate成员变量的前面,这样表示只有在声明这个delegate的类内部才可以直接对它进行subscriber 调用。
public delegate void KeyPressedDelegate(int keyCode);
class Publisher
{
public event KeyPressedDelegate KeyPressed;
...
void FireKeyPressedEvent(int KeyCode)
{
if (KeyPressed != null)
//只有在Publisher才可以
KeyPressed(keyCode);
}
}
// outside of Publisher...
Publisher publisher = new Publisher();
// !!! 不允许 !!! 会编译错误 !!!
publisher.KeyPressed(100);
接着,event delegate是以一个成员变量的方式存在,如果能以属性的方式让外界进行存取,不是更好吗。于是又增加了event
Accessors。在C#语言中,是使用add和remove来封装实际的 += 和 -= 操作。如下:
class Publisher
{
protected event KeyPressedDelegate m_KeyPressed;
// event accessor。定义一个事件属性。
public event KeyPressedDelegate KeyPressed
{
add
{
m_KeyPressed += value;
}
remove
{
m_KeyPressed -= value;
}
}
void FireKeyPressedEvent(int KeyCode)
{
if (KeyPressed != null)
m_KeyPressed(keyCode);
}
}
不管是事件变量或者是事件属性,对声明事件变量和属性的类的外部,只能对它做 += 和 -= 操作。这样可以加强它的安全性。当然event accessor只有add和remove操作,所以不管是任何人(包括声明该事件属性的类内部),也只能对事件属性做 += 和 -= 操作。
经过这样的改善,可以理论上更减弱publisher和subscriber之间的耦合力了。
事件数据
接下来我们谈一谈另一个在事件模型中的重要角色,就是在事件发布中被传递的“事件数据”。
一个subscriber在接受同一种事件的时候,可能来自不同的publisher,所以自然地希望知道发出事件的人是谁,也就是在传递的参数当中,必需包含一个publisher 对象的引用。在
Java中,推荐所有的事件数据类都继承java.util.EventObject 类。因为在生成一个EventObject 对象的时候,必需给一个event source 对象作为参数。然后可以通过EventObject的getSource()方法来取得这个对象。在EventObject里面,并没有包含其他任何事件数据,所以如果在事件的传递过程当中,有任何事件数据需要传递,就必需从EventObject 派生出一个新的子类出来。如下图:
在
.net当中也有一个相似的类叫System.EventArgs,但是这个类的内容是空的,如下:
public class EventArgs
{
public static readonly EventArgs Empty;
static EventArgs()
{
Empty = new EventArgs();
}
public EventArgs()
{}
}
.NET认为不一定所有的subscriber都对event source感兴趣,所以如果需要的话,就把event source当成是delegate方法的参数来传递好了。.NET定义了一个标准的delegate EventHandler,以下是它的签名(signature):
public delegate void EventHandler(object sender, EventArgs e);
以后,只要你需要的delegate的签名与EventHandler相同的话,就直接用它了。这里所谓的签名相同,是指参数的类型和返回值的类型皆相同。
Java和.NET都希望用户在定义的事件数据类的时候,尽可能的使用推荐的基类,因为这样在publisher对发出的事件数据内容有所变更或扩大的时候,对subscriber的冲击会比较小,这是由于多型(polymorphism)机制的帮助。
结束语 经过这番解析之后,应该能够比较清楚的了解到Java和.NET事件处理框架的设计思路,希望有助于读者更进一步理解其框架的形成过程。从语言的角度来看,.NET的确有一些针对性的改善和试图简化对事件的处理,Java则仍保有其一贯简约的风格。读者若有任何意见和指教,可以通过e-mail与我交流:bruceyou@sina100.com。