从WinForm程序中看委托和事件
作为一个自学C#的小白,无论我们的学习起点是各种书籍还是视频,最开始总是从控制台程序和窗体应用程序,一行简单的Console.WriteLine("Hello World");或者是一个窗体几个控件就能实现一个小程序。作者本意或许是想告诉初学者们,编程并不难,并且很有趣。消除学生的畏难情绪,培养学习兴趣。但是,这会不会在学生心中留下一个印象,编程不过如此?从此很长一段时间内,编程水平只是停留在拖控件。我们所不知道的是,这种简单背后,是功能强大的visual studio和C#语言规范在支撑。在我学习委托和事件时,在博客园,编程书,视频网站各种渠道去找资料,却总是一知半解。并不是这些资料讲解的不到位,相反,每篇文章都会在某一点上对我有新的启发,如果没有这些资料,今天我也写不出这篇文章,只是在看资料之后,之前的我没有真正的去思考,更准确一点说,是不知道如何去思考。
这篇文章不敢说写的多好,存在错误也绝非我所愿,这并不是一句谦辞,实在是我目前编程水平有限,也欢迎看到这篇文章的朋友能够指出其中错误。我愿意把它当成一个起点,从这里开始,一步步去积累提高自己。
就从窗体应用程序说起吧
在创建项目时,vs自动生成的Form1类,继承了Form类。在主程序program.cs中,创建了Form1类的实例,并调用Application.Run(new Form1())来运行。这里有两点需要注意:
1、 Form1类就是一个普通类,和我们自己后面添加的类文件没有什么不同。
2、 Form1类的修饰符partial,这个类有两部分组成:第一部分是开发人员自己编写的程序代码,存放在Form1.cs文件,我们在vs中选中窗体按F7显示的那些代码。第二部分是Form1.Designer.cs文件,这里的代码是vs自动生成的,都是和Form1窗体中的控件有关的,可以认为该文件负责管理窗体中的所有控件。
先来看Form1.Designer.cs(后面用Form1类代替,知道这些代码是写在该文件中即可)
其中包含两个方法,Dispose()和InitializeComponent(),还有一些私有字段,如panel1,button1。Dispose()是在Form1类中实现父类Form类中的虚方法,释放Form1类所占用的资源,不用多说。从InitializeComponent()中,可以知道当我们在vs中向一个窗体拖动一个控件时,究竟发生了什么?
例如,向一个空窗体中添加一个button按钮,这是每个学习winform编程的人第一节课必备操作,此时vs的操作是:
1、 在Form1类中声明一个私有字段 button1
private System.Windows.Forms.Button button1;
button1是System.Windows.Forms.Button类型的变量,转到Button类的定义,可以看到该类继承了System.Windows.Forms.ButtonBase和System.Windows.Forms.IButtonControl,而System.Windows.Forms.ButtonBase继承了System.Windows.Forms.Control类。到此为止,我们等下再去仔细研究Control类的内容。
【拓展:和Button类似,Winform中其他控件类也都是这种方式实现的。如果想自己设计一款控件,就可以按照这种方式来实现。先设计一个MyComponent类,让它继承自Control类,然后在MyComponent类中实现该控件的特有功能】
2、 vs做的第二件事,是在InitializeComponent()方法中对button1变量进行了实例化:
this.button1 = new System.Windows.Forms.Button();
我们转到Button类、ButtonBase类、Control类的定义中可以看到,其中有许多属性
3、 vs做的第三件事就是对button1对象的常用属性进行了初始化(这里我截取一部分)
//
// button1
//
this.button1.BackColor = System.Drawing.Color.ForestGreen;
this.button1.Font = new System.Drawing.Font("Microsoft YaHei", 12F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
this.button1.Location = new System.Drawing.Point(20, 273);
如果我们后面在设计器中对控件的其他属性做了修改,vs也会将代码添加到这里。
到此为止,在我们没有对button1控件做任何操作之前,vs为我们做了以上三件事,来简化编程流程,让我们更专注于Form1.cs文件中代码的开发。
按钮控件最常用的功能就是点击Click,下面来看下这个过程是怎么实现的。
在设计器中双击该控件(button控件的默认事件就是click),vs在Form1.cs中会自动生成并跳转到button1_Click方法,我们在这个方法体内部实现button1按钮被点击时程序要执行的操作。
private void button1_Click(object sender, EventArgs e)
{
//
}
可以看到,该方法有两个参数,第一个是object类型,sender;第二个是System.EventArgs类型,e。这两个参数是什么意思,会留到很后面再说,在这里简单提一下。
同时,vs在Form1.Designer.cs中,会生成如下代码:
this.button1.Click += new System.EventHandler(this.button1_Click); (1)
这行代码什么意思?它跟我们点击按钮要实现的功能有什么关系?
举个例子,你用美团app定了一份水饺,+=符号就是“订”这个动作。+=左边就表示水饺,而右边明确指出这是一份牛肉水饺。(简单理解)
+=:称为委托操作符,字面意思可理解为“订阅”。
在它的左边是this.button1.Click是一个“事件Event”,查看其定义可以发现,这是一个声明在Control类中的东西(在不知道它到底是啥之前,暂且称之为东西):
public event EventHandler Click; (2)
这东西既不是属性,也不是字段,更不是方法,看起来更像变量声明。从声明中可以看出,有一个event关键字。这个东西是EventHandler类型的。
我们从EventHandler类型入手,在vs中F12转到EventHandler类型的定义,可以看到以下内容:
public delegate void EventHandler(object sender, EventArgs e); (3)
很明显,这又是一个声明,看起来和方法声明差不多,多了一个delegate关键字,这个单词有“授权,委托”的意思,当我们再想看看delegate是什么东西的时候,vs出现了“无法导航到插入符号下的符号”弹窗。
到这里,我们既没有搞明白+=左边的this.button1.Click到底是什么?它跟Control类中的Click有什么关系?而且还多了一个delegate关键字和event关键字不知道是干嘛的。
这个时候把程序编译下,看源码。需要弄明白
public delegate void EventHandler(object sender, EventArgs e);
这句代码到底做了什么。这句代码的字面意思是声明一个名为EventHandler的委托,显然,delegate是声明委托的关键字。
从下面这张图可以看出,当声明GreetingDelegate委托时,编译器自动声明一个密封类,类名就是GreetingDelegate。所以声明委托就是声明类,委托本质上就是一个类。
类中有一个构造函数和BeginInvoke、EndInvoke、Invoke方法。
——图片引用自https://www.cnblogs.com/JimmyZhang/archive/2007/09/23/903360.html,作者是《.NET之美》一书的作者,有兴趣的朋友可以看下。
我们知道,String类表示字符串,Int32类表示32位有符号整数。既然委托就是类,那EventHandler类表示什么?
“为了增强灵活性和减少重复代码,可以将方法作为参数传递给另一方法。为了能将方法作为参数传递,必须要有一个能表示方法的数据类型。这个数据类型就是委托”
——《C#本质论第六版》
现在我们知道,EventHandler类表示方法。C#中方法成千上万,都用一个类肯定不能表示。所以一个委托只能表示一类方法。哪一类方法呢?
看一下最开始vs自动生成的button1_Click方法:
private void button1_Click(object sender, EventArgs e)
委托声明:
public delegate void EventHandler(object sender, EventArgs e)
二者相同点有返回值,参数类型和参数顺序。所以,委托就表示它声明中返回值,参数类型和顺序相同的一类方法。
现在回到(2),现在我们知道这个Click就是一个EventHandler类型的“事件”,表示一个参数是(object sender, EventArgs e)的方法。这里要注意,Click是一个事件,而不是一个委托。
event关键字是干嘛用的呢?这就涉及到publish-subscribe模式和委托的缺点。具体内容见《C#本质论第六版》P378-384。简单来讲,就是委托的封装不充分,而事件的封装更充分,更不容易出错,事件是一种特殊的委托。所以在publish-subscribe模式中,我们使用事件。
最后一个问题,我们知道Click事件声明在Control类中,而Button类通过ButtonBase类,间接地继承于Control类,所以Button类的实例button1自然也可以调用Click事件了。
总结一下,等号左边的this.button1.Click是一个事件,是EventHandler类型的一个变量。
+=右边,就很简单了,使用new关键字,实例化一个EventHandler类型的对象。这个对象指向Form1类中button1_Click方法
this.button1.Click += new System.EventHandler(this.button1_Click);
这行代码含义:为该类中button1_Click()方法订阅button1.Click事件。
问题在于,为什么是+=,而不是=?
我们习惯了下面这种写法:把右边的对象赋值给左边的变量。
String str = “abc”;
FileInfo f = new FileInfo(@“”);
这就是事件的作用,也就是前面提到的,为什么事件比委托封装的更充分,更不易出错。具体内容见C#本质论P383“高级主题:事件的内部机制”
这里只要知道,在调用事件时,赋值操作符是禁用的。只能使用+=或-=来订阅或者取消订阅。
继续说,
我们废了这么多劲去订阅这个事件,为了什么呢?答:人机交互。
当用户点击了界面上的按钮(Click事件被触发,实际上是调用了Click事件的Invoke()方法,前面有提到),就会调用button1_Click方法,程序就会继续运行来执行某种我们希望的操作。
在控制台应用程序中,Control类是Click事件的发布者(Publisher),而Form1类中的button1_Click()是Click事件的订阅者(Subscriber)。当Click.Invoke()方法被调用,发布者就会通知所有订阅者。
假设这样一种情况,如果发布者中包含订阅者感兴趣的数据,这些数据对订阅者的执行至关重要,数据应该如何传递给订阅者??
更直接一点,委托声明:
public delegate void EventHandler(object sender, EventArgs e)
sender和e是什么?EventArgs是什么类型?要弄明白这个问题,再看一段新的代码:
1 using System; 2 3 namespace ConsoleApp1 4 { 5 public class Thermostat 6 { 7 public class TemperatureArgs:EventArgs 8 { 9 public float NewTemperature { get; set; } 10 11 public TemperatureArgs(float newTemperature) 12 { 13 this.NewTemperature = newTemperature; 14 } 15 16 } 17 18 public event MyEventHandler<TemperatureArgs> OnTemperatureChange; 19 20 private float currentTemperature; 21 public float CurrentTemperature 22 { 23 get { return currentTemperature; } 24 25 set 26 { 27 if (value != currentTemperature) 28 { 29 currentTemperature = value; 30 31 OnTemperatureChange?.Invoke(this, new TemperatureArgs(value)); 32 } 33 } 34 } 35 public int Number { get; set; } 36 37 } 38 }
1 using System; 2 3 4 namespace ConsoleApp1 5 { 6 class Program 7 { 8 static void Main(string[] args) 9 { 10 Heater heater1 = new Heater(90); 11 12 Cooler cooler = new Cooler(60); 13 Thermostat thermostat1 = new Thermostat() { CurrentTemperature =0,Number=1}; 14 Thermostat thermostat2 = new Thermostat() { CurrentTemperature = 0, Number = 2 }; 15 16 thermostat1.OnTemperatureChange += heater1.OnTemperatureChanged; 17 thermostat2.OnTemperatureChange += heater1.OnTemperatureChanged; 18 19 thermostat1.OnTemperatureChange += cooler.OnTemperatureChanged; 20 21 thermostat1.CurrentTemperature = 100; //温度变化 22 Console.ReadKey(); 23 24 Console.WriteLine("Hello World"); 25 } 26 } 27 28 //声明泛型委托 29 public delegate void MyEventHandler<TEventArgs>(object sender, TEventArgs e) 30 where TEventArgs : EventArgs; 31 32 public class Heater 33 { 34 public Heater(float temp) 35 { 36 this.Temperature = temp; 37 } 38 39 public float Temperature; 40 41 public void OnTemperatureChanged(object sender,Thermostat.TemperatureArgs e) 42 { 43 Thermostat thermostat = (Thermostat)sender; 44 if (thermostat.Number==1) 45 { 46 Console.WriteLine("Thermostat Number : {0}",1); 47 } 48 else if (thermostat.Number==2) 49 { 50 Console.WriteLine("Thermostat Number : {0}", 2); 51 } 52 53 float newTemperature = e.NewTemperature; 54 55 if (newTemperature>Temperature) 56 { 57 Console.WriteLine("Cooler:ON"); 58 } 59 else 60 { 61 Console.WriteLine("Cooler:OFF"); 62 } 63 } 64 } 65 66 public class Cooler 67 { 68 public Cooler(float temperature) 69 { 70 this.Temperature = temperature; 71 } 72 public float Temperature; 73 74 public void OnTemperatureChanged(object sender, Thermostat.TemperatureArgs e) 75 { 76 float newTemperature = e.NewTemperature; 77 78 if (newTemperature<Temperature) 79 { 80 Console.WriteLine("Heater:ON"); 81 } 82 else 83 { 84 Console.WriteLine("Heater:OFF"); 85 } 86 } 87 } 88 }
基本功能介绍:Thermostat类代表恒温器,控制水温保持在一定范围内。Heater类和Cooler类分别是加热器和冷却器,负责调节水温。恒温器能够获取当前水温值,加热器和冷却器根据当前水温值和预设水温值水温值比较,做出相应动作。
在该例中,Thermostat是“温度变化”事件的发布者,Heater类和Cooler中OnTemperatureChanged()是该事件订阅者,并需要获取当前温度值。
首先,在主程序中声明泛型委托:
public delegate void MyEventHandler<TEventArgs>(object sender, TEventArgs e)
where TEventArgs : EventArgs;
这是一种比较规范的写法,理论上任何委托类型都可以使用。
第一个参数sender:是调用委托的那个类的实例。它有什么作用呢?
假设有两个Thermostat的实例,Heater.OnTemperatureChanged()订阅了这两个实例中的OnOnTemperatureChange事件。此时,任何一个实例都可能触发对OnTemperatureChanged()的调用。判断具体是哪个Thermostat实例触发了事件,需要在Heater.OnTemperatureChanged()内部利用sender参数进行判断。
我们为Thermostat类增加Number属性,代表恒温器编号。在主程序中创建两个Thermostat对象thermostat1和thermostat2,并将编号分别设备1,2,初始温度均为0.
在Heater.OnTemperatureChanged()中,将sender转换为Thermostat对象。并根据该对象的Number属性值执行相应的操作。
第二个参数e:它包含了事件的附件数据。数据类型是TEventArgs,从泛型约束中可知,该类继承自EventArgs类,EventArgs类的定义中只有一个Empty属性,用来指出不存在事件数据。
我们在TEventArgs类中添加了一个新属性NewTemperature,用于将温度从恒温器传递给订阅者。所以这个e参数就是我们用来传递订阅者感兴趣的数据的。
刚才的Number属性也可以添加在TEventArgs类中作为感兴趣数据传递出去,作用都是一样的。
一种最简单,也最常用的委托声明,就是前面提到的EventHandler委托:
Public delegate void EventHandler(object sender,EventArgs e)
类比上面的说明,button1_Click()方法订阅了this.button1.Click事件,我们在该方法中就可以利用sender参数来访问button1对象的各种数据,这里我以访问Text属性为例。
Button button1 = (Button)sender;
string text = button1.Text;
PS:这并不是最简单的方法,这里只是说明下可以这么用。直接用this.button1.Text简单。
这么声明委托就是一种简便方法,我把所有数据都声明在调用委托的类中,使用sender就能访问到所有数据,e变量中不放任何数据。