在C#中使用委托的方式触发事件
事件(event)是个非常重要的概念,我们的程式时刻都在触发和接收着各种事件:鼠标点击事件,键盘事件,及处理操作系统的各种事件。所谓事件就是由某个对象发出的消息。比如用户按下了某个按钮,某个文件发生了改动,socket上有数据到达。触发事件的对象称作发送者(sender),捕捉事件并且做出响应的对象称作接收者(receiver),一个事件能存在多个接受者。
在异步机制中,事件是线程之间进行通信的一个非常常用的方式。比如:用户在界面上按下一个按钮,执行某项耗时的任务。程式此时启动一个线程来处理这个任务,用户界面上显示一个进度条指示用户任务执行的状态。这个功能就能使用事件来进行处理。能将处理任务的类作为消息的发送者,任务开始时,发出“taskstart”事件,任务进行中的不同时刻发出“taskdoing”事件,并且携带参数说明任务进行的比例,任务结束的时候发出“taskdone”事件,在画面中接收并且处理这些事件。这样实现了功能,并且界面和后台执行任务的模块耦合程度也是最低的。
具体说c#语言,事件的实现依赖于“委托”(delegate)的概念,先了解一下代理。
委托(delegate)
delegate是c#中的一种类型,他实际上是个能够持有对某个方法的引用的类。和其他的类不同,delegate类能够拥有一个签名(signature),并且他只能持有和他的签名相匹配的方法的引用。他所实现的功能和c/c++中的函数指针十分相似。他允许你传递一个类a的方法m给另一个类b的对象,使得类b的对象能够调用这个方法m。但和函数指针相比,delegate有许多函数指针不具有的好处。首先,函数指针只能指向静态函数,而delegate既能引用静态函数,又能引用非静态成员函数。在引用非静态成员函数时,delegate不仅保存了对此函数入口指针的引用,而且还保存了调用此函数的类实例的引用。其次,和函数指针相比,delegate是面向对象、类型安全、可靠的受控(managed)对象。也就是说,runtime能够确保delegate指向一个有效的方法,你无须担心delegate会指向无效地址或越界地址。
实现一个delegate是非常简单的,通过以下3个步骤即可实现一个delegate:
1. 声明一个delegate对象,他应当和你想要传递的方法具有相同的参数和返回值类型。
2. 创建delegate对象,并将你想要传递的函数作为参数传入。
3. 在要实现异步调用的地方,通过上一步创建的对象来调用方法。
下面是个简单的例子:
public class mydelegatetest
{
// 步骤1,声明delegate对象
public delegate void mydelegate(string name);
// 这是我们欲传递的方法,他和mydelegate具有相同的参数和返回值类型
public static void mydelegatefunc(string name)
{
console.writeline("hello, {0}", name);
}
public static void main ()
{
// 步骤2,创建delegate对象
mydelegate md = new mydelegate(mydelegatetest.mydelegatefunc);
// 步骤3,调用delegate
md("sam1111");
}
}
输出结果是:hello, sam1111
下面我们来看看事件是怎么处理的:
事件(event)
c#中的事件处理实际上是一种具有特别签名的delegate,象下面这个样子:
public delegate void myeventhandler(object sender, myeventargs e);
其中的两个参数,sender代表事件发送者,e是事件参数类。myeventargs类用来包含和事件相关的数据,所有的事件参数类都必须从system.eventargs类派生。当然,如果你的事件不含特别的参数,那么能直接用system.eventargs类作为参数。
结合delegate的实现,我们能将自定义事件的实现归结为以下几步:
1:定义delegate对象类型,他有两个参数,第一个参数是事件发送者对象,第二个参数是事件参数类对象。
2:定义事件参数类,此类应当从system.eventargs类派生。如果事件不带参数,这一步能省略。
3:定义事件处理方法,他应当和delegate对象具有相同的参数和返回值类型。
4:用event关键字定义事件对象,他同时也是个delegate对象。
5:用+=操作符添加事件到事件队列中(-=操作符能够将事件从队列中删除)。
6:在需要触发事件的地方用调用delegate的方式写事件触发方法。一般来说,此方法应为protected访问限制,既不能以public方式调用,但能被子类继承。名字是能是oneventname。
7:在适当的地方调用事件触发方法触发事件。
下面是个例子,例子模仿容器和控件的模式,由控件触发一个事件,在容器中捕捉并且进行处理。
事件的触发者:
/// <summary>
/// 事件的触发者
/// </summary>
public class control
{
public delegate void somehandler(object sender, system.eventargs e);
/**
* 能采用系统提供的system.eventhandler, 这里为了说明情况使用了自己定义的delegate
* 如果需要在事件的参数中使用自己定义的类型,也要自己定义delegate
*/
//public event system.eventhandler someevent;
public event somehandler someevent;
public control()
{
//这里使用的delegate必须和事件中声名的一致
//this.someevent += new system.eventhandler(this.control_someevent);
this.someevent += new somehandler(this.processsomeevent);
}
public void raisesomeevent()
{
eventargs e = new eventargs();
console.write("please input a:");
string s = console.readline();
//在用户输入一个小a的情况下触发事件,否则不触发
if (s == "a")
{
someevent(this, e);
}
}
//事件的触发者自己对事件进行处理,这个方法的参数必须和代理中声名的一致
private void processsomeevent(object sender, eventargs e)
{
console.writeline("hello");
}
}
事件的接收者:
/// <summary>
/// 事件的接收和处理者
/// </summary>
class container
{
p rivate control ctrl = new control();
public container()
{
//这里使用的delegate必须和事件中声名的一致
//ctrl.someevent += new eventhandler(this.onsomeevent);
ctrl.someevent += new control.somehandler(this.responsesomeevent);
ctrl.raisesomeevent();
}
public static void main()
{
container pane = new container();
//这个readline是暂停程式用的,否则画面会一闪而过什么也看不见
console.readline();
}
//这是事件的接受者对事件的响应
p rivate void responsesomeevent(object sender, eventargs e)
{
console.writeline("some event occur!");
}
}
程式运行的结果如下:
please input a:a
hello
some event occur!
事件的应用
例如有下面的需求需要实现:程式主画面中弹出一个子窗口。此时主画面仍然能接收用户的操作(子窗口是非模态的)。子窗口上进行某些操作,根据操作的结果要在主画面上显示不同的数据。我发现一些程式员这样实现这个功能:
主画面弹出子窗口后,将自己的指针交给子画面,然后在子画面中使用这个指针,调用主画面提供的方法,改动主画面上的数据显示。这样虽然能达到目的,不过各个模块之间产生了非常强的耦合。一般说来模块之间的调用应该是单方向的:模块a调用了模块b,模块b就不应该反向调用a,否则就破坏了程式的层次,加强了耦合程度,也使得功能的改动和追加变得非常困难。
这时正确的做法应该是在子窗口的操作过程中发出各种事件,而由主窗口捕捉这些事件进行处理,各个模块专心的做自己的事情,不必过问其他模块的事情。
http://www.sudu.cn/info/html/edu/20071227/50118.html