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方法,即沒有返回值。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM