前兩天在與朋友聊天時提到了事件,故寫下此文與他分享對事件的理解。因不敢獨享所以拿出來請大家指正。
在進行WinForm編程時,事件這個概念無時無刻都圍繞身邊。點個按鈕就是一個事件。在.Net中,事件這個概念總是讓人覺得比較復雜,有著深奧的理論,而且其中的delegate關鍵字本身就讓人覺得很深奧。
其實呢,事件並沒有那麼復雜而且深奧。只是MS為了讓程序員寫的代碼少一點,鼓搗出個代理的概念。其實如果您對Java的界面編程有所了解之後,對.Net事件的理解就會順利多了。當然,下面我們將先接觸一段Java的代碼。
在Java的GUI編程中,沒有代理這個概念,它用的是接口。我們先來看一個帶按鈕的窗口:
1import java.awt.event.ActionEvent;
2import java.awt.event.ActionListener;
3import java.awt.event.WindowAdapter;
4import java.awt.event.WindowEvent;
5import javax.swing.JButton;
6import javax.swing.JFrame;
7
8public class EventStudy {
9 public static void main(String[] args) {
10 JFrame f = new JFrame();
11 JButton b = new JButton();
12 f.addWindowListener(new WindowAdapter(){
13 @Override
14 public void windowClosing(WindowEvent e) {
15 System.exit(0);
16 }
17 });
18 f.setSize(300, 200);
19 b.setText("I'm a Button");
20 b.addActionListener(new ActionListener(){
21 @Override
22 public void actionPerformed(ActionEvent e) {
23 System.out.println("the Button is Clicked.");
24 }
25 });
26 f.add(b);
27 f.setVisible(true);
28 }
29}
30
現在,我們來看看上面的代碼。這是一個包含了一個代碼的窗體。其中,當單擊了按鈕之後,在控制台上會顯示“the Button is Clicked.”那麼Java是怎麼做到這些事情的呢?我們先來看看下面這段代碼:
1b.addActionListener(new ActionListener(){
2 @Override
3 public void actionPerformed(ActionEvent e) {
4 System.out.println("the Button is Clicked.");
5 }
6});
這段代碼其含義就是向JButton對象注冊一個事件監聽器。在Java中,事件監聽器就是一個接口。比如這個ActionListener接口,就包含一個actionPreformed方法。繼承了這個接口的類就可以傳入JButton對象的addActionListener方法中。一旦我們單擊按鈕之後,Swing的事件處理機制會調用actionPerformed方法,並把一個ActionEvent對象傳入這個方法。
好了,讓我們回到.Net。首先我們通過一個例子模擬一下按鈕被單擊的效果吧。當然,這個按鈕並不是一個真正的按鈕,不過是一個自己編寫的類罷了。
1using System;
2using System.Collections.Generic;
3using System.Linq;
4using System.Text;
5
6namespace EventStudy
7{
8 interface ActionListener
9 {
10 void actionPreformed();
11 }
12
13 class MyButton
14 {
15 private ActionListener al = null;
16 public MyButton() { }
17 public string Text { get; set; }
18
19 public void setActionListener(ActionListener al)
20 {
21 this.al = al;
22 }
23
24 public void ClickMe()
25 {
26 if(al != null)
27 al.actionPreformed();
28 }
29 }
30
31 class Program
32 {
33 class myActionListener : ActionListener
34 {
35 ActionListener Members#region ActionListener Members
36 public void actionPreformed()
37 {
38 Console.WriteLine("Button Clicked!");
39 }
40 #endregion
41 }
42
43 static void Main(string[] args)
44 {
45 MyButton b = new MyButton();
46 b.Text = "A Button.";
47 b.setActionListener(new myActionListener());
48 b.ClickMe();
49 }
50 }
51}
首先呢,我們要有一個ActionListener接口,然後是MyButton類來模擬實際的按鈕。MyButton類中的ClickMe方法就是模擬人工單擊按鈕這個過程。
最後,我們就在Main函數中設定好一切,然後“單擊”這個按鈕。
一般來說,當我們單擊一個按鈕之後,Windows會重繪按鈕的圖片,讓它看起來像是被按下去一樣,然後再去調用Click事件。在此我們省略重繪按鈕圖片的過程,直接讓它觸發事件。因此,ActionListener對象的actionPreformed方法就會被調用。
當然,上面用的是Java GUI對事件處理的設計模式。換到.Net中,我們可以把這個只包含一個方法的接口換成一個代理:
1namespace EventStudy
2{
3 delegate void OnClick();
4
5 class MyButton
6 {
7 private OnClick click = null;
8 public MyButton() { }
9 public string Text { get; set; }
10
11 public void setOnClickEvent(OnClick oc)
12 {
13 this.click = oc;
14 }
15
16 public void ClickMe()
17 {
18 if (click != null)
19 click();
20 }
21 }
22
23 class Program
24 {
25 static void Main(string[] args)
26 {
27 MyButton b = new MyButton();
28 b.Text = "A Button.";
29 b.setOnClickEvent(delegate()
30 {
31 Console.WriteLine("Button Clicked!");
32 });
33 b.ClickMe();
34 }
35 }
36}
現在,接口變成代理了,相應的一些代碼也有所改動。其結果還是一樣,不過代碼確實可以少寫點了。
對於事件處理,有一個多播的概念。那麼多播是怎麼回事呢?其實就是向一個事件注冊多個監聽者。如果不好理解就來看看下面的代碼吧:
1class MyButton
2{
3 private List<OnClick> clickEvents = new List<OnClick>();
4 public MyButton() { }
5 public string Text { get; set; }
6
7 public void addOnClickEvent(OnClick oc)
8 {
9 this.clickEvents.Add(oc);
10 }
11
12 public void ClickMe()
13 {
14 foreach (OnClick click in this.clickEvents)
15 click();
16 }
17}
18
19class Program
20{
21 static void Main(string[] args)
22 {
23 MyButton b = new MyButton();
24 b.Text = "A Button.";
25 b.addOnClickEvent(delegate()
26 {
27 Console.WriteLine("First Listener:Button Clicked!");
28 });
29 b.addOnClickEvent(delegate()
30 {
31 Console.WriteLine("Second Listener:Button Clicked!");
32 });
33 b.ClickMe();
34 }
35}
其實多播事件就是先用一個容器來保存所有注冊的事件監聽器(或者說處理函數),然後在觸發事件時順序地調用這些事件監聽器。
當然,在.Net中有一個event關鍵字來讓我們更輕松地完成這個任務:
1class MyButton
2{
3 public event OnClick Click;
4 public MyButton() { }
5 public string Text { get; set; }
6
7 public void ClickMe()
8 {
9 Click();
10 }
11}
12
13class Program
14{
15 static void Main(string[] args)
16 {
17 MyButton b = new MyButton();
18 b.Text = "A Button.";
19 b.Click += delegate()
20 {
21 Console.WriteLine("First Listener:Button Clicked!");
22 };
23 b.Click += delegate()
24 {
25 Console.WriteLine("Second Listener:Button Clicked!");
26 };
27 b.ClickMe();
28 }
29}
現在,我們的MyButton已經非常接近實際.Net類庫中的事件處理的設計方式了。目前的差距就是關於事件處理函數的參數。一般來說,WinForm的控件的事件處理函數都會包含兩個參數:sender和e,一個是觸發事件的對象sender和這個事件相關的參數。要讓我們的MyButton更貼近真實,我們來看看如何加入這兩個內容:
1class MyClickEventArgs : EventArgs
2{
3 public string MyMessage { set; get; }
4}
5
6delegate void OnClick(object sender, MyClickEventArgs e);
7
8class MyButton
9{
10 public event OnClick Click;
11 public MyButton() { }
12 public string Text { get; set; }
13
14 public void ClickMe()
15 {
16 MyClickEventArgs e = new MyClickEventArgs();
17 e.MyMessage = "This is a Message";
18 Click(this, e);
19 }
20}
21
22class Program
23{
24 static void Main(string[] args)
25 {
26 MyButton b = new MyButton();
27 b.Text = "A Button.";
28 b.Click += delegate(object sender, MyClickEventArgs e)
29 {
30 Console.WriteLine("Button Clicked!");
31 Console.WriteLine("Message:{0}", e.MyMessage);
32 Console.WriteLine("Button Text:{0}", ((MyButton)sender).Text);
33 };
34 b.ClickMe();
35 }
36}
首先,事件參數應該從EventArgs基類繼承。當然,編寫自己的事件參數類也不是不可以。至於sender,就是MyButton對象自己。至於EventArgs中到底有什麼參數應該視情況而定。
最後,讓我們來看看事件的先後順序。比如按鈕中的MouseDown,MouseUp,MouseClick這三個事件是順序觸發的。那麼這個過程是如何來實現的呢。下面我們繼續修改上面的代碼,加入兩個新的事件:BeforeClick和AfterClick。
1delegate void ClickEventHanler(object sender, MyClickEventArgs e);
2
3class MyButton
4{
5 public event ClickEventHanler BeforeClick;
6 public event ClickEventHanler OnClick;
7 public event ClickEventHanler AfterClick;
8
9 public MyButton() { }
10
11 public string Text { get; set; }
12
13 public void ClickMe()
14 {
15 MyClickEventArgs e;
16
17 e = new MyClickEventArgs();
18 e.MyMessage = "Before Click";
19 BeforeClick(this, e);
20
21 e = new MyClickEventArgs();
22 e.MyMessage = "On Click";
23 OnClick(this, e);
24
25 e = new MyClickEventArgs();
26 e.MyMessage = "After Click";
27 AfterClick(this, e);
28 }
29}
30
31class Program
32{
33 static void Main(string[] args)
34 {
35 MyButton b = new MyButton();
36 b.Text = "A Button.";
37 b.BeforeClick += new ClickEventHanler(Program.HandleEvent);
38 b.OnClick += new ClickEventHanler(Program.HandleEvent);
39 b.AfterClick += new ClickEventHanler(Program.HandleEvent);
40 b.ClickMe();
41 }
42
43 public static void HandleEvent(object sender, MyClickEventArgs e)
44 {
45 Console.WriteLine("Button Text:{0}", ((MyButton)sender).Text);
46 Console.WriteLine("Message:{0}", e.MyMessage);
47 }
48}
這次為了方便,我們把所有的事件處理代理都聲明為ClickEventHandler。然後在MyButton類中聲明3個事件:BeforeClick,OnClick,AfterClick。在Program類中,聲明一個HandleEvent方法作為事件處理的一個公共方法。
在這個例子中,變動最大的是ClickMe方法,這個方法會按照順序觸發3個事件,這樣我們就可以看到注冊的3個事件按照先後順序來觸發了。
至於為什麼每次觸發事件都要重新new一個EventArgs,這考慮到多線程的問題。當然,在沒有什麼苛刻環境的情況下,改變EventArgs的屬性然後再傳入事件也是可以的。至於每次都會new一個新的對象,在語意上也是合理的。畢竟每個事件的參數都應該是獨立的。
看完上面的這些例子之後,你會發現事件並沒有想像中那麼復雜,無非是一個函數的調用而已。而.Net之所以整出這麼多新的概念其目的就是讓我們編程的時候更加簡潔。想想看,是Java的事件監聽器寫起來方便還是.Net的代理寫起來方便呢?