1. 程式人生 > >C#圖解教程 第十四章 事件

C#圖解教程 第十四章 事件

事件

釋出者和訂閱者


很多程式都有一個共同的需求,既當一個特定的程式事件發生時,程式的其他部分可以得到該事件已經發生的通知。 
釋出者/訂閱者模式(publisher/subscriber pattern)可以滿足這種需求。


  • 釋出者(publisher) 釋出某個事件的類或結構,其他類可以在該事件發生時得到通知
  • 訂閱者(subscriber) 註冊並在事件發生時得到通知的類或結構
  • 事件處理程式(event handler) 由訂閱者註冊到事件的方法,在釋出者觸發事件時執行。事件處理程式可定義在事件所在的類或結構中,也可定義在不同的類或結構中
  • 觸發(raise)事件 呼叫(invoke)或觸發(fire)事件的術語。當事件觸發時,所有註冊到它的方法都會被依次呼叫

前一章介紹了委託。事件的很多部分都與委託類似。實際上,事件就像專門用於特殊用途的簡單委託。事件包含了一個私有的委託。 
 
有關事件的私有委託需要了解的重要事項如下:

  • 事件提供了對它的私有控制委託的結構化訪問。即,你無法直接訪問委託
  • 事件中可用的操作比委託少,對於事件我們只可新增、刪除或呼叫事件處理程式
  • 事件被觸發時,它呼叫委託來依次呼叫呼叫列表中的方法

原始碼元件概覽


需要在事件中使用的程式碼有5部分。

  • 委託型別宣告 事件和事件處理程式必須有共同的簽名和返回型別,它們通過委託型別進行描述
  • 事件處理程式宣告 訂閱者類中會在事件觸發時執行的方法宣告。它們不一定是有顯式命名的方法,還可以是第13章描述的匿名方法或Lambda表示式
  • 事件宣告 釋出者類必須宣告一個訂閱者類可以註冊的事件成員。當宣告的事件為public時,稱為釋出了事件
  • 事件註冊 訂閱者必須訂閱事件才能在它被觸發時得到通知
  • 觸發事件的程式碼 釋出者類中“觸發”事件並導致呼叫註冊的所有事件處理程式的程式碼

 

宣告事件


釋出者類必須提供事件物件。建立事件比較簡單–只需要委託型別和名字。事件宣告的語法如下程式碼所示。 
程式碼中聲明瞭CountADozen事件。

  • 事件宣告在一個類中
  • 它需要委託型別的名稱,任何附加到事件(如註冊)的處理程式都必須與委託型別的簽名和返回型別匹配
  • 它宣告為public,這樣其他類和結構可以在它上面註冊事件處理程式
  • 不能使用物件建立表示式(new 表示式)來建立物件
class Incrementer
{
           關鍵字   委託型別      事件名
             ↓        ↓           ↓
    public event EventHandler CountedADozen;
    ...
}

可以通過使用逗號分隔同時宣告多個事件。

public event EventHandler MyEvent1,MyEvent2,OtherEvent;

還可以使用static關鍵字讓事件變成靜態

public static event EventHandler CountedADozen;

事件是成員 
一個常見誤解是把事件認為是型別。和方法、屬性一樣,事件是類或結構的成員,這一點引出幾個重要特性。

  • 由於事件是成員:
    • 我們不能在一段可執行程式碼中宣告事件
    • 它必須宣告在類或結構中
  • 事件成員被隱式自動初始化為null

事件宣告需要委託型別的名字,我們可以宣告一個委託型別或使用已存在的。如果我們宣告一個委託型別,它必須指定事件儲存的方法的簽名和返回型別。 
BCL(Base Class Library,基類庫)聲明瞭一個叫做EventHandler的委託,專門用於系統事件。

訂閱事件


  • 使用+=運算子來為事件增加事件處理程式
  • 事件處理程式的規範可以是以下任意一種
    • 例項方法的名稱
    • 靜態方法的名稱
    • 匿名方法
    • Lambda表示式

例:為CountedADozen事件增加3個方法。

incrementer.CountedADozen+=IncrementDozensCount;//例項方法
incrementer.CountedADozen+=ClassB.CounterHandlerB;//靜態方法
mc.CountedADozen+=new EventHandler(cc.CounterHandlerC);//委託形式

例:Lambda表示式和匿名方法

incrementer.CountedADozen+=()=>DozensCount++;//Lambda表示式
incrementer.CountedADozen+=delegate{DozensCount++;};//匿名方法

