C#のイベントをCUIで発行してみる

前回の続きで、今回はCUIでイベントの購読と発行をやってみたいと思います。

今回はこの本の例題を参考にしています。

www.amazon.co.jp






その前に


イベントについてもうちょっと詳しく書きます。

イベントとデリゲートの違い

デリゲートはメソッドを追加できます。

イベントもそれは同じなんですが、

  • 内部からは、普通にデリゲートとして呼び出せる
  • 外部からは、add/removeのみ可

という制限があります。

イベント構文を使わないでイベントっぽいものを作る場合、Observerデザインパターンとかで書くことになります。


また、GUIのイベントはコールバックを利用してイベントをやりくりしています。

コールバック

マルチスレッドで、非同期処理の終了時に

「処理が終わったらこのメソッドを呼んでほしい」

というものをあらかじめ渡しておく。

このような、「他スレッドで呼び出してほしいメソッド」のことを、コールバックと呼ぶ。

なので、

GUIでは、基本的にフォームに対するアクション(ボタンクリック操作とか)はプログラムのループで監視されていて、

コールバックを通じて非同期なイベントを実現している。


裏で動いてるループは、ユーザの動きをずっと監視していて、あるイベントの条件を満たしたら、

currentEvent.Fire(); 

を実行するイメージ。あくまでイメージです。


そのため、GUIに限って言えば自分でイベントの登録とかを書くことはあまりありません。

コントロールの中身にイベントがもう作られていて、GUI側で監視の紐づけとかを勝手にやってくれます。

やってみよう

イベントの購読

GUIではボタンクリックなどがイベントとなっていましたが、

CUIではファイル操作とかWindows上で起こったことなどがイベントになったりしています。

今回はFileSystemWatcherクラスという、

「指定したディレクトリでファイルが生成されたり削除されたとき」にイベントを発行してくれるクラスを利用します。

このFileSystemWatcherが、イベント発行クラスとなります。


以下はC:\Tempにファイルが生成されたときにメッセージを表示するプログラムです。

    class Program 
    { 
        static void Main(string[] args) 
        { 
            FileSystemWatcher fsw = new FileSystemWatcher(@"C:\temp"); 
            fsw.Created += new FileSystemEventHandler(fsw_Created); 
            fsw.EnableRaisingEvents = true; 
            Console.ReadLine(); 
        } 

        private static void fsw_Created(object sender,FileSystemEventArgs e) 
        { 
            Console.WriteLine(e.FullPath + "が生成されました。"); 
        } 
    } 

購読者クラスが行うことはGUIと同じです。

イベントハンドラの定義と、イベントハンドラの登録。

GUIではコントロールのダブルクリックでClickイベントハンドラが自動で定義/登録されましたが、今回は手動で行いました。

ここではfsw_Createdメソッドが自作イベントハンドラになります。

別にこの名前でなくてもいいですが、基本的には"イベント発行クラス名_イベント名"で書かれることが多いと思います。


FileSystemWatcherはどのイベントハンドラが呼び出されるのかを知らないので、明示的に示してやる必要があります。

            fsw.Created += new FileSystemEventHandler(fsw_Created); 

これで、Createdイベントが発生したときはfsw_Createdメソッドを呼び出してね、と登録が完了します。

次に、FileSystemWatcherオブジェクトに、イベントの監視を依頼するのが、以下の一文です。

            fsw.EnableRaisingEvents = true; 


これで、あとはC:\Tempに何かファイルを作ったりするだけで、メッセージが表示されるようになります。

CUI1

イベントの発行

今度は、イベントを発行する側の定義方法について。

