イベント (event)

イベントはデリゲートに基づくため、そちらも理解する必要があります。

イベントに関わるクラス

  • イベントを送信 (発生) するクラス … パブリッシャー (publisher)
  • イベントを受信 (処理) するクラス … サブスクライバー (subscriber)

EventArgsクラスは、イベントに関するデータを格納するクラスの基本クラスです。EventArgs クラス - EventArgs クラス (System) | Microsoft Learn

EventHandler デリゲート

[System.Runtime.InteropServices.ComVisible(true)]
[System.Serializable]
public delegate void EventHandler(
    object sender, // イベントの元 (source)
    EventArgs e    // イベント データを含まないオブジェクト
    );
EventHandler Delegate (System) | Microsoft Learn

CancelEventHandler

public delegate void CancelEventHandler(
    object sender,
    CancelEventArgs e
)
CancelEventHandler デリゲート (System.ComponentModel) | MSDN

CancelEventArgs

CancelEventHandler 代理人 (System.ComponentModel) | Microsoft Learn

イベントの登録 (subscribe/購読)

イベントに加算代入演算子でハンドラのオブジェクトを代入することで、イベントにサブスクライバー (subscriber) を登録できます。

button.Click += new EventHandler( button_Click );

このときコールバックメソッドは、

void button_Click( object sender, EventArgs e )
{
    ...
}

のように定義します。ところでC# 2.0以降ならば、new EventHandler()を省略して次のようにも記述できます。

button.Click += button_Click;

登録するのが他のクラスのメソッドであるとき、そのクラスのインスタンスがnullだと「インスタンス メソッドへのデリゲートに null の 'this' を指定することはできません。」としてArgumentExceptionが投げられます。

MyClass myClass = null;
button.Click += myClass.Method; // ArgumentException

複数のイベントへの登録

同一のイベントハンドラをほかにも使用するならば、

EventHandler clickEventHandler = new EventHandler( button_Click );

button1.Click += clickEventHandler;
button2.Click += clickEventHandler;

のようにも記述できます。

同一のイベントへの同一のハンドラの登録

同一のイベントに同一のハンドラを登録すると、1回のイベントで複数回ハンドラが呼び出されます。

EventHandler clickEventHandler = new EventHandler( button_Click );

button1.Click += clickEventHandler;
button1.Click += clickEventHandler;

登録されているハンドラは、リフレクションを用いて確認できます。c# - How to obtain the invocation list of any event - Stack Overflow

Control control = button1;
string eventName = "Click";

PropertyInfo property = typeof(Component).GetProperty("Events", BindingFlags.NonPublic | BindingFlags.Instance);
EventHandlerList handlerList = (EventHandlerList)property.GetValue(control);

FieldInfo field = typeof(Control).GetField("Event" + eventName, BindingFlags.NonPublic | BindingFlags.Static);
object key = field.GetValue(control);
EventHandler handler = (EventHandler)handlerList[key];

Delegate[] invocationList = handler.GetInvocationList();

コールバックメソッドの省略

匿名メソッドを使用することで、コールバックメソッドを省略できます。

button.Click += delegate( object sender, EventArgs e ) { ... };

引数を使用しないならば、それを省略することもできます。

button.Click += delegate { ... };

C# 3.0以降ならばラムダ式を用いることで、delegateキーワードを省略できます。この方法では型名も省略できますが、引数は省略できません。

button.Click += (sender, e) => { ... };

イベントの解除 (unsubscribe/登録解除)

登録時とは逆で、ハンドラのオブジェクトを減算します。

button.Click -= new EventHandler( button_Click );

匿名メソッドではメソッドの名前が存在しないため、生成したハンドラ オブジェクトを保持しておき、それを用いて解除します。

EventHandler handler = null;
handler = delegate
{
    button1.Click -= handler;
    ...
};
button1.Click += handler;

独自のイベント定義

イベントに独自のデータを持たせる必要があるときには、System.EventArgsを継承してクラスを作成します。EventArgs クラス (System) | MSDN

class SampleEventArgs : System.EventArgs
{
    int data;

    public SampleEventArgs( int data)
    {
        this.data= data;
    }
}

イベント用のデリゲートの定義

