前兩天在與朋友聊天時提到了事件,故寫下此文與他分享對事件的理解。因不敢獨享所以拿出來請大家指正。
在進行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的代理寫起來方便呢?