VBでイベント機能を利用するには、イベントを発生させるクラスでEventステートメントによりイベントを宣言し、RaiseEventステートメントによりイベントを発生させ、イベントを受信するクラスではWithEventsで変数を宣言することなどにより簡単に可能となる。これと同様のことをC#で行うにはどのようにすればよいのだろうか。
これがVBほど簡単にはいかない。ヘルプなどを調べると「デリゲート」やら「イベントハンドラ」などの「?」な言葉が並ぶ。ここではこれらの理屈は抜きにしてこれらを実現させる簡単な方法を考えてみる。
まず次のような超単純なクラス「SleepClass」を作ってみる。ここではまだイベントを使用していない。とりあえずStartメソッドを実行すると5秒間停止するだけである。
public class SleepClass { public void Start() { System.Threading.Thread.Sleep(5000); } }
次に5秒停止後イベントが発生するようにしてみる。このときイベントが発生するだけで何もデータを返さないものとする。
public class SleepClass { // データを持たないイベントデリゲートの宣言 //ここでは"Time"というイベントデリゲートを宣言する public event EventHandler Time; public void Start() { System.Threading.Thread.Sleep(5000); if (Time != null) { //"Time"イベントの発生 Time(this, EventArgs.Empty); } } }
イベントを受け取る側では次のように宣言する。ここではbutton1_ClickイベントによりSleepClassクラスのStartメソッドを呼び出し、イベント「Time」が発生するとSleepClass_Timeが呼び出され実行される。
private void button1_Click(object sender, System.EventArgs e) { SleepClass clsSleep = new SleepClass(); //イベントハンドラの追加 clsSleep.Time += new EventHandler(this.SleepClass_Time); clsSleep.Start(); } private void SleepClass_Time(object sender, System.EventArgs e) { //イベントが発生したとき MessageBox.Show("OK!"); }
ここでEventHandlerやEventArgsとは一体なんだろうという疑問が湧くだろうが、ここではあえてこれらの説明を省き、「こういうもの」としておこう。
これでほとんどの場合用が済んでしまうような気もするが、ヘルプにあるようにSleepClassを少し改良してみる。
public class SleepClass { // データを持たないイベントデリゲートの宣言 public event EventHandler Time; protected virtual void OnTime(EventArgs e) { if (Time != null) { Time(this, e); } } public void Start() { System.Threading.Thread.Sleep(5000); OnTime(EventArgs.Empty); } }
ここで変更された点といえばもちろん「protected virtual void OnTime(EventArgs e)」が追加されたことである。このSleepClassを継承して新たなクラスを作ることを考えればこうするべきということらしい。
説明は全くしなかったが、ここでの肝はすでに用意されているEventHandlerデリゲートと、EventArgsクラスを使ったことである。そのためにデリゲートの宣言と、返すデータのクラスを宣言する必要がなくなり、かなり簡略化ができた。しかしその分上記のような決まりきったやり方でしか使えないのが欠点といえる。
さて次はデータを返すイベントを作成する方法を考える。実は上記の例で使用した「EventHandlerデリゲート」と「EventArgsクラス」はイベントがデータを持たないから使えたわけで、そうでなければこれらに代わるものを自分で新しく用意する必要がある。
返されるデータは上の例の「EventArgs」に代わる(から継承される)クラスになるため、結局その部分だけを書き換えることになる。下にその例を示す。
//Timeイベントで返されるデータ //ここではstring型のひとつのデータのみ返すものとする public class TimeEventArgs : EventArgs { public string Message; } public class SleepClass { //デリゲートの宣言 //TimeEventArgs型のオブジェクトを返すようにする public delegate void TimeEventHandler(object sender, TimeEventArgs e); //イベントデリゲートの宣言 public event TimeEventHandler Time; protected virtual void OnTime(TimeEventArgs e) { if (Time != null) { Time(this, e); } } public void Start() { System.Threading.Thread.Sleep(5000); //返すデータの設定 TimeEventArgs e = new TimeEventArgs(); e.Message = "終わったよ。"; //イベントの発生 OnTime(e); } }
次は呼び出す側。
private void button1_Click(object sender, System.EventArgs e) { SleepClass clsSleep = new SleepClass(); clsSleep.Time += new SleepClass.TimeEventHandler(this.SleepClass_Time); clsSleep.Start(); } private void SleepClass_Time(object sender, TimeEventArgs e) { //返されたデータを取得し表示 MessageBox.Show(e.Message); }
このように、データを返さない単純なケースと比較して、返すデータを入れるTimeEventArgsクラスを用意する手間が余計に必要になる。ここで一番大事なのは、イベントが発生したときに実行するメソッド(ここでは、SleepClass_Timeメソッド)の2番目の引数の型が変更されることにより、以前使用していた「EventHandler」は使えなくなり、新しくデリゲートを宣言しなければいけないという点だ。後は問題ないだろう。
いままではEventArgsの派生クラスを用いてデータを返していたが、必ずしもそうする必要はない。次の例ではEventArgsの派生クラスを使わずに文字列と整数を返しているが、やっていることは今までとまったく同じである。ただし、前に紹介したようなEventArgsの派生クラスを使った方法が.NETでは推奨された方法なので、このようなやり方は使うべきではない。
public class SleepClass2 { //デリゲートの宣言 public delegate void TimeEventHandler(string message, int number); //イベントデリゲートの宣言 public event TimeEventHandler Time; protected virtual void OnTime(string message, int number) { if (Time != null) { Time(message, number); } } public void Start() { System.Threading.Thread.Sleep(5000); //イベントの発生 OnTime("終わったっす。", 5000); } }
使用する側では次のようになる。
private void button1_Click(object sender, System.EventArgs e) { SleepClass2 clsSleep = new SleepClass2(); clsSleep.Time += new SleepClass2.TimeEventHandler(this.SleepClass2_Time); clsSleep.Start(); } private void SleepClass2_Time(string message, int number) { MessageBox.Show(message + ":" + number.ToString()); }