2012年3月14日 星期三

事件 (Event)

事件說明

什麼是事件

事件是物件的一種機制,當物件在某種條件狀況下,該事件就會被引發。若客戶端程式有訂閱這個事件,該物件就會送出事件引發訊息。
傳送事件的類別稱為發行者(Publisher)-> Raise Event
接收事件的類別稱為訂閱者(Subscriber)-> Handle Event

事件是物件的一種機制,比較學術的說法則是,事件是一種特殊的委派方法。通常可由二個方面來看事件:發行者和訂閱者。

  • 發行者(Publisher):引發事件的類別 (Raise Event Class)
  • 訂閱者(Subscriber):接收事件的類別 (Handle Event Class)

委派與事件的關係

當設計一個事件時,我們並不知道訂閱者會用哪一段程式碼來處理事件。 所以在設計事件時,就必須透過委派的協助,利用委派幫我們指向事件要處理的方法(函式)。 所以:

  • 發行者在宣告事件的時候,其回傳型別就必須是一個委派型別。
  • 訂閱者在訂閱事件的時候,其處理函式就必須與該委派具有相同的簽名(Signature:也就是要有相同的回傳值參數列)。

事件的宣告

[存取修飾詞] delegate 回傳型別 委派名稱 ([參數列]);
[存取修飾詞] event   委派名稱 事件名稱;
//宣告一個委派
public delegate void SampleEventHandler(object sender, SampleEventArgs e); 

//宣告一個事件
public event SampleEventHandler SampleEvent;        //指明以 SampleEventHandler 這個委派型別為基礎

觸發事件

觸發事件是發行者要做的事,要注意的是,當程式碼要引發事件的時候,必須先判斷該事件值是否為 null ,因為訂閱者不見得有訂閱這個事件,所以必須用 null 判斷。

if (SampleEventHandler != null)
    SampleEventHandler (this, e);    

訂閱事件

訂閱事件是訂閱者要做的事,主要可分成二個部份:

Step1:撰寫事件處理函式:這個函式的簽章 (Signature) 必須與事件的委派簽章相符 (相同的回傳值,相同的參數列)。

void HandleSampleEvent(object sender, CustomEventArgs a)
{
    // Do something useful here.
}

Step2:訂閱事件:在適當的地方,撰寫訂閱事件程式碼。

// 1) 訂閱事件
// 使用加法指派運算子 (+=) 將事件處理常式附加到事件上
publisher.SampleEvent += HandleSampleEvent;

// 2) 取消訂閱事件
// 使用減法指派運算子 (-=) 移除附加在事件上的事件處理常式
publisher.SampleEvent -= HandleSampleEvent;

實例演練一

下面範例我們定義了一個類別,這個類別有一個事件 MyWarningEvent ,一個屬性值 Count 。 透過 Increment 和 Decrement 方法可以增減 Count 值。 當 Count 值大於 10 或小於 0 時,則引發 MyWarningEvent 事件。

傳送事件的類別(Publisher)

public class MyEventArgs : EventArgs
{
    public int Count = 0;
    public MyEventArgs(int _count)
    {
        Count = _count;
    }
}

public class MyClass
{
    private int Count = 0;

    public delegate void MyEventHandle(object sender, MyEventArgs e);
    public event MyEventHandle MyWarningEvent;

    //====================================================================
    // 將事件引發包裹在一個 protected virtual method 中
    // 這樣子它的衍生類別才能覆寫這個方法中的行為
    // 否則像下方的 Decrement 方法中,直接在該方法中將事件引發即可
    //====================================================================
    protected virtual void RaiseMyEvent(int _count)
    {
        if (MyWarningEvent != null)
        {
            MyWarningEvent.Invoke(this, new MyEventArgs(Count));
        }
    }

    public void Increment()
    {
        Count++;
        if ((Count > 10) || (Count < 0))         //引發事件
        {
            RaiseMyEvent(Count);
        }
    }

    public void Decrement()
    {
        Count--;

        if ((Count > 10) || (Count < 0))         //引發事件
        {
            if (MyWarningEvent != null)
            {
                MyWarningEvent.Invoke(this, new MyEventArgs(Count));
            }
        }
    }
}

接收事件的類別(Subscriber)