觸發事件


事件成員本身只儲存了需要被呼叫的事件處理程式。如果事件沒觸發,什麼都不會發生。我們需要確保在合適的時候有程式碼來做這件事情。 
例:下面程式碼觸發了CountedADozen事件。

  • 在觸發事件前和null比較,從而檢視是否包含事件處理程式,如果事件是null,則表示沒有,不能執行
  • 觸發事件的語法和呼叫方法一樣
    • 使用事件名稱,後面跟引數列表
    • 引數列表需與事件委託型別相匹配
if(CountedADozen!=null)
{
    CountedADozen(source,args);
}

把事件宣告和觸發事件的程式碼放在一起便有了如下的釋出者類宣告。 
下面展示了整個程式,程式碼需要注意的地方如下:

  • 在建構函式中,Dozens類訂閱事件,將IncrementDozensCount作為事件處理程式
  • 在Incrementer類的DoCount方法中,每增長12個數就觸發CountedADozen事件
delegate void Handler();    //宣告委託
//釋出者
class Incrementer
{
    public event Handler CountedADozen;//建立事件併發布
    void DoCount(object source,EventArgs args)
    {
        for(int i=1;i<100;i++)
        {
            if((i%12==0)&&(CountedADozen!=null))
            {
                CountedADozen(source,args);
            }
        }
    }
}
//訂閱者
class Dozens
{
    public int DozensCount{get;private set;}
    public Dozens(Incrementer incrementer)
    {
        DozensCount=0;
        incrementer.CountedADozen+=IncrementDozensCount;//訂閱事件
    }
    void IncrementDozensCount()//宣告事件處理程式
    {
        DozensCount++;
    }
}
class Program
{
    static void Main()
    {
        var incrementer=new Incrementer();
        var dozensCounter=new Dozens(incrementer);
        incrementer.DoCount();
        Console.WriteLine("Number of dozens = {0}",dozensCounter.DozensCount);
    }
}

標準事件的用法


GUI程式設計是事件驅動的,即程式執行時,它可以在任何時候被事件打斷,比如滑鼠點選,按下按鍵或系統定時器。在這些情況發生時,程式需要處理事件然後繼續其他事情。 
程式事件的非同步處理是使用C#事件的絕佳場景。Windows GUI程式設計廣泛的使用事件,對於事件的使用,.NET框架提供了一個標準模式。事件使用的標準模式根本就是System名稱空間宣告的EventHandler委託型別。EventHandler委託型別的宣告程式碼如下。

  • 第一個引數用來儲存觸發事件的物件的引用。由於是object型別,所以可以匹配任何型別的例項
  • 第二個引數用來儲存狀態資訊,指明什麼型別適用於該程式
  • 返回型別是void
public delegate void EventHandler(object sender,EventArgs e);

EventHandler委託型別的第二個引數是EventArgs類的UI項,它宣告在System名稱空間中。既然第二個引數用於傳遞資料,你可能會誤認為EventArgs類的物件應該可以儲存一些型別的資料。

  • EventArgs設計為不能傳遞任何資料。它用於不需要傳遞資料的事件處理程式–通常會被忽略
  • 如果你希望傳遞資料,必須宣告一個派生自EventArgs的類,使用合適的欄位來儲存需要傳遞的資料

例:Incrementer+EventHandler

  • 在宣告中使用系統定義的EventHandler委託替換Handler
  • 訂閱者中宣告的事件處理程式簽名必須與事件委託(object、EventArgs引數)的簽名(和返回型別)匹配。對於IncrementDozensCount事件處理程式來說,該方法忽略了正式引數
  • 觸發事件的程式碼在呼叫事件時必須使用適當的引數型別的物件
public delegate void EventHandler(object sender,EventArgs e);
//釋出者
class Incrementer
{
    public event EventHandler CountedADozen;//建立事件併發布
    public void DoCount()
    {
        for(int i=1;i<100;i++)
        {
            if((i%12==0)&&(CountedADozen!=null))
            {
                CountedADozen(this,null);
            }
        }
    }
}
//訂閱者
class Dozens
{
    public int DozensCount{get;private set;}
    public Dozens(Incrementer incrementer)
    {
        DozensCount=0;
        incrementer.CountedADozen+=IncrementDozensCount;//訂閱事件
    }
    void IncrementDozensCount(object source,EventArgs e)//宣告事件處理程式
    {
        DozensCount++;
    }
}
class Program
{
    static void Main()
    {
        var incrementer=new Incrementer();
        var dozensCounter=new Dozens(incrementer);
        incrementer.DoCount();
        Console.WriteLine("Number of dozens = {0}",dozensCounter.DozensCount);
    }
}
通過擴充套件EventArgs來傳遞資料

