読者です 読者をやめる 読者になる 読者になる

XORveR.com の日記

XORveR.com の公式ブログです。

もやもやしていたので、イベントのRaiseヘルパーをカバレッジ100%に・・・(以下略)

まえがき

イベントハンドラを Raise しようとした時、普通のスレッドからUIスレッドのリスナーを直接に呼び出すとコケます。
そこで使われる手法ですが

            var dispatcher = Application.Current.Dispatcher;
            if (dispatcher.CheckAccess()) {
                // 現在のスレッドから呼び出せる
                eventhandle(sender, args);
            } else {
                // 現在のスレッドから呼び出せないので、dispatcherから呼び出し
                dispatcher.Invoke(() => eventhandle(sender, args));
            }

よく見かける構文です。なるほど、よくできてますね。

少なくとも MsTest で動かさない限りはですが。

  1. MsTest で動かそうとすると、ぬるぽします。
  2. 別の手段では、Invoke 側がカバレッジされません。


え?イベントハンドリングなんてテストしない?
これをなんとかしようというのが、今回のお題です。
ええ、当然に単なる自己満足です!!!!!!!!

MsTest で動かすと、Application.Current は null を返します。

よってnull参照して落ちます。
とどのつまりは MsTest は、GUI なんてカンケーネー(かどうかは知りませんが)というスタンスなんですね。
ここで問題なのは、Application に頼るからです。