MyClass obj = new MyClass();

//事件處理方法
private void On_MyEvent(object sender, MyEventArgs e)
{
    Console.WriteLine(e.Count);
}

//訂閱事件
private void button1_Click(object sender, EventArgs e)
{
    obj.MyWarningEvent += On_MyEvent;
}

//取消訂閱事件
private void button2_Click(object sender, EventArgs e)
{
    obj.MyWarningEvent -= On_MyEvent;
}

private void btnIncrement_Click(object sender, EventArgs e)
{
    obj.Increment();
}

private void btnDecrement_Click(object sender, EventArgs e)
{
    obj.Decrement();
}

實例演練二

下面範例我們設計了一個使用者自訂控制項,它具有二個事件:小偷入侵、發生火災。同時我們用二個按鈕,分別來摸擬當這二個事件的發生。

傳送事件的類別(Publisher)

public delegate void Alarm_Handler();               //宣告事件的 Delegate 型別

public partial class ucAlarm : UserControl
{
    public event Alarm_Handler ThiefBreakInto;      //宣告事件 ThiefBreakInto 小偷入侵    
    public event Alarm_Handler Fire;                //宣告事件 FireOn 火燒

    public ucAlarm()
    {
        InitializeComponent();

        btnThief.Click += new System.EventHandler(this.On_ThiefBreakInto);  //摸擬發生小偷入侵事件        
        btnFire.Click += new System.EventHandler(this.On_Fire);             //摸擬發生火災事件
    }

    private void On_Fire(object sender, EventArgs e)
    {
        if (Fire != null)                           //當事件發生時,觸發"火災"事件            
            Fire.Invoke();
    }

    private void On_ThiefBreakInto(object sender, EventArgs e)
    {
        if (ThiefBreakInto != null)                 //當事件發生時,觸發"小偷入侵"事件            
            ThiefBreakInto.Invoke();
    }
}

接收事件的類別(Subscriber)

訂閱者使用加法指派運算子 (+=) 訂閱事件,一個事件是可以同時委派多個處理方法的。

public partial class Form2 : Form
{
    public Form2()
    {
        InitializeComponent();

        //1.1) 訂閱ThiefBreakInto事件 
        alarm1.ThiefBreakInto += alarm1_ThiefBreakInto; //使用加法指派運算子 (+=) 將事件處理常式附加到事件上

        //1.2) 訂閱Fire事件                                                     
        alarm1.Fire += alarm1_Fire;                     

        // 再訂閱Fire事件,執行 Call_Boss (一個事件是可以委派多個方法的)
        alarm1.Fire += Call_Boss;
    }

    //定義事件處理常式方法,其簽章 (Signature) 與事件的委派簽章相符。 
    private void alarm1_ThiefBreakInto(DateTime dt)
    {
        Console.WriteLine("Call 110 : " + System.DateTime.Now.ToString());
    }

    private void alarm1_Fire(DateTime dt)
    {
        Console.WriteLine("Call 119 : " + System.DateTime.Now.ToString());
    }

    private void Call_Boss(DateTime dt)
    {
        Console.WriteLine("Call Boss : " + System.DateTime.Now.ToString());
    }
}
//結果顯示
//Call 110 : 2012/8/8 上午 09:51:12
//Call 119 : 2012/8/8 上午 09:51:15
//Call Boss : 2012/8/8 上午 09:51:15

上例補充說明:

我們在上面例子中,針對 ThiefBreakInto 這個事件,總共加入了2個處理方法。
當委派的方法被 invoke 時,會依方法加入的先後順序執行,如上例的結果顯示。
如果被呼叫的方法有回傳值的話,那麼上面的做法就只能取到最後一個方法的回傳值。
若需求狀況有必要取得所有方法的回傳值的話,就必須利用 GetInvocationList 方法,取得委派的引動清單後,再分開 invoke 每一個方法,這樣就可以取得每一次的回傳值。

//利用 GetInvocationList ,分開invoke
private void On_ThiefBreakInto(object sender, EventArgs e)
{
    if (ThiefBreakInto != null)
    {
        foreach (Alarm_Handler handler in ThiefBreakInto.GetInvocationList())
        {
            handler();
        }
    }
}

沒有留言:

張貼留言