事件(Events)的研究:如何在類別物件中撰寫事件

在ASP.NET或Windows Forms的程式設計中,我們很常用到事件,幾乎是視為已經習以為常的事。但是在我的程式設計經驗裡面,在自己設計的類別中幾乎不太用到事件的設計,總覺得這是一個很無聊的動作。今天剛好有機會要用到多重觸發的運行架構,因此特別研究了一下事件的撰寫方式,並記錄在此。一個C#事件的基本架構如下圖所示:

在上圖中,我們可以看到使用者控制項中,有宣告了一個委派(Delegates),以及一個事件(Events)指向委派,而當使用者控制項觸發了(Fire、Raise)事件時,訂閱這些事件的應用程式,就會跟著一起動起來了,並將行為指定給Event Handler運行。

一個利用事件來進行程式設計的簡單範例

在這邊我們先用C# Console唯一提供的CancelKeyPress Events來進行設計示範,這個CancelKeyPress唯一的作用,就是當使用者在Console中按下Ctrl+C時,就會觸發這個事件來進行中斷程序。我們要做的事情就是攔截這個事件的工作,並將ConsoleCancelEventArgs.Cancel設定成true,讓Console不至於中斷跳出。程式碼如下:

static void Main(string[] args)
{
	//在Console唯一一個事件中,掛上我們自己定義的函式
	Console.CancelKeyPress += myCancelKeyPress;

	while (true)
	{
		Console.WriteLine("按下X鈕才能離開。");
		ConsoleKeyInfo cki = Console.ReadKey(true);
		Console.WriteLine("你按了:{0}", cki.Key);
		if (cki.Key == ConsoleKey.X) break;
	}
}

private static void myCancelKeyPress(object sender, ConsoleCancelEventArgs args)
{
	Console.WriteLine("你按了:{0};Cancel屬性現在為:{1},將改回true,否則真的會被中斷掉。", args.SpecialKey, args.Cancel);
	args.Cancel = true;
}

其中在Console.CancelKeyPress += myCancelKeyPress;這一行程式碼,正規的寫法應該是要寫成下列式子,但是因為我們調用的EventArgs都是用官方的ConsoleCancelEventArgs所以等效。

Console.CancelKeyPress += new ConsoleCancelEventHandler(myCancelKeyPress);

當然,你可以將委派用黏巴達寫法來化簡,在不重複利用函式的情況為前提下,也就是說你可以把一整個函式終結掉,讓程式碼變得更精簡。

static void Main(string[] args)
{
	//在Console唯一一個事件中,掛上我們自己定義的函式
	Console.CancelKeyPress += (s, e) =>
	{
		Console.WriteLine("你按了:{0};Cancel屬性現在為:{1},將改回true,否則真的會被中斷掉。", e.SpecialKey, e.Cancel);
		e.Cancel = true;
	};

	while (true)
	{
		Console.WriteLine("按下X鈕才能離開。");
		ConsoleKeyInfo cki = Console.ReadKey(true);
		Console.WriteLine("你按了:{0}", cki.Key);
		if (cki.Key == ConsoleKey.X) break;
	}
}

然後,這個Console運行時期的圖片如下所示,我們可以發現Ctrl-C真的被Console.CancelKeyPress攔截到並觸發了:

在自建類別中,撰寫事件供給他人使用

如果一直都在用別人寫好的物件事件,而自己都不曾寫過給別人用,那麼這未免也太過分了吧!(笑)所以接下來的範例,我們將包裝一個叫做計算機的命名空間(namespace Calculator),然後我們會分別建立兩個類別、一個委派。

  1. AnswerArgs類別
  2. AnswerEventHandler委派
  3. Calc類別

AnswerEventHandler這個委派應該要獨立被放在namespace目錄下,不應該被放在與Calc類別之中。例如你在下System.Windows.Forms,也可以找到「MouseEventHandler Delegate」、「KeyEventHandler Delegate」等委派,來受到MouseMove或KeyDown等事件之委託。

namespace Calculator
{
	//事件參數包
	public class AnswerArgs : EventArgs
	{
		public int iAnswer;
	}
	//事件委派
	public delegate void AnswerEventHandler(object sender, AnswerArgs e);
	//主要類別
	public class Calc
	{
		//定義一個OnKnowAnswer事件,並將其委派給AnswerEventHandler
		public event AnswerEventHandler OnKnowAnswer;
		public string cName { set; get; }
		public int Add(int A, int B)
		{
			AnswerArgs e = new AnswerArgs() { iAnswer = A + B };
			if (OnKnowAnswer != null) { OnKnowAnswer(this, e); }
			return A + B;
		}
	}
}

在外部引用類別事件的方式

萬事俱備,接著讓我們在主程序中來引用剛才建立好的類別事件吧!

static void Main(string[] args)
{
	Calculator.Calc oTemp = new Calculator.Calc() { cName = "計算機一" };
	//利用黏巴達為事件掛上一個簡單的處理函式
	oTemp.OnKnowAnswer += (s, e) => { Console.WriteLine("簡單輸出: " + e.iAnswer); };
	oTemp.Add(1, 2);
	oTemp.Add(3, 4);
	Console.WriteLine("---");

	//事件化的優點a:同一時間可以掛上多個函式
	oTemp.OnKnowAnswer += myOnKnowAnswer;
	oTemp.Add(5, 6);
	oTemp.Add(7, 8);
	Console.WriteLine("---");

	//事件化的優點b:同類型的物件事件,多數時候是可以共用同一個函式的
	Calculator.Calc oTemp2 = new Calculator.Calc() { cName = "計算機二" };
	oTemp2.OnKnowAnswer += myOnKnowAnswer;
	oTemp2.Add(9, 10);
	Console.WriteLine("---");

	//事件化的優點c,除非你一開始就對Add方法下void,否則事件有可能只是個通知,你依然可以允許方法的執行結果可以有其他出口
	int iTemp = oTemp2.Add(11, 12);
	Console.WriteLine("Direct return: " + iTemp);

	Console.WriteLine("--- END ---");
	Console.Read();
}

private static void myOnKnowAnswer(object sender, Calculator.AnswerArgs e)
{
	Console.WriteLine(string.Format("The {0} answer is: {1}", ((Calculator.Calc)sender).cName, e.iAnswer));
}

輸出結果照片如下:

最後值得一提的是(在Console示範時也有提醒過),如果你願意的話,可以將程式碼寫得更正規:(純粹可以表示這是受哪一個在委派託管,意義不大。)

//oTemp.OnKnowAnswer += myOnKnowAnswer;
oTemp.OnKnowAnswer += new Calculator.AnswerEventHandler(myOnKnowAnswer);

註:如果沒有要帶入任何的EventArgs,也不想宣告delegate EventHandler,可以參考這一篇利用泛型委派的寫法:運用泛型委派進行基本的觀察者模式實作

C# Events UserDefinedClass SelfDefinedClass Delegates FireEvents RaiseEvents