イベントを処理するためのデリゲートを定義します。第2引数に、作成したイベントのクラスを持たせます。

delegate void SampleEventHandler( object source, SampleEventArgs e );

EventHandler<TEventArgs>デリゲートを用いれば、独自のデリゲートの定義は不要です。その場合にはdelegateの定義を記述せず、それを用いる側で次のようにイベント宣言します。

event EventHandler<SampleEventArgs> CustomEvent;

イベントの宣言 (declaring events)

先に定義したデリゲートにevent修飾子を適用することで、イベントを宣言できます。event (C# リファレンス) | MSDN

class MyClass
{
    event SampleEventHandler CustomEvent; // イベント宣言

    void Method()
    {
        if( CustomEvent != null )
        {
            // イベント クラスのインスタンスの生成
            SampleEventArgs args = new SampleEventArgs( 123 );

            // イベントの発生 (Raising an Event)
            CustomEvent( this, args );
        }
    }
}

イベントを発生するときは、そのイベントがnullではないことを確認します。イベントの実体は関数ポインタに近く、そこに呼び出し先が登録されていない状態で呼び出すとNullReferenceException例外が発生します。c# - NullReferenceException when triggering event - Stack Overflow

この「呼び出し先が登録されているならば呼び出す処理」は次のようにも記述でき、これを用いないとC# 6.0以降では「IDE1005 C# デリゲート呼び出しを簡略化できます。」と通知されます。

CustomEvent?.Invoke(this, new SampleEventArgs( 123 ));

これを使用する側は、次のようにすることでイベントを受け取れます。

class Handler
{
    MyClass myClass;

    Handler()
    {
        myClass = new MyClass();
        myClass.CustomEvent += new SampleEventHandler( Callback );
    }

    void Callback( object sender, EventArgs e )
    {
    }
}

イベントを発生できる場所

イベントは特殊なマルチキャスト デリゲートであり、それが宣言されたクラスまたは構造体の内部からしか発生できません。event (C# リファレンス) | MSDN

class MyClass
{
    public event EventHandler CustomEvent;
    public void Method()
    {
        if( CustomEvent != null )
        {
            this.CustomEvent(this, EventArgs.Empty); // OK
        }
    }
}

class Handler
{
    void Method()
    {
        MyClass myClass = new MyClass();
        myClass.CustomEvent(myClass, EventArgs.Empty); // CS0070 イベント 'MyClass.CustomEvent' は、+= または -= の左側にのみ表示されます

        myClass.CustomEvent += delegate (object sender, EventArgs e) { }; // OK
        myClass.Method(); // OK
    }
}

イベント発生時に渡すべきデータがないならば、ハンドラがnullであることを想定しなくても良いように、EventArgs.Emptyフィールドを渡します。 EventArgs.Empty フィールド (System) | MSDN EventArgs.Empty instead of null? - c# - Why use EventArgs.Empty instead of null? - Stack Overflow

このEventArgs.Emptyを渡すことは、new EventArgs()を渡すことと同義です。Empty - eventargs.cs

静的イベント (static event)

eventをstaticとすることで、静的なイベントとして宣言できます。

delegate void MyEvent();

class MyClass
{
    public static event MyEvent CustomEvent; // 静的イベント

    public static void Method() // 静的メソッド
    {
        if( CustomEvent != null )
        {
            CustomEvent(); // イベントの発生
        }
    }
}

イベントを処理する側では、クラス名で登録します。

MyClass.CustomEvent += delegate()
{
};

カスタム イベント アクセサ (custom event accessor)

カスタム イベント アクセサで、イベントへのアクセスを制御できます。たとえば、

public event SampleEventHandler CustomEvent;

のようなイベント宣言があったとき、これはコンパイラによって次のように展開されます。

private SampleEventHandler _CustomEvent;

public event SampleEventHandler CustomEvent
{
    add
    {
        _CustomEvent += value;
    }

    remove
    {
        _CustomEvent -= value;
    }
}

そこでこの定義を独自に実装することで、イベント処理をカスタマイズできます。

基本クラスのイベント

イベントが宣言されているクラスを継承したとき、派生クラスから基本クラスのイベントを発生することはできません。よってその必要があるときは、次のような方法で対処します。

Reactive Extensions (RX)

Microsoft Learnから検索