イベントを発行できるクラスには、いくつか必要な要素があります。

  • イベント情報を格納する"Custom EventArgs"クラスの定義
  • イベント発行クラス内で、イベントの宣言
  • イベントの発行メソッドの定義(通称OnEventNameメソッド
  • OnEventNameの呼び出しコード

では、実際に書いてみましょう。

今回は、テキストファイルを1行ごとに読み込んで、行の先頭に特定の文字列が存在していた場合にイベントを発行するクラスを作ります。

//イベント情報を格納できる、カスタムEventArgsを定義
public class TextFoundEventArgs : EventArgs{
    public TextFoundEventArgs(string line){
        Line = line;
    }
    public string Line{ get; private set; }

//イベント発行クラスの宣言
public class TextFinder
{
    public event EventHandler<TextFoundEventArgs> TextFound;

    public TextFinder(string filename)
    {
        _filename=filename;
    }

    private string _filename;
    
    //イベント発行メソッドの定義
    protected virtual void OnTextFound(TextFoundEventArgs e){
        if(TextFound != null){    
            TextFound(this,e);
        }
    }

    //OnEventNameの呼び出し    
    public void Execute(string target)
{
    using(StreamReader sr = new StreamReader(_filename)
    {
        string line;
        while((line = sr.ReadLine()) != null)
        {
            if(line .TrimStart().StartWith(target))
            {
                TextFoundEventArgs e = new TextFoundEventArgs(line);
                OnTextFound(e);
            }
        }
    }
}

以上で、イベントを発行できるクラスの定義が完了しました。

簡単に説明していきます。

まず、イベントハンドラに渡すためのカスタムEventHanderを作ります。

ここでは、TextFoundEventArgsという名前で作りました。

名前は何でもいいですが、イベントに関係があるような名前がいいでしょう。

このクラスはEventArgsを継承しています。

わざわざこのクラスを継承する理由ですが、TEventArgs型のジェネリックデリゲート(イベントハンドラ)で利用できるようにするためです。

次に発行者クラスを見ていきましょう。

まず、イベントハンドラが宣言されています。

public event  EventHandler<TextFoundEventArgs> TextFound;

これは、通常イベントハンドラは引数に(object sender, EventArgs e)を取りますが、

カスタムEventArgsを引数に取れるようにジェネリック化するようにしています。

具体的には、このTextFoundイベントハンドラは引数に(object sender,TextFoundEventArgs e)を取ります。

こうすることで、イベントハンドラに自分が渡したい独自の情報を追加することができます。

次にOnEventNameメソッドを宣言しています。

protected virtual void OnTextFound(TextFoundEventArgs e)
{
    if(TextFound != null)
    {
        TextFound(this,e);
    }
}

まずprotectedですが、このメソッドは外部から呼び出されてはいけません。(それがイベントであるための条件なので)

次にvirtualです。

これはただ単に、このクラスを継承したクラスもいちいちOnEventNameメソッドを新規追加しなくていいように、という配慮です。省略しても動きます。

そして引数はTextFoundEventArgs eです。

イベントが発生した時の情報は、イベントが発生した瞬間に生成されるべきものです。

そのため、このメソッド内では関与せずに、引数として与えておきます。

メソッドの中身はいたってシンプルで、

だけです。

イベントハンドラの引数は(this,e)となっています。

thisはこのTextFinderクラス、eは後から与えられます。

最後に、コードの実行です。

流れを簡単に追うと、

  • ファイルを読み込む
  • テキストを1行ずつ読み込んでいく
  • 行の先頭が特定の文字列だったら、その時の行をstringでTextFoundEventArgsに格納し、それをOnTextFoundメソッドに渡してあげる

という形になっています。

これでイベント発行者の定義が完了しました。


次は、イベントを購読するクラスについて書いていきます。

    static void Main(string[] args){
        TextFinder tf = new TextFinder(@"C:\Temp\text.txt");
        tf.TextFound += new EventHandler<TextFoundEventArgs>(tf_TextFound);
        tf.Execute("Public");
    }

    staic void tf_TextFound(object sender,TextFoundEventArgs e){
        Console.WriteLine(e.line);
    }

やっていることはこの記事の上の方で紹介していることと大体同じなので、詳細な説明は割愛しますが。

一応ポイントとしては、イベントハンドラメソッドを登録する際、ジェネリックデリゲートなイベントハンドラを生成して渡しているぐらいでしょうか。

このジェネリックデリゲート自体は自分でどこかで定義しなくてはいけないわけではなく、カスタムEventArgsのみ自分で用意しておけば使えます。

これでイベントはバッチリ!

CUIでは、やっていることは結構簡素なものでしたね。


次回はちょっと飛躍して、WPFのMVVMモデルでイベントがどんな風に使われているのかを理解しながら、実際にVMを作ってみようと思います。