為了向EventArgs傳入資料,並且符合標準慣例,我們需要宣告一個派生自EventArgs的自定義類,用於儲存需要傳入的資料。類的名稱應該以EventArgs結尾。 
例:宣告自定義的EventArgs類,它將字串儲存在IterationCount欄位中。

public class IncrementerEventArgs:EventArgs
{
    public int IterationCount{get;set;}
}

除了自定義類外,你還需要一個使用自定義類的委託型別。要獲取該類,可以使用泛型(第17章)版本的委託EventHandler<>。要使用泛型委託,需要做到以下兩點:

  • 將自定義類的名稱放在尖括號內
  • 在需要使用自定義委託型別的時候使用整個字串。

例:使用了自定義類和自定義委託的事件示例

public class IncrementerEventArgs:EventArgs
{
    public int IterationCount{get;set;}
}
//釋出者
class Incrementer
{
                       使用自定義類的泛型委託
                                ↓
    public event EventHandler<IncrementerEventArgs> CountedADozen;//建立事件併發布
    public void DoCount()
    {
        IncrementerEventArgs args=new IncrementerEventArgs();
        for(int i=1;i<100;i++)
        {
            if((i%12==0)&&(CountedADozen!=null))
            {
                args.IterationCount=i;
                CountedADozen(this,args);
            }
        }
    }
}
//訂閱者
class Dozens
{
    public int DozensCount{get;private set;}
    public Dozens(Incrementer incrementer)
    {
        DozensCount=0;
        incrementer.CountedADozen+=IncrementDozensCount;//訂閱事件
    }
    void IncrementDozensCount(object source,IncrementerEventArgs e)//宣告事件處理程式
    {
        Console.WriteLine("Incremented at iteration: {0} in {1}",e.IterationCount,source.ToString());
        DozensCount++;
    }
}
class Program
{
    static void Main()
    {
        var incrementer=new Incrementer();
        var dozensCounter=new Dozens(incrementer);
        incrementer.DoCount();
        Console.WriteLine("Number of dozens = {0}",dozensCounter.DozensCount);
    }
}

移除事件處理程式

用完事件處理程式後,可以使用-=運算子把事件處理程式從事件中移除。

例:移除事件處理程式示例

class Publisher
{
    public event EventHandler SimpleEvent;
    public void RaiseTheEvent(){SimpleEvent(this,null);}
}
class Subscriber
{
    public void MethodA(object o,EventArgs e)
    {
        Console.WriteLine("AAA");
    }
    public void MethodB(object o,EventArgs e)
    {
        Console.WriteLine("BBB");
    }
}
class Program
{
    static void Main()
    {
        var p=new Publisher();
        var s=new Subscriber();
        p.SimpleEvent+=s.MethodA;
        p.SimpleEvent+=s.MethodB;
        p.RaiseTheEvent();
        Console.WriteLine("\r\nRemove MethodB");
        p.SimpleEvent-=s.MethodB;
        p.RaiseTheEvent();
    }
}

如果一個處理程式向事件註冊了多次,那麼移除程式時,將只移除列表中該處理程式的最後一個例項。

事件訪問器


之前我提到+=和-=運算子是事件允許的唯一運算子。看到這裡我們應該知道,這些運算子有預定義行為。 
我們可以修改這些運算子的行為,並且使用它們時可以讓事件執行任何我們希望的自定義程式碼。但這是高階主題,此處只做簡單介紹。 
要改變這兩個運算子的操作,可以為事件定義事件訪問器。

  • 有兩個訪問器:add和remove
  • 宣告事件的訪問器看上去和宣告一個屬性差不多

例:具有訪問器的事件宣告。兩個訪問器都有隱式值引數value,它接受例項或靜態方法的引用。

public event EventHandler CountedADozen
{
    add
    {
        ...    //執行+=運算子的程式碼
    }
    remove
    {
        ...    //執行-=運算子的程式碼
    }
}

宣告事件訪問器後,事件不包含任何內嵌委託物件。我們必須實現自己的機制來儲存和移除事件註冊方法。 
事件訪問器表現為void方法,即沒有返回值。

from: http://www.cnblogs.com/moonache/p/6340222.html