原文链接:http://www.cnblogs.com/warensoft/archive/2010/03/19/1689806.html
在C#中,委托(delegate)是一种引用类型,在其他语言中,与委托最接近的是函数指针,但委托不仅存储对方法入口点的引用,还存储对用于调用方法的对象实例的引用。
简单的讲委托(delegate)是一种类型安全的函数指针,首先,看下面的示例程序,在C++中使用函数指针。
首先,存在两个方法:分别用于求两个数的最大值和最小值。
int Max(int x,int y)
{
return x>y?x:y;
}
int Min(int x,int y)
{
return x
}
上面两个函数的特点是:函数的返回值类型及参数列表都一样。那么,我们可以使用函数指针来指代这两个函数,并且可以将具体的指代过程交给用户,这样,可以减少用户判断的次数。
下面我们可以建立一个函数指针,将指向任意一个方法,代码如下所示:
//定义一个函数指针,并声明该指针可以指向的函数的返回值为int类型,参数列表中包//括两个int类型的参数
int (*p)(int,int);
//让指针p指向Max函数
p=max;
//利用指针调用Max
c=(*p)(5,6);
我们的问题在于,上面的代码中,为什么不直接使用Max函数,而是利用一个指针指向Max之后,再利用指针调用Max函数呢?
实际上,使用指针的方便之处就在于,当前时刻可以让指针p指向Max,在后面的代码中,我们还可以利用指针p再指向Min函数,但是不论p指向的是谁,调用p时的形式都一样,这样可以很大程度上减少判断语句的使用,使代码的可读性增强!
在C#中,我们可以使用委托(delegate)来实现函数指针的功能,也就是说,我们可以像使用函数指针一样,在运行时利用delegate动态指向具备相同签名的方法(所谓的方法签名,是指一个方法的返回值类型及其参数列表的类型)。
6.1 使用委托(delegate)
6.1.1 委托的建立
建立委托(delegate),过程有点类似于建立一个函数指针。过程如下:
1. 建立一个委托类型,并声明该委托可以指向的方法的签名(函数原型)
delegate void MyDelegate(int a,int b);
2.建立一个委托类的实例,并指向要调用的方法
//利用委托类的构造方法指定,这是最为常见的一种方式
MyDelegate md=new MyDelegate(Max);
//利用自动推断方式来指明要调用的方法,该形式更类型于函数指针
MyDelegate md=Max;
3.利用委托类实例调用所指向的方法
int c=md(4,5);
下面通过实例来演示C#中委托的使用。
l 案例操作020601:利用委托实现方法的动态调用
首先,添加如下控件:
Ø 两个RadioButton,分别用来让用户选择求最大值以及求最小值
Ø 二个TextBox,用来输入两个操作数
Ø 一个TextBox,用来显示运算结果
Ø 一个Button,用来执行运算
界面如下图所示:
下一步,在窗口中添加两个方法:Max,Min,这两方法的代码如下:
int Max(int x,int y)
{
return x>y?x:y;
}
int Min(int x,int y)
{
return x
}
窗口中的代码,如下图所示:
下一步:为了使用委托来实现动态指向,我们需要建立一个委托类“MyDelegate”,并建立该委托类型的一个实例,如下图所示:
上面的代码中,我们可以发现,此时,还没有让MyDelegate类型的实例“md”指向任何一个方法(即:md的值为null),原因是:在编写代码的时候,我们还不知道用户想要调用哪一个方法。
下一步,分别为两个RadioButton编写它们的“CheckedChanged”事件,代码如下:
private void rbtMax_CheckedChanged(object sender, EventArgs e)
{
if (this.rbtMax .Checked ==true)
{
this.md = new MyDelegate(this.Max );
}
}
private void rbtMin_CheckedChanged(object sender, EventArgs e)
{
if (this.rbtMin .Checked ==true)
{
this.md = new MyDelegate(this.Min );
}
}
这段代码是,如果用户选择了求最大值的RadioButton,则让MyDelegate类型的实例“md”指向Max方法,如果用户选择了求最小值的RadioButton,则让MyDelegate类型的实例“md”指向Min方法。这样作的目的,就是要把选择的过程交给用户。
下一步,我们为界面中的Button编写Click事件,并利用委托来调用求最值的方法。代码如下所示:
private void btGetResult_Click(object sender, EventArgs e)
{
if (this.md ==null )
{
MessageBox.Show("委托md没有指向任何方法!");
return;
}
int a = int.Parse(this.tbxOP1 .Text );
int b = int.Parse(this.tbxOP2 .Text );
int c = this.md(a,b);
this.tbxResult.Text = c.ToString();
}
从上面的代码中,可以发现,在使用委托之前,先要判断其值是否为空,如果不为空,则可以进行调用,同时,使用者可以看到,在调用md时,我们并没有关心md到底指向了哪一个方法,总之,md不为空的时候,就一定会指向Max和Min当中的一个。
为了让求最大值的RadioButton在程序开始运行的时候就被选中,在Form的Load事件中添加如下代码:
private void Form1_Load(object sender, EventArgs e)
{
this.md = new MyDelegate(this.Max );
}
运行的效果如下图所示:
求最大值
求最小值
l 委托使用的注意事项
Ø 在C#中,所有的委托都是从System.MulticastDelegate类派生的。
Ø 委托隐含具有sealed属性,即不能用来派生新的类型。
Ø 委托最大的作用就是为类的事件绑定事件处理程序。
Ø 在通过委托调用函数前,必须先检查委托是否为空(null),若非空,才能调用函数。
Ø 在委托实例中可以封装静态的方法也可以封装实例方法。
Ø 在创建委托实例时,需要传递将要映射的方法或其他委托实例以指明委托将要封装的函数原型(.NET中称为方法签名:signature)。注意,如果映射的是静态方法,传递的参数应该是类名.方法名,如果映射的是实例方法,传递的参数应该是实例名.方法名。
Ø 只有当两个委托实例所映射的方法以及该方法所属的对象都相同时,才认为它们是想等的(从函数地址考虑)。
6.1.2 讨论委托类型
从上面的案例中,我们可以发现,在使用委托之前,先要定义一个委托类型,如下所示:
delegate int MyDelegate(int a, int b);
MyDelegate md = null;
既然叫做委托类型,就说明MyDelegate实际上是一个类,上面的写法只是一种简单的缩略写法,实际上,我们自己定义的委托,都是继承自System.MulticastDelegate类的,但是我们确不能自己定义一个类去继承自System.MulticastDelegate类,为了证明这点,我们可以使用ildasm工具,来查看“MyDelegate”的IL代码。
首先在Visual Studio控制台,如下图所示:
在打开的“Visual Studio2008 Command Prompt”窗口中,输入ildam,如下图所示:
运行之后,会出现ildasm的窗口,如下图所示:
下一步,打开刚才编译好的exe程序,如下图所示:
展开结点,并找到“MyDelegate”类型,将其展开,如下图所示:
在上图中,我们可以看到,对“MyDelegate”,存在如下“说明”:
extends [mscorlib]System.MulticastDelegate
与此同时,还存在着四个方法,即:
Ø .ctor:构造方法
Ø BeginInvoke
Ø EndInvoke
Ø Invoke
l MulticastDelegate类
MultiDelegate类是一个特殊类(Special Class),和System.Delegate类一样,该类只能够被编译器以及内置的工具类所继承,我们自定义的类是不能够显式的继承自该类的。
MultiDelegate类当中可以包括一个委托的链表,这个表中,可以包括一个或多个元素(每个元素都是一个委托),我们可以将这个表称为调用链。当我们调用一个MultiDelegate的时候,位于该MultiDelegate调用链中的委托就会被串行调用。这样我们就可以只调用一个方法,而多个相同签名的方法就会同时被串行调用。关于多播委托的说明,我们会在后面的内容中进行讲解。
l Invoke方法
为了解释Invoke方法,我们先来回顾一下,当一个委托指向了一个方法时是如何调用的,代码如下所示:
int c = this.md(a,b);
我们在调用委托,并执行该委托所指向的方法时,本质上就是调用了其Invoke方法。实际上,我们可以直接调用其Invoke方法,代码如下所示:
int c = this.md.Invoke(a,b);
另外,与Invoke方法对应的BeginInvoke,是对Invoke方法的一个异步调用,而EndInvoke是异步调用完成后的处理方法,关于异步调用的说明,我们将在多线程的章节中进行说明。
6.1.3 使用多播委托(MulticastDelegate)
前面刚刚提及到MulticastDelegate,下面我们来看一下它的应用。
有的时候,我们想要调用一个委托,但同时可以执行多个方法(自定义事件中最为常见),比如,一个工作文档生成之后,系统要将生成文档日志,而且还要被保存到数据库中,对于以上二个操作,如果只想调用一个委托,就可以顺序完成,那么使用多播委托,就可以实现。
多播委托(MulticastDelegate)提供了一种类似于流水线式的钩子机制,只要加载到这条流水线上的委托,都会被顺序执行。因为所有的委托都继承自MulticastDelegate,因此所的委托都具备多播特性。
下面能过一个控制台程序来说明多播委托(MulticastDelegate)的使用方式。
l 案例操作050602:使用多播委托
首先,建立一个控制台程序。在其中添加两个具备相同签名的方法:
Ø void CreateLogFile(string originalPath):用于创建日志文件
Ø void WriteToDb(string originalPath):用于将文件写入数据库
代码如下:
方法:void CreateLogFile(string originalPath)
///
/// 用于生成日志文档
///
/// 文件的原始路径
static void CreateLogFile(string originalPath)
{
if (!Directory .Exists ("log"))
{
Directory.CreateDirectory("log");
}
StreamWriter sw = new StreamWriter("log/log.txt" ,true);
sw.WriteLine("新文件已经创建,创建时间:{0},文件路径:{1}",DateTime .Now .ToLongTimeString (),originalPath );
sw.Close();
Console.WriteLine("已经写入日志!");
}
方法:void WriteToDb(string originalPath)
///
/// 用于将文件写入数据库
///
/// 文件的原始路径
static void WriteToDb(string originalPath)
{
FileStream fs = new FileStream(originalPath ,FileMode.Open );
var buffer=new byte[fs.Length ];
fs.Read(buffer ,0,buffer.Length );
fs.Close();
SqlConnection con = new SqlConnection("server=.;database=test;uid=sa;pwd=sa");
SqlCommand cmd = con.CreateCommand();
cmd.CommandText = "insert into tb_files values(@ID,@FileName,@CreationTime,@FileBytes)";
cmd.Parameters.Add("@ID",SqlDbType.UniqueIdentifier).Value=Guid.NewGuid ();
cmd.Parameters.Add("@CreationTime",SqlDbType.DateTime).Value =DateTime.Now ;
cmd.Parameters.Add("@FileName",SqlDbType.NText).Value=Path.GetFileName (originalPath);
cmd.Parameters.Add("@FileBytes",SqlDbType.Image ).Value=buffer ;
con.Open();
cmd.ExecuteNonQuery();
con.Close();
Console.WriteLine("已经写入数据库");
}
上面两个方法,具备相同签名,如果想同时串行调用这两个方法,还要定义一个委托类型,代码如下:
///
/// 生成一个委托,用于实现多播操作
///
/// 文件的原始路径
delegate void MyMulticastDelegate(string path);
主函数代码如下所示:
static void Main(string[] args)
{
//创建原始文件
StreamWriter sw = new StreamWriter("new file.txt",false );
sw.WriteLine("this is a new file");
sw.Close();
//创建委托,并指向CreateLogFile方法
MyMulticastDelegate logDelegate=new MyMulticastDelegate (CreateLogFile);
//创建委托,并指向WriteToDb方法
MyMulticastDelegate dbDelagate = new MyMulticastDelegate(WriteToDb );
MyMulticastDelegate multicastDelegate = logDelegate;
//在多播委托的调用链中添加新的委托元素
multicastDelegate = multicastDelegate + dbDelagate;
//调用多播委托,并且序列执行两个委托所指向的方法
multicastDelegate("new file.txt");
}
在主函数中,首先创建一个原始文件,然后建立两个委托分别指向CreateLogFile方法以及WriteToDb方法,如下代码段所示:
MyMulticastDelegate logDelegate=new MyMulticastDelegate(CreateLogFile);
MyMulticastDelegate dbDelagate = new MyMulticastDelegate(WriteToDb );
下一步,将这两个方法合并到一个多播委托中,代码如下所示:
MyMulticastDelegate multicastDelegate = logDelegate;
multicastDelegate = multicastDelegate + dbDelagate;
最后,利用多播委托,同时串行执行两个操作,代码段如下所示:
multicastDelegate("new file.txt");
从上面的代码中,我们可以发现,对于两个委托来讲,“+”加操作是有意义的。如下面代码所示:
MyMulticastDelegate multicastDelegate = logDelegate;
multicastDelegate = multicastDelegate + dbDelagate;
这一点可以说明,如果想要将两个委托,放入到一个多播委托的调用链中,可以使用“+”操作符,换句话说,对于委托的“+”操作,就是在调用链中增加一个新的结点,并将一个新委托放置到该结点中。另外,和int类型的自加操作类似,委托的自加操作也进行简写,这种写法在注册事件的时候较为常用,代码如下:
MyMulticastDelegate multicastDelegate = logDelegate;
multicastDelegate += dbDelagate;
该案例的完整代码如下:
///
/// 用于生成日志文档
///
/// 文件的原始路径
static void CreateLogFile(string originalPath)
{
if (!Directory .Exists ("log"))
{
Directory.CreateDirectory("log");
}
StreamWriter sw = new StreamWriter("log/log.txt" ,true);
sw.WriteLine("新文件已经创建,创建时间:{0},文件路径:{1}",DateTime .Now .ToLongTimeString (),originalPath );
sw.Close();
Console.WriteLine("已经写入日志!");
}
///
/// 用于将文件写入数据库
///
/// 文件的原始路径
static void WriteToDb(string originalPath)
{
FileStream fs = new FileStream(originalPath ,FileMode.Open );
var buffer=new byte[fs.Length ];
fs.Read(buffer ,0,buffer.Length );
fs.Close();
SqlConnection con = new SqlConnection("server=.;database=test;uid=sa;pwd=sa");
SqlCommand cmd = con.CreateCommand();
cmd.CommandText = "insert into tb_files values(@ID,@FileName,@CreationTime,@FileBytes)";
cmd.Parameters.Add("@ID",SqlDbType.UniqueIdentifier).Value=Guid.NewGuid ();
cmd.Parameters.Add("@CreationTime",SqlDbType.DateTime).Value =DateTime.Now ;
cmd.Parameters.Add("@FileName",SqlDbType.NText).Value=Path.GetFileName (originalPath);
cmd.Parameters.Add("@FileBytes",SqlDbType.Image ).Value=buffer ;
con.Open();
cmd.ExecuteNonQuery();
con.Close();
Console.WriteLine("已经写入数据库");
}
///
/// 生成一个委托,用于实现多播操作
///
/// 文件的原始路径
delegate void MyMulticastDelegate(string path);
static void Main(string[] args)
{
//创建原始文件
StreamWriter sw = new StreamWriter("new file.txt",false );
sw.WriteLine("this is a new file");
sw.Close();
//创建委托,并指向CreateLogFile方法
MyMulticastDelegate logDelegate=new MyMulticastDelegate (CreateLogFile);
//创建委托,并指向WriteToDb方法
MyMulticastDelegate dbDelagate = new MyMulticastDelegate(WriteToDb );
MyMulticastDelegate multicastDelegate = logDelegate;
//在多播委托的调用链中添加新的委托元素
multicastDelegate = multicastDelegate + dbDelagate;
//调用多播委托,并且序列执行两个委托所指向的方法
multicastDelegate("new file.txt");
}
该案例的运行效果如下:
首先,系统中并不存在日志文件,如下图所示:
用于储存文件的数据库结构如下:
数据表中的原始数据为空,如下图所示:
执行完程序之后,窗口的效果如下图所示:
日志文件已经生成,内容如下图所示:
数据库的效果如下图所示:
6.1.4 匿名方法
在前面的代码中,用户可以发现,在使用委托时,无论该代码难易,都需要将功能性代码放置在一个方法中,再利用委托指向该方法。在C#2.0以及C#3.0中这种情况得到了改善,在C#2.0中,我们可以利用匿名方法(Anonymous Method)来简化委托的使用,在C#3.0中,我们可以Lambda表达式使其得到进一步简化,关于Lambda表达式的相关内容,请参见C#3.0程序设计。
所谓匿名方法(Anonymous Method),是指在使用委托时,可以不再事先定义一个方法,然后再让委托指向方法,匿名委托允许开发人员,使用内联方式,直接让一个委托指向一个功能代码段。下面代码对比了传统方法中委托的会用,以及利用匿名方法的简化操作:
l 传统方法使用委托:先定义一个方法,再定义委托,并指向方法
public void Run()
{
StreamWriter sw = new StreamWriter("e:\ex\log.txt",true );
for (int i = 0; i < 10000000; i++)
{
sw.WriteLine(i.ToString ());
}
sw.Close();
}
delegate void MyDelegate();
protected void Button2_Click(object sender, EventArgs e)
{
MyDelegate md = new MyDelegate(this.Run );
md();
}
l 利用匿名方法简化委托的使用
delegate void MyDelegate();
protected void Button2_Click(object sender, EventArgs e)
{
MyDelegate md = new MyDelegate(
delegate()
{
StreamWriter sw = new StreamWriter("e:\ex\log.txt", true);
for (int i = 0; i < 10000000; i++)
{
sw.WriteLine(i.ToString());
}
sw.Close();
}
);
md();
}
从上面代码的对比中,不难发现,使用匿名方法,省去了定义方法的步骤。实际上,在多线程编程的时候,使用匿名方法可以使得代码变的简化,并提高了可读性。下面代码是在不使用匿名方法的情况下编写多线程代码:
public void Run()
{
for (int i = 0; i < 1000000; i++)
{
this.textBox1.Text = i.ToString();
}
thread.Abort();
}
Thread thread = null;
private void button1_Click(object sender, EventArgs e)
{
CheckForIllegalCrossThreadCalls = false;
this.thread = new Thread(new ThreadStart (this.Run ));
this.thread.Start();
}
利用匿名方法,可以将上面的代码改写为:
private void button1_Click(object sender, EventArgs e)
{
CheckForIllegalCrossThreadCalls = false;
Thread thread = null;
thread = new Thread(
delegate()
{
for (int i = 0; i < 1000000; i++)
{
this.textBox1.Text = i.ToString();
}
thread.Abort();
}
);
thread.Start();
}
使用内联方式可以让代码更好理解!
6.2 自定义事件
自定义事件,是委托的一个典型应用!开发人员在编写WinForm或者ASP.NET应用程序时,都会使用Button的Click事件。那么,这种事件触机制是如何实现的呢?我们能不能编写自己定义的事件;另外,我们需要在什么时候编写自己定义的事件呢?
6.2.1 为什么要使用自定义事件
很多开发人员都会向笔者提出这样的问题—“我们为什么要自己编写事件?控件里面已经集成了很多的事件,这还不够我们用吗?”。
实现上,如果不清楚掌握如果编写自定义事件,想要开发出真正能用的应用程序是很困难的。其主要原因有以下两点:
1. 自定义控件
通常情况下,开发人员为了减少重复界面功能代码的编写量,往往会开发出一些用户控件或者是自定义控件。这时,我们就会产生问题,微软为我们提供的控件中包括了大量的事件,那么我们自己编写的控件中,是不是也会存在很多的事件呢?
2. 自己编写的组件及类库
事件并不是UI层的控件类所专有的,很多底层的类也都包括了事件,比如SqlConnection类,该类的类视图如下所示:
在这个类中就包括了一个InfoMessage事件,该事件作用是“当 SQL Server 返回一个警告或信息性消息时发生”。也就是说,在我们自己编写一个类的时候,往往会发生一种特殊的数据传递情况,即:数据的传递的方向是由下向上的。为了解释这种“由下向上”的传递方式,我们先来看一个应用程序的分层结构图:
上图是一个比较常规的Socket通信软件的三层构架图,客户端及服务器的界面是两个“EXE”程序,但是为了系统的逻辑更好管理,我们通常是将所有的通信逻辑封装到业务逻辑层的两个“DLL”文件中。图中箭头方向说明整个系统功能的实现,是界面的“EXE”调用底层的“DLL”实现的。
然而,所有通信的功能都是在DLL中所定义的类里实现的,那么,当客户端的DLL向服务器的DLL发送了一个消息,并被服务器的“DLL”所截获之后,服务器界面的“EXE”又是怎么得知的呢?我们平时在使用QQ,MSN的时候有没有想过这样的问题呢?
这个时候,又出现了我们刚才提到的那个问题—“由下向上”,即,由下层“DLL”中的类,通知上层“EXE”中的界面。我们总不能再使用DLL调用一次EXE吧!
注意:在一般情况下.NET平台的DLL是可以引用EXE的,但是如上图所示,EXE已经对DLL进行了引用,此时IDE就不再允许DLL对EXE进行引用,这是为了防止发生循环引用产生死锁。即使IDE允许我们再利用DLL引用EXE,引用之后所创建的实例和当前运行的界面的实例也是位于不同内存的,所以任何操作都不会生效。
此时,利用委托实现的事件机制就可以为我们解决问题!
6.2.2 控件中的事件
为了理解事件机制,首先从控件中的事件谈起。我们以Button的Click事件为例进行说明。
当开发人员双击了Button之后,IDE会将设计器转向代码视图,并为我们生成一个用于事件回调的方法,如下图所示:
注意:这里的button1_Click并不是事件,它仅仅是事件发生时所调用的方法!
我们的问题是,为什么在Button1的Click事件发生之后,button1_Click方法就会被调用呢?实际上,在我们双击Button1的时候,IDE自动的添加了一段代码,该段代码位于“Form1.Designer.cs”中(.NET1.1中并不包括Form1.Designer.cs),“Form1.Designer.cs”的位置如下图所示:
打开Form1.Designer.cs,并展开“InitializeComponent()”方法,找到第42行,如下图所示:
我们可以看到如下代码:
this.button1.Click += new System.EventHandler(this.button1_Click);
实际上这段代码,也就是所谓的事件注册代码。该代码的意思是:如果this.button1的Click事件发生之后,就转向this. button1_Click方法进行处理。
为了更好的理解事件的注册过程,我们先第42行代码进行修改,如下图所示:
这里,我们将原来的
this.button1.Click += new System.EventHandler(this.button1_Click);
修改为
this.button1.Click = new System.EventHandler(this.button1_Click);
在这个程序里,这里的修改是为了更好理解,当然这种写法是语法错误的。
下面我们对其进行分析:
首先,观察“=”右面的表达式。
new System.EventHandler(this.button1_Click);
通过6.1.1一节中的说明,大家可以发现,这段代码实际上是建立了一个委托类型的实例,并让该委托指向了this.button1_Click方法。也就是说,在程序运行的“某一时刻”,系统会通过这个委托实例间接的调用this.button1_Click方法。
然后,我们再来观察“=”左面的表达示。在C风格的语言中“=”是赋值表达式,也就是说,“=”两侧表达式的数据类型应该是一样的。因此,既然“=”右侧的表达式是一个委托类型(System.EventHandler)的实例,那么this.button1.Click也应该是一个委托类型(System.EventHandler)。
通过上面的说明,我们得到一个信息,前面这段事件注册代码,是让this.button1.Click和System.EventHandler(this.button1_Click)指向了同一段内存空间,简单来讲,就是让this.button1.Click指向了this.button1_Click方法,调用了this.button1.Click,就相当于调用了this.button1_Click方法。因此,我们说,当this.button1的Click事件发生之后,方法this.button1_Click就会被调用。
在程序运行的时候,系统会自己检测this.button1是否被点击了,如果被点击了,就在button1的内部调用button1.Click,这时,Windows窗口中的button1_Click方法就会被执行。
当然,事件注册代码完全可以手写。因为,除了控件中事件注册代码是自动生成以外,其他类中的事件注册都是手写的。手工注册事件的方法如下:
首先,可以在事件发生之前的任何代码中添加事件(通常是在窗口的构造方法中),下面我们来手工注册button1的MouseMove事件,如下图所示:
当我们写完“=”时,会出现一个提示“Press TAB to insert”,这时,我们只需要按2下“TAB”键,事件的注册以及用于回调的方法,就会自己添加到代码窗口里,如下图所示:
自动生成的代码是将this.button1的MouseMove事件指向了button1_MouseMove方法。这样手写的代码和IDE自动生成的代码是完全一样的。
当然,作为控件的事件,我们完全可以自动生成,如果想自动生成button1的其他事件,只需要查看button1的属性窗口,并点击“”按钮,就会出现该控件的事件列表,如下图所示:
然后双击你想要的事件,代码就会自动生成了。
在前的面代码中为了更好理解事件注册,我们曾将
this.button1.Click += new System.EventHandler(this.button1_Click);
修改为
this.button1.Click = new System.EventHandler(this.button1_Click);
我们会发现,无论是自己写的事件注册代码,还是自动生成的代码,都是使用“+=”来实现的,实际上,作为事件注册的代码,我们仅仅能够使用“+=”来实现注册,简单的使用“=”是语法错误的!!!
“+=”操作符在C风格语言中是常用的操作符,比如
int i=0;
i+=1;
等同于
int i=0;
i=i+1;
因此,
this.button1.Click += new System.EventHandler(this.button1_Click);
在原则上等同于
this.button1.Click = this.button1.Click +
new System.EventHandler(this.button1_Click);
用自然语言来描述上面的代码就是“一个委托=这个委托本身+另外一个委托”。那么委托相加意味着什么呢?
在6.1.3一节中,我们讨论过MultiDelegate(多播委托),而事件本身也是委托,并且所有委托都是System.MultiDelegate类的派生类,在6.1.3中,我们曾经演示过,多个委托类型实例相加,就是将这些委托实例存放在一个多播委托的调用链中,当调用多播委托时,该多播委托的调用链中的所有委托都会顺序的被调用。
利用多播委托的原理,我们可以将多个方法注册给一个事件,如下所示:
this.button1.Click +=new System.EventHandler(this.button1_Click);
this.button1.Click +=new System.EventHandler(this.button1_Click1);
this.button1.Click +=new System.EventHandler(this.button1_Click2);
上面的代码,就将三个方法注册到了button1的Click事件中,当button1的Click事件触发之后,方法button1_Click,button1_Click1,button1_Click2将会被顺序调用。这样作的好处是,我们可以将多个功能以及逻辑完全独立的操作放在不同的方法中,当事件发生之后,这些方法将会顺序的被调用,以实现我的需要的级联操作。
6.2.3 控件中事件的回调方法
说完了事件的注册,下面我们来谈一下事件的回调方法。首先,我们还要再一次回顾事件注册的代码:
this.button1.Click +=new System.EventHandler(this.button1_Click);
上面代码中,使用“new System.EventHandler(this.button1_Click)”将一个System.EventHandler委托类型的实例指向了this.button1_Click方法。通过6.1.1一节中所谈到的内容,我们知道,如果想让一个委托指向一个方法,那么该委托以及所被指向的方法一定要具备相同的签名(Signature,具备相同的参数列表,相同的返回值)。因此,System.EventHandler类型和this.button1_Click方法具备相同的签名,下面,我们来看一下System.EventHandler委托的签名是什么样的:
public delegate void EventHandler(
Object sender,
EventArgs e
)
System.EventHandler的签名是:返回值为void;有两个参数,Object sender, EventArgs e。因此button1_Click方法也具备相同形式,代码如下:
private void button1_Click(object sender, EventArgs e)
{
}
实际上,我们所能够看到的事件回调方法的签名基本上都着不多,只不过第二个参数略有区别,下面,我们对该方法的参数进行说明。
Ø Object sender
从该参数的命名上,可以看出其作用,sender(发送者)的意思是:谁触发的这个事件,那么sender就是谁,由于所有的类型在理论上讲都可以包括事件,因此sender的类型被定义成Object类型,当多个事件同时指向一个事件回调方法的时候,通过该参数可以区分出是哪一个类触发的事件,以便做出不同的处理,此时,需要对参数sender作出类型转化。
l 案例操作020603:多个事件指向同一个回调方法
首先,添加三个Button,一个TextBox
界面如下:
然后,在主窗口中添加一个方法ButtonClick,这三个按钮的Click事件将调用该方法。
代码如下:
protected void ButtonClick(object sender, EventArgs e)
{
Button bt = sender as Button;
this.textBox1.Text ="我是:"+ bt.Text;
}
上面代码中,为了知道点击的是哪个按钮,我们将sender转化成了Button类型。
下面来指定这三个按钮的Click事件回调方法
首先,切换到button1的属性窗口(F4),点击“”按钮,找到“Click”事件,并设置所调用的方法名为ButtonClick ,如下图所示。
然后,以相同的方法设置button2,button3的Click事件,并它们都指向ButtonClick方法。
最后,运行程序,下面是运行情况:
点击button1:
点击button2:
点击button3:
Ø EventArgs e
EventArgs类型是事件参数,当事件发生时,可以通过该参数来传递一些数据。当然EventArgs类本身是传递不了什么数据的,该类的类视图如下:
从类视图中不难发现,该类中的成员很少,通常情况下,如果想传递数据,那么事件参数类一般会是一个EventArgs类的派生类。比如TextBox类的KeyDown事件中事件参数就是一个KeyEventArgs类型的参数,事件回调方法的原型如下:
private void textBox1_KeyDown(object sender, KeyEventArgs e)
{
}
KeyEventArgs类的类视图如下所示:
该类中有三个属性:“Alt”,“Control”,“Shift”分另用来表示按下键盘某一个键的同时,有没有按下这三个功能键。
另外“KeyCode”属性可以用来表示当前用户所按的键位名。
在下一节中,我们会说明如何编写自定义事件参数。
l 案例操作020604:利用TextBox的KeyDown事件来模拟QQ聊天窗口
新建一个Windows窗口,包括以下控件
一个RichTextBox控件(rtbMessage):用来显示聊天信息
一个TextBox控件(tbxInput):用来输入聊天信息
一个Button控件( btSubmit):用来提交
界面如下所示:
功能如下:
点击button可以让消息传递到上面的RichTextBox中。当然,如果按“Ctrl+Enter”也可以使文字传递到RichTextBox中。
首先,我们在Windows窗口中添加一个方法,代码如下:
public void Display()
{
this.rtbMessage.AppendText(this.tbxInput .Text +"n");
this.tbxInput.Text = "";
}
该方法的功能就是将文本框中的文字添加到RichTextBox中。
下一步,编写Button的Click事件,代码如下:
private void btSubmit_Click(object sender, EventArgs e)
{
this.Display();
}
下一步编写TextBox的KeyDown事件,代码如下:
private void tbxInput_KeyDown(object sender, KeyEventArgs e)
{
if (e.Control ==true&&e.KeyCode .ToString ()=="Return")
{
this.Display();
}
}
该方法在执行前先检测用户是否同时按下的Control和回车键,然后再进行显示操作。
程序运行的效果如下:
按下Control+回车之后,文字上屏,效果如下:
在C#中,委托(delegate)是一种引用类型,在其他语言中,与委托最接近的是函数指针,但委托不仅存储对方法入口点的引用,还存储对用于调用方法的对象实例的引用。
简单的讲委托(delegate)是一种类型安全的函数指针,首先,看下面的示例程序,在C++中使用函数指针。
首先,存在两个方法:分别用于求两个数的最大值和最小值。
int Max(int x,int y)
{
return x>y?x:y;
}
int Min(int x,int y)
{
return x
}
上面两个函数的特点是:函数的返回值类型及参数列表都一样。那么,我们可以使用函数指针来指代这两个函数,并且可以将具体的指代过程交给用户,这样,可以减少用户判断的次数。
下面我们可以建立一个函数指针,将指向任意一个方法,代码如下所示:
//定义一个函数指针,并声明该指针可以指向的函数的返回值为int类型,参数列表中包//括两个int类型的参数
int (*p)(int,int);
//让指针p指向Max函数
p=max;
//利用指针调用Max
c=(*p)(5,6);
我们的问题在于,上面的代码中,为什么不直接使用Max函数,而是利用一个指针指向Max之后,再利用指针调用Max函数呢?
实际上,使用指针的方便之处就在于,当前时刻可以让指针p指向Max,在后面的代码中,我们还可以利用指针p再指向Min函数,但是不论p指向的是谁,调用p时的形式都一样,这样可以很大程度上减少判断语句的使用,使代码的可读性增强!
在C#中,我们可以使用委托(delegate)来实现函数指针的功能,也就是说,我们可以像使用函数指针一样,在运行时利用delegate动态指向具备相同签名的方法(所谓的方法签名,是指一个方法的返回值类型及其参数列表的类型)。
6.1 使用委托(delegate)
6.1.1 委托的建立
建立委托(delegate),过程有点类似于建立一个函数指针。过程如下:
1. 建立一个委托类型,并声明该委托可以指向的方法的签名(函数原型)
delegate void MyDelegate(int a,int b);
2.建立一个委托类的实例,并指向要调用的方法
//利用委托类的构造方法指定,这是最为常见的一种方式
MyDelegate md=new MyDelegate(Max);
//利用自动推断方式来指明要调用的方法,该形式更类型于函数指针
MyDelegate md=Max;
3.利用委托类实例调用所指向的方法
int c=md(4,5);
下面通过实例来演示C#中委托的使用。
l 案例操作020601:利用委托实现方法的动态调用
首先,添加如下控件:
Ø 两个RadioButton,分别用来让用户选择求最大值以及求最小值
Ø 二个TextBox,用来输入两个操作数
Ø 一个TextBox,用来显示运算结果
Ø 一个Button,用来执行运算
界面如下图所示:
下一步,在窗口中添加两个方法:Max,Min,这两方法的代码如下:
int Max(int x,int y)
{
return x>y?x:y;
}
int Min(int x,int y)
{
return x
}
窗口中的代码,如下图所示:
下一步:为了使用委托来实现动态指向,我们需要建立一个委托类“MyDelegate”,并建立该委托类型的一个实例,如下图所示:
上面的代码中,我们可以发现,此时,还没有让MyDelegate类型的实例“md”指向任何一个方法(即:md的值为null),原因是:在编写代码的时候,我们还不知道用户想要调用哪一个方法。
下一步,分别为两个RadioButton编写它们的“CheckedChanged”事件,代码如下:
private void rbtMax_CheckedChanged(object sender, EventArgs e)
{
if (this.rbtMax .Checked ==true)
{
this.md = new MyDelegate(this.Max );
}
}
private void rbtMin_CheckedChanged(object sender, EventArgs e)
{
if (this.rbtMin .Checked ==true)
{
this.md = new MyDelegate(this.Min );
}
}
这段代码是,如果用户选择了求最大值的RadioButton,则让MyDelegate类型的实例“md”指向Max方法,如果用户选择了求最小值的RadioButton,则让MyDelegate类型的实例“md”指向Min方法。这样作的目的,就是要把选择的过程交给用户。
下一步,我们为界面中的Button编写Click事件,并利用委托来调用求最值的方法。代码如下所示:
private void btGetResult_Click(object sender, EventArgs e)
{
if (this.md ==null )
{
MessageBox.Show("委托md没有指向任何方法!");
return;
}
int a = int.Parse(this.tbxOP1 .Text );
int b = int.Parse(this.tbxOP2 .Text );
int c = this.md(a,b);
this.tbxResult.Text = c.ToString();
}
从上面的代码中,可以发现,在使用委托之前,先要判断其值是否为空,如果不为空,则可以进行调用,同时,使用者可以看到,在调用md时,我们并没有关心md到底指向了哪一个方法,总之,md不为空的时候,就一定会指向Max和Min当中的一个。
为了让求最大值的RadioButton在程序开始运行的时候就被选中,在Form的Load事件中添加如下代码:
private void Form1_Load(object sender, EventArgs e)
{
this.md = new MyDelegate(this.Max );
}
运行的效果如下图所示:
求最大值
求最小值
l 委托使用的注意事项
Ø 在C#中,所有的委托都是从System.MulticastDelegate类派生的。
Ø 委托隐含具有sealed属性,即不能用来派生新的类型。
Ø 委托最大的作用就是为类的事件绑定事件处理程序。
Ø 在通过委托调用函数前,必须先检查委托是否为空(null),若非空,才能调用函数。
Ø 在委托实例中可以封装静态的方法也可以封装实例方法。
Ø 在创建委托实例时,需要传递将要映射的方法或其他委托实例以指明委托将要封装的函数原型(.NET中称为方法签名:signature)。注意,如果映射的是静态方法,传递的参数应该是类名.方法名,如果映射的是实例方法,传递的参数应该是实例名.方法名。
Ø 只有当两个委托实例所映射的方法以及该方法所属的对象都相同时,才认为它们是想等的(从函数地址考虑)。
6.1.2 讨论委托类型
从上面的案例中,我们可以发现,在使用委托之前,先要定义一个委托类型,如下所示:
delegate int MyDelegate(int a, int b);
MyDelegate md = null;
既然叫做委托类型,就说明MyDelegate实际上是一个类,上面的写法只是一种简单的缩略写法,实际上,我们自己定义的委托,都是继承自System.MulticastDelegate类的,但是我们确不能自己定义一个类去继承自System.MulticastDelegate类,为了证明这点,我们可以使用ildasm工具,来查看“MyDelegate”的IL代码。
首先在Visual Studio控制台,如下图所示:
在打开的“Visual Studio2008 Command Prompt”窗口中,输入ildam,如下图所示:
运行之后,会出现ildasm的窗口,如下图所示:
下一步,打开刚才编译好的exe程序,如下图所示:
展开结点,并找到“MyDelegate”类型,将其展开,如下图所示:
在上图中,我们可以看到,对“MyDelegate”,存在如下“说明”:
extends [mscorlib]System.MulticastDelegate
与此同时,还存在着四个方法,即:
Ø .ctor:构造方法
Ø BeginInvoke
Ø EndInvoke
Ø Invoke
l MulticastDelegate类
MultiDelegate类是一个特殊类(Special Class),和System.Delegate类一样,该类只能够被编译器以及内置的工具类所继承,我们自定义的类是不能够显式的继承自该类的。
MultiDelegate类当中可以包括一个委托的链表,这个表中,可以包括一个或多个元素(每个元素都是一个委托),我们可以将这个表称为调用链。当我们调用一个MultiDelegate的时候,位于该MultiDelegate调用链中的委托就会被串行调用。这样我们就可以只调用一个方法,而多个相同签名的方法就会同时被串行调用。关于多播委托的说明,我们会在后面的内容中进行讲解。
l Invoke方法
为了解释Invoke方法,我们先来回顾一下,当一个委托指向了一个方法时是如何调用的,代码如下所示:
int c = this.md(a,b);
我们在调用委托,并执行该委托所指向的方法时,本质上就是调用了其Invoke方法。实际上,我们可以直接调用其Invoke方法,代码如下所示:
int c = this.md.Invoke(a,b);
另外,与Invoke方法对应的BeginInvoke,是对Invoke方法的一个异步调用,而EndInvoke是异步调用完成后的处理方法,关于异步调用的说明,我们将在多线程的章节中进行说明。
6.1.3 使用多播委托(MulticastDelegate)
前面刚刚提及到MulticastDelegate,下面我们来看一下它的应用。
有的时候,我们想要调用一个委托,但同时可以执行多个方法(自定义事件中最为常见),比如,一个工作文档生成之后,系统要将生成文档日志,而且还要被保存到数据库中,对于以上二个操作,如果只想调用一个委托,就可以顺序完成,那么使用多播委托,就可以实现。
多播委托(MulticastDelegate)提供了一种类似于流水线式的钩子机制,只要加载到这条流水线上的委托,都会被顺序执行。因为所有的委托都继承自MulticastDelegate,因此所的委托都具备多播特性。
下面能过一个控制台程序来说明多播委托(MulticastDelegate)的使用方式。
l 案例操作050602:使用多播委托
首先,建立一个控制台程序。在其中添加两个具备相同签名的方法:
Ø void CreateLogFile(string originalPath):用于创建日志文件
Ø void WriteToDb(string originalPath):用于将文件写入数据库
代码如下:
方法:void CreateLogFile(string originalPath)
///
/// 用于生成日志文档
///
/// 文件的原始路径
static void CreateLogFile(string originalPath)
{
if (!Directory .Exists ("log"))
{
Directory.CreateDirectory("log");
}
StreamWriter sw = new StreamWriter("log/log.txt" ,true);
sw.WriteLine("新文件已经创建,创建时间:{0},文件路径:{1}",DateTime .Now .ToLongTimeString (),originalPath );
sw.Close();
Console.WriteLine("已经写入日志!");
}
方法:void WriteToDb(string originalPath)
///
/// 用于将文件写入数据库
///
/// 文件的原始路径
static void WriteToDb(string originalPath)
{
FileStream fs = new FileStream(originalPath ,FileMode.Open );
var buffer=new byte[fs.Length ];
fs.Read(buffer ,0,buffer.Length );
fs.Close();
SqlConnection con = new SqlConnection("server=.;database=test;uid=sa;pwd=sa");
SqlCommand cmd = con.CreateCommand();
cmd.CommandText = "insert into tb_files values(@ID,@FileName,@CreationTime,@FileBytes)";
cmd.Parameters.Add("@ID",SqlDbType.UniqueIdentifier).Value=Guid.NewGuid ();
cmd.Parameters.Add("@CreationTime",SqlDbType.DateTime).Value =DateTime.Now ;
cmd.Parameters.Add("@FileName",SqlDbType.NText).Value=Path.GetFileName (originalPath);
cmd.Parameters.Add("@FileBytes",SqlDbType.Image ).Value=buffer ;
con.Open();
cmd.ExecuteNonQuery();
con.Close();
Console.WriteLine("已经写入数据库");
}
上面两个方法,具备相同签名,如果想同时串行调用这两个方法,还要定义一个委托类型,代码如下:
///
/// 生成一个委托,用于实现多播操作
///
/// 文件的原始路径
delegate void MyMulticastDelegate(string path);
主函数代码如下所示:
static void Main(string[] args)
{
//创建原始文件
StreamWriter sw = new StreamWriter("new file.txt",false );
sw.WriteLine("this is a new file");
sw.Close();
//创建委托,并指向CreateLogFile方法
MyMulticastDelegate logDelegate=new MyMulticastDelegate (CreateLogFile);
//创建委托,并指向WriteToDb方法
MyMulticastDelegate dbDelagate = new MyMulticastDelegate(WriteToDb );
MyMulticastDelegate multicastDelegate = logDelegate;
//在多播委托的调用链中添加新的委托元素
multicastDelegate = multicastDelegate + dbDelagate;
//调用多播委托,并且序列执行两个委托所指向的方法
multicastDelegate("new file.txt");
}
在主函数中,首先创建一个原始文件,然后建立两个委托分别指向CreateLogFile方法以及WriteToDb方法,如下代码段所示:
MyMulticastDelegate logDelegate=new MyMulticastDelegate(CreateLogFile);
MyMulticastDelegate dbDelagate = new MyMulticastDelegate(WriteToDb );
下一步,将这两个方法合并到一个多播委托中,代码如下所示:
MyMulticastDelegate multicastDelegate = logDelegate;
multicastDelegate = multicastDelegate + dbDelagate;
最后,利用多播委托,同时串行执行两个操作,代码段如下所示:
multicastDelegate("new file.txt");
从上面的代码中,我们可以发现,对于两个委托来讲,“+”加操作是有意义的。如下面代码所示:
MyMulticastDelegate multicastDelegate = logDelegate;
multicastDelegate = multicastDelegate + dbDelagate;
这一点可以说明,如果想要将两个委托,放入到一个多播委托的调用链中,可以使用“+”操作符,换句话说,对于委托的“+”操作,就是在调用链中增加一个新的结点,并将一个新委托放置到该结点中。另外,和int类型的自加操作类似,委托的自加操作也进行简写,这种写法在注册事件的时候较为常用,代码如下:
MyMulticastDelegate multicastDelegate = logDelegate;
multicastDelegate += dbDelagate;
该案例的完整代码如下:
///
/// 用于生成日志文档
///
/// 文件的原始路径
static void CreateLogFile(string originalPath)
{
if (!Directory .Exists ("log"))
{
Directory.CreateDirectory("log");
}
StreamWriter sw = new StreamWriter("log/log.txt" ,true);
sw.WriteLine("新文件已经创建,创建时间:{0},文件路径:{1}",DateTime .Now .ToLongTimeString (),originalPath );
sw.Close();
Console.WriteLine("已经写入日志!");
}
///
/// 用于将文件写入数据库
///
/// 文件的原始路径
static void WriteToDb(string originalPath)
{
FileStream fs = new FileStream(originalPath ,FileMode.Open );
var buffer=new byte[fs.Length ];
fs.Read(buffer ,0,buffer.Length );
fs.Close();
SqlConnection con = new SqlConnection("server=.;database=test;uid=sa;pwd=sa");
SqlCommand cmd = con.CreateCommand();
cmd.CommandText = "insert into tb_files values(@ID,@FileName,@CreationTime,@FileBytes)";
cmd.Parameters.Add("@ID",SqlDbType.UniqueIdentifier).Value=Guid.NewGuid ();
cmd.Parameters.Add("@CreationTime",SqlDbType.DateTime).Value =DateTime.Now ;
cmd.Parameters.Add("@FileName",SqlDbType.NText).Value=Path.GetFileName (originalPath);
cmd.Parameters.Add("@FileBytes",SqlDbType.Image ).Value=buffer ;
con.Open();
cmd.ExecuteNonQuery();
con.Close();
Console.WriteLine("已经写入数据库");
}
///
/// 生成一个委托,用于实现多播操作
///
/// 文件的原始路径
delegate void MyMulticastDelegate(string path);
static void Main(string[] args)
{
//创建原始文件
StreamWriter sw = new StreamWriter("new file.txt",false );
sw.WriteLine("this is a new file");
sw.Close();
//创建委托,并指向CreateLogFile方法
MyMulticastDelegate logDelegate=new MyMulticastDelegate (CreateLogFile);
//创建委托,并指向WriteToDb方法
MyMulticastDelegate dbDelagate = new MyMulticastDelegate(WriteToDb );
MyMulticastDelegate multicastDelegate = logDelegate;
//在多播委托的调用链中添加新的委托元素
multicastDelegate = multicastDelegate + dbDelagate;
//调用多播委托,并且序列执行两个委托所指向的方法
multicastDelegate("new file.txt");
}
该案例的运行效果如下:
首先,系统中并不存在日志文件,如下图所示:
用于储存文件的数据库结构如下:
数据表中的原始数据为空,如下图所示:
执行完程序之后,窗口的效果如下图所示:
日志文件已经生成,内容如下图所示:
数据库的效果如下图所示:
6.1.4 匿名方法
在前面的代码中,用户可以发现,在使用委托时,无论该代码难易,都需要将功能性代码放置在一个方法中,再利用委托指向该方法。在C#2.0以及C#3.0中这种情况得到了改善,在C#2.0中,我们可以利用匿名方法(Anonymous Method)来简化委托的使用,在C#3.0中,我们可以Lambda表达式使其得到进一步简化,关于Lambda表达式的相关内容,请参见C#3.0程序设计。
所谓匿名方法(Anonymous Method),是指在使用委托时,可以不再事先定义一个方法,然后再让委托指向方法,匿名委托允许开发人员,使用内联方式,直接让一个委托指向一个功能代码段。下面代码对比了传统方法中委托的会用,以及利用匿名方法的简化操作:
l 传统方法使用委托:先定义一个方法,再定义委托,并指向方法
public void Run()
{
StreamWriter sw = new StreamWriter("e:\ex\log.txt",true );
for (int i = 0; i < 10000000; i++)
{
sw.WriteLine(i.ToString ());
}
sw.Close();
}
delegate void MyDelegate();
protected void Button2_Click(object sender, EventArgs e)
{
MyDelegate md = new MyDelegate(this.Run );
md();
}
l 利用匿名方法简化委托的使用
delegate void MyDelegate();
protected void Button2_Click(object sender, EventArgs e)
{
MyDelegate md = new MyDelegate(
delegate()
{
StreamWriter sw = new StreamWriter("e:\ex\log.txt", true);
for (int i = 0; i < 10000000; i++)
{
sw.WriteLine(i.ToString());
}
sw.Close();
}
);
md();
}
从上面代码的对比中,不难发现,使用匿名方法,省去了定义方法的步骤。实际上,在多线程编程的时候,使用匿名方法可以使得代码变的简化,并提高了可读性。下面代码是在不使用匿名方法的情况下编写多线程代码:
public void Run()
{
for (int i = 0; i < 1000000; i++)
{
this.textBox1.Text = i.ToString();
}
thread.Abort();
}
Thread thread = null;
private void button1_Click(object sender, EventArgs e)
{
CheckForIllegalCrossThreadCalls = false;
this.thread = new Thread(new ThreadStart (this.Run ));
this.thread.Start();
}
利用匿名方法,可以将上面的代码改写为:
private void button1_Click(object sender, EventArgs e)
{
CheckForIllegalCrossThreadCalls = false;
Thread thread = null;
thread = new Thread(
delegate()
{
for (int i = 0; i < 1000000; i++)
{
this.textBox1.Text = i.ToString();
}
thread.Abort();
}
);
thread.Start();
}
使用内联方式可以让代码更好理解!
6.2 自定义事件
自定义事件,是委托的一个典型应用!开发人员在编写WinForm或者ASP.NET应用程序时,都会使用Button的Click事件。那么,这种事件触机制是如何实现的呢?我们能不能编写自己定义的事件;另外,我们需要在什么时候编写自己定义的事件呢?
6.2.1 为什么要使用自定义事件
很多开发人员都会向笔者提出这样的问题—“我们为什么要自己编写事件?控件里面已经集成了很多的事件,这还不够我们用吗?”。
实现上,如果不清楚掌握如果编写自定义事件,想要开发出真正能用的应用程序是很困难的。其主要原因有以下两点:
1. 自定义控件
通常情况下,开发人员为了减少重复界面功能代码的编写量,往往会开发出一些用户控件或者是自定义控件。这时,我们就会产生问题,微软为我们提供的控件中包括了大量的事件,那么我们自己编写的控件中,是不是也会存在很多的事件呢?
2. 自己编写的组件及类库
事件并不是UI层的控件类所专有的,很多底层的类也都包括了事件,比如SqlConnection类,该类的类视图如下所示:
在这个类中就包括了一个InfoMessage事件,该事件作用是“当 SQL Server 返回一个警告或信息性消息时发生”。也就是说,在我们自己编写一个类的时候,往往会发生一种特殊的数据传递情况,即:数据的传递的方向是由下向上的。为了解释这种“由下向上”的传递方式,我们先来看一个应用程序的分层结构图:
上图是一个比较常规的Socket通信软件的三层构架图,客户端及服务器的界面是两个“EXE”程序,但是为了系统的逻辑更好管理,我们通常是将所有的通信逻辑封装到业务逻辑层的两个“DLL”文件中。图中箭头方向说明整个系统功能的实现,是界面的“EXE”调用底层的“DLL”实现的。
然而,所有通信的功能都是在DLL中所定义的类里实现的,那么,当客户端的DLL向服务器的DLL发送了一个消息,并被服务器的“DLL”所截获之后,服务器界面的“EXE”又是怎么得知的呢?我们平时在使用QQ,MSN的时候有没有想过这样的问题呢?
这个时候,又出现了我们刚才提到的那个问题—“由下向上”,即,由下层“DLL”中的类,通知上层“EXE”中的界面。我们总不能再使用DLL调用一次EXE吧!
注意:在一般情况下.NET平台的DLL是可以引用EXE的,但是如上图所示,EXE已经对DLL进行了引用,此时IDE就不再允许DLL对EXE进行引用,这是为了防止发生循环引用产生死锁。即使IDE允许我们再利用DLL引用EXE,引用之后所创建的实例和当前运行的界面的实例也是位于不同内存的,所以任何操作都不会生效。
此时,利用委托实现的事件机制就可以为我们解决问题!
6.2.2 控件中的事件
为了理解事件机制,首先从控件中的事件谈起。我们以Button的Click事件为例进行说明。
当开发人员双击了Button之后,IDE会将设计器转向代码视图,并为我们生成一个用于事件回调的方法,如下图所示:
注意:这里的button1_Click并不是事件,它仅仅是事件发生时所调用的方法!
我们的问题是,为什么在Button1的Click事件发生之后,button1_Click方法就会被调用呢?实际上,在我们双击Button1的时候,IDE自动的添加了一段代码,该段代码位于“Form1.Designer.cs”中(.NET1.1中并不包括Form1.Designer.cs),“Form1.Designer.cs”的位置如下图所示:
打开Form1.Designer.cs,并展开“InitializeComponent()”方法,找到第42行,如下图所示:
我们可以看到如下代码:
this.button1.Click += new System.EventHandler(this.button1_Click);
实际上这段代码,也就是所谓的事件注册代码。该代码的意思是:如果this.button1的Click事件发生之后,就转向this. button1_Click方法进行处理。
为了更好的理解事件的注册过程,我们先第42行代码进行修改,如下图所示:
这里,我们将原来的
this.button1.Click += new System.EventHandler(this.button1_Click);
修改为
this.button1.Click = new System.EventHandler(this.button1_Click);
在这个程序里,这里的修改是为了更好理解,当然这种写法是语法错误的。
下面我们对其进行分析:
首先,观察“=”右面的表达式。
new System.EventHandler(this.button1_Click);
通过6.1.1一节中的说明,大家可以发现,这段代码实际上是建立了一个委托类型的实例,并让该委托指向了this.button1_Click方法。也就是说,在程序运行的“某一时刻”,系统会通过这个委托实例间接的调用this.button1_Click方法。
然后,我们再来观察“=”左面的表达示。在C风格的语言中“=”是赋值表达式,也就是说,“=”两侧表达式的数据类型应该是一样的。因此,既然“=”右侧的表达式是一个委托类型(System.EventHandler)的实例,那么this.button1.Click也应该是一个委托类型(System.EventHandler)。
通过上面的说明,我们得到一个信息,前面这段事件注册代码,是让this.button1.Click和System.EventHandler(this.button1_Click)指向了同一段内存空间,简单来讲,就是让this.button1.Click指向了this.button1_Click方法,调用了this.button1.Click,就相当于调用了this.button1_Click方法。因此,我们说,当this.button1的Click事件发生之后,方法this.button1_Click就会被调用。
在程序运行的时候,系统会自己检测this.button1是否被点击了,如果被点击了,就在button1的内部调用button1.Click,这时,Windows窗口中的button1_Click方法就会被执行。
当然,事件注册代码完全可以手写。因为,除了控件中事件注册代码是自动生成以外,其他类中的事件注册都是手写的。手工注册事件的方法如下:
首先,可以在事件发生之前的任何代码中添加事件(通常是在窗口的构造方法中),下面我们来手工注册button1的MouseMove事件,如下图所示:
当我们写完“=”时,会出现一个提示“Press TAB to insert”,这时,我们只需要按2下“TAB”键,事件的注册以及用于回调的方法,就会自己添加到代码窗口里,如下图所示:
自动生成的代码是将this.button1的MouseMove事件指向了button1_MouseMove方法。这样手写的代码和IDE自动生成的代码是完全一样的。
当然,作为控件的事件,我们完全可以自动生成,如果想自动生成button1的其他事件,只需要查看button1的属性窗口,并点击“”按钮,就会出现该控件的事件列表,如下图所示:
然后双击你想要的事件,代码就会自动生成了。
在前的面代码中为了更好理解事件注册,我们曾将
this.button1.Click += new System.EventHandler(this.button1_Click);
修改为
this.button1.Click = new System.EventHandler(this.button1_Click);
我们会发现,无论是自己写的事件注册代码,还是自动生成的代码,都是使用“+=”来实现的,实际上,作为事件注册的代码,我们仅仅能够使用“+=”来实现注册,简单的使用“=”是语法错误的!!!
“+=”操作符在C风格语言中是常用的操作符,比如
int i=0;
i+=1;
等同于
int i=0;
i=i+1;
因此,
this.button1.Click += new System.EventHandler(this.button1_Click);
在原则上等同于
this.button1.Click = this.button1.Click +
new System.EventHandler(this.button1_Click);
用自然语言来描述上面的代码就是“一个委托=这个委托本身+另外一个委托”。那么委托相加意味着什么呢?
在6.1.3一节中,我们讨论过MultiDelegate(多播委托),而事件本身也是委托,并且所有委托都是System.MultiDelegate类的派生类,在6.1.3中,我们曾经演示过,多个委托类型实例相加,就是将这些委托实例存放在一个多播委托的调用链中,当调用多播委托时,该多播委托的调用链中的所有委托都会顺序的被调用。
利用多播委托的原理,我们可以将多个方法注册给一个事件,如下所示:
this.button1.Click +=new System.EventHandler(this.button1_Click);
this.button1.Click +=new System.EventHandler(this.button1_Click1);
this.button1.Click +=new System.EventHandler(this.button1_Click2);
上面的代码,就将三个方法注册到了button1的Click事件中,当button1的Click事件触发之后,方法button1_Click,button1_Click1,button1_Click2将会被顺序调用。这样作的好处是,我们可以将多个功能以及逻辑完全独立的操作放在不同的方法中,当事件发生之后,这些方法将会顺序的被调用,以实现我的需要的级联操作。
6.2.3 控件中事件的回调方法
说完了事件的注册,下面我们来谈一下事件的回调方法。首先,我们还要再一次回顾事件注册的代码:
this.button1.Click +=new System.EventHandler(this.button1_Click);
上面代码中,使用“new System.EventHandler(this.button1_Click)”将一个System.EventHandler委托类型的实例指向了this.button1_Click方法。通过6.1.1一节中所谈到的内容,我们知道,如果想让一个委托指向一个方法,那么该委托以及所被指向的方法一定要具备相同的签名(Signature,具备相同的参数列表,相同的返回值)。因此,System.EventHandler类型和this.button1_Click方法具备相同的签名,下面,我们来看一下System.EventHandler委托的签名是什么样的:
public delegate void EventHandler(
Object sender,
EventArgs e
)
System.EventHandler的签名是:返回值为void;有两个参数,Object sender, EventArgs e。因此button1_Click方法也具备相同形式,代码如下:
private void button1_Click(object sender, EventArgs e)
{
}
实际上,我们所能够看到的事件回调方法的签名基本上都着不多,只不过第二个参数略有区别,下面,我们对该方法的参数进行说明。
Ø Object sender
从该参数的命名上,可以看出其作用,sender(发送者)的意思是:谁触发的这个事件,那么sender就是谁,由于所有的类型在理论上讲都可以包括事件,因此sender的类型被定义成Object类型,当多个事件同时指向一个事件回调方法的时候,通过该参数可以区分出是哪一个类触发的事件,以便做出不同的处理,此时,需要对参数sender作出类型转化。
l 案例操作020603:多个事件指向同一个回调方法
首先,添加三个Button,一个TextBox
界面如下:
然后,在主窗口中添加一个方法ButtonClick,这三个按钮的Click事件将调用该方法。
代码如下:
protected void ButtonClick(object sender, EventArgs e)
{
Button bt = sender as Button;
this.textBox1.Text ="我是:"+ bt.Text;
}
上面代码中,为了知道点击的是哪个按钮,我们将sender转化成了Button类型。
下面来指定这三个按钮的Click事件回调方法
首先,切换到button1的属性窗口(F4),点击“”按钮,找到“Click”事件,并设置所调用的方法名为ButtonClick ,如下图所示。
然后,以相同的方法设置button2,button3的Click事件,并它们都指向ButtonClick方法。
最后,运行程序,下面是运行情况:
点击button1:
点击button2:
点击button3:
Ø EventArgs e
EventArgs类型是事件参数,当事件发生时,可以通过该参数来传递一些数据。当然EventArgs类本身是传递不了什么数据的,该类的类视图如下:
从类视图中不难发现,该类中的成员很少,通常情况下,如果想传递数据,那么事件参数类一般会是一个EventArgs类的派生类。比如TextBox类的KeyDown事件中事件参数就是一个KeyEventArgs类型的参数,事件回调方法的原型如下:
private void textBox1_KeyDown(object sender, KeyEventArgs e)
{
}
KeyEventArgs类的类视图如下所示:
该类中有三个属性:“Alt”,“Control”,“Shift”分另用来表示按下键盘某一个键的同时,有没有按下这三个功能键。
另外“KeyCode”属性可以用来表示当前用户所按的键位名。
在下一节中,我们会说明如何编写自定义事件参数。
l 案例操作020604:利用TextBox的KeyDown事件来模拟QQ聊天窗口
新建一个Windows窗口,包括以下控件
一个RichTextBox控件(rtbMessage):用来显示聊天信息
一个TextBox控件(tbxInput):用来输入聊天信息
一个Button控件( btSubmit):用来提交
界面如下所示:
功能如下:
点击button可以让消息传递到上面的RichTextBox中。当然,如果按“Ctrl+Enter”也可以使文字传递到RichTextBox中。
首先,我们在Windows窗口中添加一个方法,代码如下:
public void Display()
{
this.rtbMessage.AppendText(this.tbxInput .Text +"n");
this.tbxInput.Text = "";
}
该方法的功能就是将文本框中的文字添加到RichTextBox中。
下一步,编写Button的Click事件,代码如下:
private void btSubmit_Click(object sender, EventArgs e)
{
this.Display();
}
下一步编写TextBox的KeyDown事件,代码如下:
private void tbxInput_KeyDown(object sender, KeyEventArgs e)
{
if (e.Control ==true&&e.KeyCode .ToString ()=="Return")
{
this.Display();
}
}
该方法在执行前先检测用户是否同时按下的Control和回车键,然后再进行显示操作。
程序运行的效果如下:
按下Control+回车之后,文字上屏,效果如下:
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
原文链接:http://blog.csdn.net/kingmax54212008/article/details/2021565
委托类似于函数指针,但函数指针只能引用静态方法,而委托既能引用静态方法,也能引用实例方法。
委托使用分三步:1、委托声明。2、委托实例化。3、委托调用。
using System;
namespace 委托
{
delegate int NumOpe(int a,int b); //第一步:委托声明
class Class1
{
static void Main(string[] args)
{
Class1 c1 = new Class1();
NumOpe p1 = new NumOpe(c1.Add); //委托实例化,注意参数是要使用的参数名,且不带括号
Console.WriteLine(p1(1,2)); //委托调用
Console.ReadLine();
}
private int Add(int num1,int num2)
{
return(num1+num2);
}
}
}
例中,委托NumOpe引用了方法Add。
委托声明了以后,就可以象类一样进行实例化,实例化时把要引用的方法(如:Add)做为参数,这样委托和方法就关联了起来,就可以用委托来引用方法了。
委托和所引用的方法必须保持一致:
1、参数个数、类型、顺序必须完全一致。
2、返回值必须一致。
二、事件
事件有很多,比如说鼠标的事件:MouserMove,MouserDown等,键盘的事件:KeyUp,KeyDown,KeyPress。
有事件,就会有对事件进行处理的方法,而事件和处理方法之间是怎么联系起来的呢?委托就是他们中间的桥梁,事件发生时,委托会知道,然后将事件传递给处理方法,处理方法进行相应处理。
比如在WinForm中最常见的是按钮的Click事件,它是这样委托的:this.button1.Click += new System.EventHandler( this.button1_Click);按按钮后就会出发button1_Click方法进行处理。EventHandler就是系统类库里已经声明的一个委托。 -------委托实例化,this.buttion1_click为方法名
三、自定义事件及其处理
EventHandler以及其它自定义的事件委托都是一类特殊的委托,他们有相同的形式:
delegate void 事件委托名(object sender,EventArgs e);
object用来传递事件的发生者,比如二中的Button控件就是一个事件发生者;EventArgs用来传递事件的细节。
例程二:
using System;
namespace 最简单的自定义事件
{
/// <summary>
/// 事件发送类
/// </summary>
class Class1
{
public delegate void UserRequest(object sender,EventArgs e); //定义委托
public event UserRequest OnUserRequest; //定义一个委托类型的事件
public void run()
{
while(true)
{
if(Console.ReadLine()=="a")
{//事件监听
OnUserRequest(this,new EventArgs()); //产生事件
}
}
}
}
/// <summary>
/// 事件接收类
/// </summary>
class Class2
{
static void Main(string[] args)
{
Class1 c1 = new Class1();
c1.OnUserRequest += new Class1.UserRequest(c1_OnUserRequest); //委托实例化后绑定到事件
c1.run();
}
private static void c1_OnUserRequest(object sender, EventArgs e)
{//事件处理方法
Console.WriteLine("/t你触发了事件!");
}
}
}
例程三:
using System;
namespace 带事件数据的事件
{
/// <summary>
/// 带事件数据的事件类,从EventArgs继承
/// </summary>
class OnUserRequestEventArgs:EventArgs
{
private string inputText;
public string InputText
{
get
{
return inputText;
}
set
{
inputText = value;
}
}
}
/// <summary>
/// 事件发送类
/// </summary>
class Class1
{
public delegate void UserRequest(object sender,OnUserRequestEventArgs e); ------------->声明委托
public event UserRequest OnUserRequest; ------->定义一个委托类型的事件
public void run()
{
while(true)
{
Console.WriteLine("请输入内容:");
string a=Console.ReadLine();
//if(a=="a")
//{
OnUserRequestEventArgs e1 = new OnUserRequestEventArgs();
e1.InputText = a;
OnUserRequest(this,e1);
//}
}
}
}
/// <summary>
/// 事件接收类
/// </summary>
class Class2
{
[STAThread]
static void Main(string[] args)
{
Class1 c1 = new Class1();
c1.OnUserRequest += new Class1.UserRequest(c1_OnUserRequest);
c1.run();
}
private static void c1_OnUserRequest(object sender, OnUserRequestEventArgs e)
{
Console.WriteLine("/t你输入的是:"+e.InputText);
}
}
}
例程三跟例程二唯一的差别在于自定义了一个类OnUserRequestEventArgs,从EventArgs继承
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
原文链接:http://blog.csdn.net/huomm/article/details/1897010
曾经对委托和事件非常头疼,好在现在以自己的方式入门了,就行以自己的方式把它过一遍,当作学习日记吧。
委托(delegate)是一种引用类型,我们在处理他的时候要当作类来看待而不是方法,说白了委托就是对方法或者方法列表的引用,调用一个委托实例就好像是调用c++中的指针一样,他封装了对制定方法的引用,或者说委托起到的是桥梁的作用,实例后的委托对象会将给定的参数传递给他所回调的方法,并去执行方法。
看一个简单的例子:
//声明一个委托
delegate int myDelegateHandler(int a, int b);
public class A
{
//静态的处理方法
public static int M1(int a, int b)
{
int c = 0;
c = a + b;
return c;
}
}
//入口类
public class B
{
public static void Main()
{
//实例一个委托
myDelegateHandler mdh = new myDelegateHandler(A.M1);
//调用委托
int sum = mdh(2, 2);
Console.WriteLine(sum.ToString());
}
}
上面是一个非常简单的例子,具体的实现步骤:
1、 先声明个委托实例 ;
2、然后提供要处理的方法;
3、再实例化委托(把委托看作是类的话,实例化委托就不难理解了,其参数是要处理的方法,这里的方法 不用加括号,实例化的过程就是装载方法的过程,就好像需要参数的构造函数一样)实例化后的委托对象就好比是c++中的指针,它本身就是封装了方法的对象;
4、最后我们调用委托对象就好比是调用了被封装方法本身,调用时的参数也就传给了被封装的方法。
5、需要注意的是 所声明的委托无论是 参数个数,参数类型,返回值类型 都要和所要封装的方法保持一致,当调用委托实例对象时,所传入的参数也要保持一致 ,否则会出现错误。
委托链
我们知道委托是对方法的封装,而且委托可以封装很多方法形成委托链,其实委托就好像是一个容器,他封装了我们想要实现的若干方法,当调用委托对象(相当于c++中的指针)时,就会顺序的执行它所封装的所有的方法,如果有返回值的话,往往返回的是最后一个被执行的方法的返回值,委托链的形成可以用"+="或"-="对不同的委托实例进行二元操作。
委托链实例:
//定义一个委托
public delegate void PrintHandler(string message);
public class PrintProvider1
{
public void Print(string msg)
{
Console.WriteLine( msg + "1111111");
}
}
public class PrintProvider2
{
public void Print(string msg)
{
Console.WriteLine( msg + "2222222");
}
}
class Delegate1
{
public static void StaticPrint(string msg)
{
Console.WriteLine(msg + "3333333");
}
public static void Main()
{
string s = "委托链";
PrintProvider1 pp1 = new PrintProvider1();
PrintProvider2 pp2 = new PrintProvider2();
//创建委托实例
PrintHandler prn1 = new PrintHandler(pp1 .Print );
PrintHandler prn2 = new PrintHandler(pp2 .Print );
PrintHandler prn3 = new PrintHandler(StaticPrint );
Console.WriteLine("委托实例prn1的打印结果");
prn1(s );
Console.WriteLine("委托实例prn2的打印结果");
prn2(s);
Console.WriteLine("委托实例prn1+prn2的打印结果");
PrintHandler prn = prn1 + prn2;
prn(s);
Console.WriteLine("委托实例prn1+prn2+prn3的打印结果");
prn += prn3;
prn(s);
Console.WriteLine("委托实例prn1+prn3的打印结果");
prn -= prn2;
prn(s);
Console.WriteLine("委托实例prn3的打印结果");
prn -= prn1;
prn(s);
Console.WriteLine("试图调用null委托会引发异常");
try
{
prn -= prn3;
prn(s);
}
catch (NullReferenceException ex)
{
Console.WriteLine(ex .Message );
}
Console.WriteLine("试图从null中移出委托是无效操作");
try
{
prn -= prn3;
}
catch (NullReferenceException ex)
{
Console.WriteLine(ex .Message );
}
Console.Read();
}
}
执行结果
委托实例prn1的打印结果
委托链1111111
委托实例prn2的打印结果
委托链2222222
委托实例prn1+prn2的打印结果
委托链1111111
委托链2222222
委托实例prn1+prn2+prn3的打印结果
委托链1111111
委托链2222222
委托链3333333
委托实例prn1+prn3的打印结果
委托链1111111
委托链3333333
委托实例prn3的打印结果
委托链3333333
试图调用null委托会引发异常
未将对象引用设置到对象的实例。
试图从null中移出委托是无效操作