EventHanlderの実装方法(C# WPF)

C#

マイクロソフトが薦めるEventHandlerの実装方法を見てみます。

イベントの処理と発生
デリゲート モデルに基づく .NET イベントを処理および発生させる方法について説明します。 このモデルを使用すると、サブスクライバーがプロバイダーに登録したり、プロバイダーから通知を受け取ったりすることができます。

とりあえず最小構成

public class User
{
    public event EventHandler NameChanged = (sender, e) => { };

    private string name;
    public string Name {
        get { return this.name;}
        set{
            this.name = value;
            this.NameChanged?.Invoke(this, EventArgs.Empty);
        }
    }
}

public class Hoge
{
    public Hoge()
    {
        User user = new User();
        user.NameChanged += this.Fuga;
    }
    private void Fuga(object sender, EventArgs e)
    {
        Console.WriteLine("Hellow World");
    }
}

EventHandlerは初期化しておきます。
しなくても構いませんが、しない場合はInvokeする時にNullチェックが必要になります。
イベントを発火したいタイミングでNameChangedをInvokeします。

そしてそのイベントをsubscribeする側を定義します。
userのEventHandlerに自前のメソッドを渡します。
user.NameChanged += this.Fuga;
この時のFuga()は引数と返り値が元のEventHandlerと一致しなければなりません。

Micorsoftが推す重要な点は
・DelegateではなくEventHandlerを使おう!
・EventHandlerに渡す情報がある場合はEventArgsの派生クラスを作ろう。
・情報が必要ない場合、EventArgsはnew EventArags()とせず、EventArgs.Emptyを使おう。
・EventArgsが使えないケースでのみDelegateを使う。
・Delegateを使う場合でもHogeEventHandlerという名称にする。

On***Changedが付いているバージョン

OnPropertyChangedなどは上記でみたケースと違ってInvoke用のメソッドが定義されていますよね。ここの部分を見ていきましょう。実はこれは派生クラスに対応するためにこういう書き方になっています。

public class User
{
    public event EventHandler NameChanged = (sender, e) => { };

    private protected string Name { get; set; }

    public void hoge()
    {
        //NameChangedでもOnNameChangedでも挙動は同じ
        this.NameChanged?.Invoke(this, EventArgs.Empty);
        this.OnNameChanged();
    }

    protected virtual void OnNameChanged()
    {
        NameChanged?.Invoke(this, EventArgs.Empty);
    }
}

public class extendedUser : User
{
    public string FirstName { get; set; }
    public void Nae()
    {
        //これは呼べる!
        this.OnNameChanged();
        //これは呼べない。
        this.NameChanged?.Invoke(this, EventArgs.Empty);
    }
}

EventHandlerの宣言のところが重要です。
public event EventHandler NameChanged
このeventキーワードがあることでこのEventHandlerはクラス内からしか実行できません。
一方でpublicであるので、delegate += hogehogeというようなsubscribe関連だけが外部に許可されています。

この制約のために実は継承クラスからeventを実行することができません。
extendedUserからeventを実行できない(this.NameChanged?.Invoke(this, EventArgs.Empty);)ため、これを解消するためにprotectedなメソッドであるOnNameChangedを作ります。
イベントの発火をOn***に任せることで、使い勝手を高めているのですね。

On***Changedはprotectedであるべきですが、virtualかどうかは任意です。
Microsoft的には推奨のようですが、overrideしなくても問題ない場合も多いと思います。

個人的な使い方

個人的にEventHandlerを使うという点は反対です。
Eventを拾う瞬間にしか利用しないEventArgsの派生クラスが発生するからです。

上記の例で言うと、Nameをイベント処理に使うのであれば以下のようなEventArgsの派生クラスが必要になります。

public class NewEventArgs : EventArgs
{
    NewEventArgs(string name)
    {
        this.Name = name;
    }

    public string Name {get;}
}

//sender側
NameChanged?.Invoke(this, new NewEventArgs(name))

//receiver側
private void Fuga(object sender, EventArgs e)
{
    Console.WriteLine(e.Name);
}

冗長だと思いませんか?
EventHandlerをやめてDelegateを使えば以下のように書けます。

public delegate void NameChangedEventHanlder(string name);

public class User
{
    // EventHandler版
    //public event EventHandler NameChanged = (sender, e) => { };

    // Delegate版
    public event NameChangedEventHanlder NameChanged = name => { };
}

//sender側
NameChanged?.Invoke(name)

//receiver側
private void Fuga(string name)
{
    Console.WriteLine(Name);
}

可読性が下がってもよければ既定のDelegateであるAction使ってさらに短くなります。

public event Action<string> NameChanged = name => { };

EventArgsに複数のプロパティを持たせたい時にDelegateだと困るじゃんって言われそうですが、
その場合はEventArgs継承しない、複数のプロパティを持たせたクラスを作ればいいのです。
手間としてはEventArgsの継承クラスを作るのと変わりません。

一方でEventArgsの派生クラスの場合、EventHandler以外にeventを渡すのは設計思想的におかしいため、EventHandler以降のメソッドにはプロパティ単位で分割して渡すことになります。引数が増えれば増えるほど、プロパティ単位で分割する作業が大変ですよね。

プロパティ単位で分割しなくていいように渡すプロパティを一括でもつクラスを作成したくなると思います。それこそが「複数のプロパティを持たせたクラス」であり、EventArgsの派生クラスと同一のプロパティを持つクラスになりかねません。二度手間であり、似たようなクラスを2つ作ることになります。

Eventをすぐさま消化できるようなケースはEventArgsの派生クラスでも良いと思いますが、Event以降にCallBackをいくつか遡る場合や、WCFでプロセス間通信を行う場合など、EventArgsに格納した所で特にメリットがない気がします。

コメント