その代案としてよく上げられているのは…Application.Current が null のときには Dispatcher.CurrentDispatcher で代用 (ェー

    if (Application.Current==null) {
        dispatcher = Dispatcher.CurrentDispatcher;
    } else {
        dispatcher = Application.Current.Dispatcher;
    }
  • Dispatcher.CurrentDispatcher プロパティは、「現在実行中のスレッドの Dispatcher を取得します。」というものです。
  • そして CheckAccess() はDispatcherのソース - Microsoftを読むと、Dispatcherインスタンスを作成したスレッドが現在のスレッドと同じなのかを返しているだけです。
  • また Dispatcher.CurrentDispatcher は、現在のスレッドが作った Dispatcher インスタンスを必ず返します。


ですから、Dispatcher.CurrentDispatcher.CheckAccess() は必ず true を返します。
よって、MsTest でカバレッジしようとしても Invoke() 側には決して行きません。

View は諦めるとして、それ以外は 100% を目指したいものです。それが自己満足であってもw
そこで、考えます。

UIスレッドのインスタンスを非UIスレッドから参照すると、何故に落ちるのか?

別スレッドのインスタンスを参照できない?そんなことはありません。
ここで、先人が調査した結果を(自己責任です)丸呑みにします。
WPF4.5入門 その41 「DispatcherObject」
blog.okazuki.jp

DispatcherObject のヘルプを見ても「

このオブジェクトは、オブジェクトを作成したスレッドからのみアクセスできます。 他のスレッドからアクセスしようとすると、 InvalidOperationException がスローされます。 Invoke または BeginInvoke によって、正しいスレッドへのマーシャリング作業がサポートされます。

」とあることが傍証です。

DispatcherObject を判断して切り替える

そこで、リスナーが DispatcherObject だった場合には、その Dispatcher を使うことにします。

        /// <summary>
        /// MulticastDelegate の Invoke しようとしている先の Dispatcher を Application.Current ではなく推測する。
        /// </summary>
        /// <param name="multicast">event などのデリゲート</param>
        /// <returns>その実行先の Dispatcher</returns>
        public static Dispatcher GetDispatcher(this MulticastDelegate multicast) {
            if (multicast == null) {
                // Invoke する対象はないので、現在の実行スレッドの Dispatcher
                return Dispatcher.CurrentDispatcher;
            }
            Delegate[] delegates = multicast.GetInvocationList();
            if (delegates[0].Target is DispatcherObject) {
                // 先頭の Invoke する対象の Dispatcher を使用します。
                // マルチキャストのリスナーそれぞれが違うスレッドという状況までは考慮しないことにしておきます。
                // DispatcherObject は、オブジェクトを作成したスレッドからのみアクセスできます。
                return (delegates[0].Target as DispatcherObject).Dispatcher;
            } else {
                // 特に問題なく実行できるはず。
                return Dispatcher.CurrentDispatcher;
            }
        }

そして、Raise はこうなります。

        /// <summary>
        /// EventHandler 通知イベントを発生させます。
        /// eventhandler.Raise(sender);
        /// </summary>
        /// <param name="eventhandle">イベントハンドラ</param>
        /// <param name="sender">送信元</param>
        public static void Raise(this EventHandler eventhandle, object sender) {
            if (eventhandle == null) {
                // 購読しているデリゲートがない場合には何もしません
                return;
            }
            //var isNewDispatcher = null == Dispatcher.FromThread(Thread.CurrentThread);
            //var currentDispatcher = Dispatcher.CurrentDispatcher;
            Dispatcher dispatcher = eventhandle.GetDispatcher();
            // arg
            var args = EventArgs.Empty;
            if (dispatcher.CheckAccess()) {
                // 現在のスレッドから呼び出せる
                eventhandle(sender, args);
            } else {
                // 現在のスレッドから呼び出せないので、dispatcherから呼び出し
                dispatcher.InvokeAsync(() => eventhandle(sender, args));
            }
            //DispatcherUtil.DoEvents();
            //if (isNewDispatcher) {
            //  // 独自スレッドで動いていたので、作成した Dispatcher はシャットダウン
            //  currentDispatcher.BeginInvokeShutdown(DispatcherPriority.SystemIdle);
            //  Dispatcher.Run();
            //}
        }

実装では、PropertyChangedEventHandler, RoutedEventHandler 用の Raise メソッドもありますが割愛です。

余談ですが Dispatcher を作った時にはシャットダウンしようかと欲を出していましたが・・・
どういうわけか、全く関係のない新規のスレッドで時々 Dispatcher がシャットダウンされていると例外を吐くため断念。

なんで Invoke() ではなく InvokeAsync() ?

dispatcher.Invoke() だとテストがハングアップします。
Invokeの原理をよく理解しているわけではないのですが、こうではないかと推測します。
スレッドAからスレッドBのリスナーをInvokeした時にスレッドBでJoin待ちしているとデッドロックする。

そんなわけで、dispatcher.InvokeAsync() に変えました。
基本的にイベントなんて通知なのですから同期である必要は無いはずですから。(ないよね?)

そしてテストコード

テストコードは以下になります。
要点は、DispatcherObject を継承している点です。
これで、他のスレッドから自分のメソッドに対して Raise すれば、InvokeAsync() のルートに流れてくれます。

    [TestClass()]
    public class EventHandlerOperationExtensionTests : DispatcherObject, INotifyPropertyChanged {

        List<string> called = new List<string>();

        /// <summary>
        /// RoutedEventHandler の Dispatcher 動作確認クラス
        /// </summary>
        class EventTest {
            public event EventHandler handler;
            public static EventTest Instance { get; set; }
            public static void CreateInstance() {
                Instance = new EventTest();
            }
            public void Raise() {
                handler.Raise(this);
            }
        }

        [TestMethod()]
        public void RaiseTest_EventHandler() {
            using (var listeners = new ListenerManager()) {
                // EventTest のインスタンスを別のスレッドで作ります
                var thread = new Thread(EventTest.CreateInstance);
                thread.Start();
                thread.Join();
                var obj = EventTest.Instance;

                // 別スレッドで作られたインスタンスを使って、自スレッド-自スレッドを Raise
                // ただし、リスナーなし
                Assert.AreEqual(0, called.Count);
                obj.Raise();
                Assert.AreEqual(0, called.Count);

                // イベントハンドラを設定
                listeners.Register(
                    () => obj.handler += EventHandler_listener,
                    () => obj.handler -= EventHandler_listener
                );

                // 別スレッドで作られたインスタンスを使って、自スレッド-自スレッドを Raise
                Assert.AreEqual(0, called.Count);
                obj.Raise();
                Assert.AreEqual(1, called.Count);

                // 別スレッドで作られたインスタンスを使って、他スレッド-自スレッドを Raise
                var thread2 = new Thread(obj.Raise);
                thread2.Start();
                thread2.Join();

                // ナニコレ?
                DispatcherUtil.DoEvents();
                Assert.AreEqual(2, called.Count);
            }
        }

        private void EventHandler_listener(object sender, EventArgs e) {
            called.Add(e.ToString());
        }
    }

ナニコレ?

InvokeSync() に変更した弊害で、直前の Join() を抜けてきても、リスナーは動いていません。
なので、リスナーが Raise されたことを検査できないのです。
そこで
http://stackoverflow.com/questions/1106881/using-the-wpf-dispatcher-in-unit-testsstackoverflow.com
から DispatcherUtil.DoEvents() を拾ってきました。、

    /// <summary>
    /// <see cref="http://stackoverflow.com/questions/1106881/using-the-wpf-dispatcher-in-unit-tests"/>
    /// </summary>
    public static class DispatcherUtil {
        [SecurityPermissionAttribute(SecurityAction.Demand, Flags = SecurityPermissionFlag.UnmanagedCode)]
        public static void DoEvents() {
            DispatcherFrame frame = new DispatcherFrame();
            Dispatcher.CurrentDispatcher.BeginInvoke(DispatcherPriority.Background,
                new DispatcherOperationCallback(ExitFrame), frame);
            Dispatcher.PushFrame(frame);
        }

        private static object ExitFrame(object frame) {
            ((DispatcherFrame)frame).Continue = false;
            return null;
        }
    }

です。
要するにスレッドのシーケンスを中断して、リスナーを動かすヘルパーです。

カバレッジ

これを、テストに追加して OpenCover で流せば、無事に Raise(this EventHandler eventhandle, object sender) は全パス確認成功となります。

では!