GUIスレッド以外で動くコンポーネント側からコントロール操作したい!

ポッキーの日でありながらバイナリデーですね!! (前フリ)

今回はふつーのスレッドでウゴウゴするコンポーネントから、コントロールを操作したいよー!
ってあれです。

GUIスレッド?

ウィジェット、WinForms でいうコントロールは GUI スレッドというスレッドの中で生まれ、GUI スレッドの中で死んでいきます。
しかし、それ以外のコンポーネントは別のスレッドに跨ったりもできるわけです。

例えば、何らかの情報を受信し窓に通知するコンポーネント HogeReceiver を作るとします。
MonoDevelop を使って途中まで書いてみました:

/// <summary>
/// Hoge receiver.
/// </summary>
public class HogeReceiver : Component {
    /// <summary>
    /// Initializes a new instance of the <see cref="Demo.Components.HogeReceiver"/> class.
    /// </summary>
    public HogeReceiver() {
    }


    /// <summary>
    /// Occurs when receive.
    /// </summary>
    public event EventHandler<ReceiveEventArgs> Receive;


    /// <summary>
    /// Start this instance.
    /// </summary>
    public bool Start() {
    }


    /// <summary>
    /// Stop this instance.
    /// </summary>
    public bool Stop() {
    }
}

使い方

HogeReceiver をフォームに貼り付けて、Start() で受信用スレッドを開始します:

    private void Form1_Load(object sender, EventArgs e) {
        hogeReceiver1.Start();
    }

Stop() で受信用スレッドを停止します:

    private void Form1_FormClosed(object sender, FormClosedEventArgs e) {
        hogeReceiver1.Stop();
    }

受信したデータを使ってうにゃうにゃする場合はこんな風にします:

    private void hogeReceiver1_Receive(object sender, ReceiveEventArgs e) {
        listBox1.Items.Add( string.Format( "{0} に {1} が {2} しました。", Date.Now.ToString(), e.Value1, e.Value2 ) );
    }

問題

しかし、ここで問題があります。
listBox1.Items.Add( ... ); という処理は hogeReceiver1 の受信用スレッドで実行されるため、hogeReceiver1_Receive で例外が送出されます。
何の例外だったか忘れましたが、GUI スレッドでコントロール使えよ!ヽ(`Д´)ノというものだった気がします。

    private void hogeReceiver1_Receive(object sender, ReceiveEventArgs e) {
        listBox1.Invoke( delegate (object sender, ) { listBox1.Items.Add( string.Format( "{0} に {1} が {2} しました。", Date.Now.ToString(), e.Value1, e.Value2 ) ); } );
    }

ということで、こんなめんどくさいことをしないといけません:

private void hogeReceiver1_Receive(object sender, ReceiveEventArgs e) {
    listBox1.Invoke( delegate (object sender, ReceiveEventArgs e) {
            listBox1.Items.Add( string.Format( "{0} に {1} が {2} しました。", Date.Now.ToString(), e.Value1, e.Value2 ) );
        }, listBox1, e );
}

Receive イベントに設定したイベントハンドラHogeReceiver 側でなおかつ Invoke() したい!
というのが今回のお題です。

じゃあさ、コントロールはどーやって持ってくるの?

いい質問ですね。
HogeReceiver 側で Invoke() するには、イベントハンドラを所有しているコントロールが分からないことには二進も三進もいきませんよね。
ですが、その心配は無用です。
EventHandler は Delegate なので、所有しているオブジェクトを取得できるプロパティ、Target というのがあるのです。
これを遣いましょう。

追加されたイベントハンドラのコレクションはどこから取得するの?

ええ、これが一番難しかったです。
System.Reflection.EventInfo というのがあったんですが、これは全く違いました。
EventInfo じゃイベントハンドラのコレクションを取得することはできません。
じゃあ、どーすればいいんでしょう?

イベントに追加したイベントハンドラのリストが取得できないって?
ジョジョ、逆に考えるんだ。
イベントハンドラが追加された時にコレクションに追加すればいいんだと考えるんだ。

ありがとう、ジョースター卿。 イベントにはプロパティみたく addremove というのを書くところがあります。
これはつまり、イベントハンドラが追加された時と追加されたイベントハンドラを削除する時に呼ばれる処理を書くことができるということです。

ヽ(゚∀゚)ノ ワー

System.ComponentModel 名前空間System.ComponentModel.EventHandlerList というのがあるのでそれをフィールドとして持った後は:

        /// <summary>
        /// Occurs when receive.
        /// </summary>
        public event EventHandler<ReceiveEventArgs> Receive {
            add {
                lock ( this.eventLock_ ) {
                    this.eventHandlerList_.AddHandler( ReceiveEventKey, value );
                }
            }
            remove {
                lock ( this.eventLock_ ) {
                    this.eventHandlerList_.RemoveHandler( ReceiveEventKey, value );
                }
            }
        }

こんな感じで value を追加したり削除したりしましょう。
フィールドはこんな感じになってます:

        /// <summary>
        /// The event lock.
        /// </summary>
        private object eventLock_ = new object();
        /// <summary>
        /// The event handler list.
        /// </summary>
        private EventHandlerList eventHandlerList_ = new EventHandlerList();
        /// <summary>
        /// The receive event key.
        /// </summary>
        private static readonly object ReceiveEventKey = new object();

イベントハンドラの実行だと思われる

後は OnReceive() の中でこんな風に呼べばいいんじゃないかな…。

        private void OnReceive(ReceiveEventArgs e) {
            EventHandler<ReceiveEventArgs> handler = (EventHandler<ReceiveEventArgs>)this.eventHandlerList_.Item( ReceiveEventKey );

            if ( handler.Target is Control ) {
                Control target = handler.Target as Control;

                if ( target != null || target.InvokeRequired ) {
                    target.Invoke( handler, target, e );
                }
            } else {
                handler( handler.Target, e );
            }
        }

あー、疲れた。
そうそう、VB で書いてたのを無理やり C# で書いてるから、いくらか間違ってるかもしれない。
そんときはさーせん。

参照