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

XORveR.com の日記

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

PCをクリーンインストールしている間に、カルチャが仕様変更になっていた話

記事の要旨

何故か

using System.Globalization;

namespace test {
    class Program {
        static void Main(string[] args) {
            // クリンゴン語は通る
            var tlhcul = CultureInfo.GetCultureInfo("tlh", "en-US");
            // 存在しないxxxは通る
            var xxxcul = CultureInfo.GetCultureInfo("xxx", "en-US");
            // test は CultureNotFoundExceptionでコケる
            var testcul = CultureInfo.GetCultureInfo("test", "en-US");
        }
    }
}

のコメントに書いてある挙動となるのだけれど、これは多分.Netのバグフィックスで変わったんじゃないかなという話。

具体的には4文字のカルチャ名は不許可!となった模様。
根拠としては "xxxx" は通らないけど、"xx-xx"は通る事から。
そもそもの ISO 3166-1 alpha-3 を調べてみると、3文字と規定されているようですので、4文字が通っていたことが間違い。

近況1

論文を書いてTechnology to limit the available number of chosen-plaintext [PeerJ Preprints]として投稿しました、が
「うちは主に生物学関係しかやっとりませんので、すんません」
という理由でリジェクトされましたorz

別の投稿先を物色して、何を血迷ったのか「オクスフォード大」のセキュリティ誌に投稿。(無茶しやがって…
すでに上記URLでオープン査読されちゃってるんで、投稿規定の面ですでにリジェクトされる可能性大。
まあ、どうせ広報用に作った論文で CC-BY だし駄目でもともと。

近況2

PCのCドライブが不調でSSDを交換しようとしたところブートできなくなりました。
しかたなくクリーンインストールして、あれやこれや復元。(面倒臭さの極みっっっっ!でもバックアップ万歳)

Linqのクエリ式を使ったプロパティ名の記述の提案

DotNet wpf

まえがき

blog.xorver.com

でも紹介している、クエリ式を使ったプロパティ名の記述は便利ですが、毎回拡張メソッドが呼ばれます。
わずかな問題だけど、気になります。

そこで、こうしたらどうだろう?という提案です。

ヘルパー

まず、以下のヘルパーを用意します。

    /// <summary>
    /// プロパティ名の調査用ヘルパー
    /// </summary>
    /// <typeparam name="TObj"></typeparam>
    public class Names<TObj> {
        /// <summary>
        /// 空オブジェクト
        /// </summary>
        public readonly TObj Null = default(TObj);
        /// <summary>
        /// プロパティ名
        /// </summary>
        /// <typeparam name="TProp">プロパティの型</typeparam>
        /// <param name="propertyNameExpression">プロパティ名を示すクエリ式</param>
        /// <returns>プロパティ名</returns>
        public string Name<TProp>(Expression<Func<TObj, TProp>> propertyNameExpression) {
            return Null.PropertyName<TObj, TProp>(propertyNameExpression);
        }
    }

実装部分

目的とするクラス内に Names という静的なサブクラスを書きます。
実例

    public class Parameter : INotifyPropertyChanged {
        public static class Names {
            private static Names<Parameter> names = new Names<Parameter>();
            public static readonly string Status = names.Name((o) => o.Status);
            public static readonly string Description = names.Name((o) => o.Description);
            public static readonly string Name = names.Name((o) => o.Name);
            public static readonly string ParameterEntryTime = names.Name((o) => o.ParameterEntryTime);
            public static readonly string ParameterUpdateTime = names.Name((o) => o.ParameterUpdateTime);
            public static readonly string SourceFullName = names.Name((o) => o.SourceFullName);
        }

静的メソッドの場合、上記「ひどい使い方」に従って、TObj は使いません。

    public class Texts {
        public static class Names {
            private static Names<Texts> names = new Names<Texts>();
            public static readonly string StaticCurrentSettingFolder = names.Name(o => CurrentSettingFolder);
            public static readonly string StaticCurrentCulture = names.Name(o => CurrentCulture);
        }

使用

「クラス.Names.プロパティ名」として記述します。

                ctrl.AddColumn(Strings.Column_Header_Description, new Binding(Parameter.Names.Description));

では!

using しているブロック内から yield return した先で処理キャンセルした時に using しているリソースはどうなる?

DotNet

まえがき

長い処理を行うメソッドがあり、そのメソッドでは、

  1. using で IDisposable なリソースを管理しています。
  2. Task.Run で実行されています。

それを、あと付けでキャンセル可能にしたくなったとします。

方法案1

定石から CancellationToken.IsCancellationRequested でキャンセルさせればOKです。

デメリット

しかしメソッド内に CancellationToken を渡すとなると Task で動作するという前提が追加されてしまいます。
それは単体テストの修正などが厄介なので避けたいです。

方法案2

そこで折衷案としてはメソッドの時々で yield return させて、戻ってきた時にキャンセルされたか判断させるということを考えました。
処理途中では yield return true を返して処理途中であることを伝え、yield return false が返ったら処理終了です。

疑問

ここで疑問が発生。
yield return で戻ってきた時にタスクをキャンセルさせると、using で管理していた IDisposable はどうなるんだろ?

実験

実際に試してみるのが一番です。

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;

namespace YieldTest {
    class Program : IDisposable {
        bool closed = false;
        static void Main(string[] args) {
            Test(50);
            Test(100);
        }
        static void Test(int delay) {
            Debug.WriteLine("--------------------------------");
            Debug.WriteLine("Test.Start delay:" + delay);
            using (CancellationTokenSource tokenSource = new CancellationTokenSource()) {
                CancellationToken token = tokenSource.Token;
                Debug.WriteLine("Test.Start");
                var task = Task.Run(() => {
                    try {
                        // 長い処理を小分けにして呼び出す
                        foreach (var cont in Continual()) {
                            //token.ThrowIfCancellationRequested();
                            if (token.IsCancellationRequested) {
                                Debug.WriteLine("Task.Run.LoopCanceled");
                                return;
                            }
                        }
                        Debug.WriteLine("Task.Run.LoopCompleted");
                    } catch (Exception e) {
                        Debug.WriteLine("Task.Run.Catch : " + e.Message);
                    } finally {
                        // Program.Dispose はループを抜けた時なのか?の調査
                        Debug.WriteLine("Task.Run.LoopExited");
                    }
                }, token).ContinueWith((o) => {
                    // キャンセルされたら後続処理は動かない
                    Debug.WriteLine("ContinueWith.Execute");
                }, token);
                token.Register(() => { Debug.WriteLine("Token.Callback.TokenCanceled"); });
                Debug.WriteLine("Test.Wait " + delay);
                Thread.Sleep(delay);
                if (task.IsCompleted == false) {
                    Debug.WriteLine("Test.Cancel");
                    tokenSource.Cancel();
                    // Cancel してもすぐには終わらないので
                    Debug.WriteLine("Test.Wait " + 100);
                    Thread.Sleep(100);
                }
                Debug.WriteLine("Test.Exit");
            }
        }

        // 長い処理メソッド
        static IEnumerable<bool> Continual() {
            using (var program = new Program()) {
                for (int i = 0; i < 5; i++) {
                    // 長い処理
                    Thread.Sleep(10);
                    // yield で中断
                    Debug.WriteLine("Continual.YieldReturn");
                    yield return true;
                }
                program.closed = true;

                // using を抜けてから yield break すると、何故かうまく回らない
                // yield 終了
                Debug.WriteLine("Continual.YieldBreak");
                yield break;
            }

            //// yield 終了
            //Debug.WriteLine("Continual.YieldBreak");
            //yield break;
        }

        #region IDisposable Support
        private bool disposedValue = false;
        protected virtual void Dispose(bool disposing) {
            if (!disposedValue) {
                disposedValue = true;
                if (disposing) {
                    Debug.WriteLine("Program.Dispose");
                    if (closed == false) {
                        throw new Exception("not closed");
                    }
                }
            }
        }
        public void Dispose() {
            Dispose(true);
        }
        #endregion
    }
}

実行結果

--------------------------------
Test.Start delay:50
Test.Start
Test.Wait 50
Continual.YieldReturn
Continual.YieldReturn
Continual.YieldReturn
Continual.YieldReturn
Test.Cancel
Token.Callback.TokenCanceled
Test.Wait 100
Continual.YieldReturn
Task.Run.LoopCanceled
Program.Dispose
例外がスローされました: 'System.Exception' (YieldTest.exe の中)
Task.Run.Catch : not closed
Task.Run.LoopExited
Test.Exit
--------------------------------
Test.Start delay:100
Test.Start
Test.Wait 100
Continual.YieldReturn
Continual.YieldReturn
Continual.YieldReturn
Continual.YieldReturn
Continual.YieldReturn
Continual.YieldBreak
Program.Dispose
Task.Run.LoopCompleted
Task.Run.LoopExited
ContinueWith.Execute
Test.Exit

foreach を抜けた段階で、どちらのケースでも Dispose が処理されるようですので一安心です。
Dispose で例外が発生(CryptoStream の変換途中の Close とか)したとしても、キャッチして適切に扱えばOKそうです。

では!

標準ではない文字列リソース管理でときどきしくじる。後悔したので対策を取った。

DotNet Refrection T4 UnitTest wpf

まえがき

いらいらして標準でない文字列リソース管理をやった。全く後悔していない。 での String.cs を T4 で json 化する方式は問題なく動いています。
しかし、コーディングしている当人は問題なく動いていません。

ええ、public const string PropertyName = "OtherName"; とかミスるんですね、これが!

対策案1

Linqのクエリ式を使ったプロパティ名の記述の、ひどい使い方について での方法で、自分のフィールド名を文字列として設定する。

対策案1の結果

役立たないので、特にコードは不詳とします。

System.Exception はユーザー コードによってハンドルされませんでした。
  HResult=-2146233088
  Message=Template runtime error
  Source=TemporaryT4Assembly
  StackTrace:
       場所 Microsoft.VisualStudio.TextTemplating9E8AB61775526334C6D46ECC568F9EA900D98271D962C359E9D3B07FCA9DECDD048F8A79972FBF5F11EAA24C870BA3F8B009EF637F402FB1D214E97D4F72BED5.GeneratedTextTransformation.TransformText() 場所 D:\usr\vs2015\XORveR\XORveR_FP\Resources\Strings.en-US.tt:行 40
       場所 System.Dynamic.UpdateDelegates.UpdateAndExecute1[T0,TRet](CallSite site, T0 arg0)
       場所 Microsoft.VisualStudio.TextTemplating.TransformationRunner.PerformTransformation()
  InnerException: 
       FileName=file:///C:\Users\アカウント名\AppData\Local\Temp\twhymmjk.dll
       FusionLog==== 事前バインド状態情報 ===
ログ: Where-ref バインドです。場所 = C:\Users\アカウント名\AppData\Local\Temp\twhymmjk.dll
ログ: Appbase = file:///D:/Program Files (x86)/Microsoft Visual Studio 14.0/Common7/IDE/
ログ: Initial PrivatePath = NULL
呼び出しているアセンブリ: (Unknown)
===
ログ: このバインドは LoadFrom の読み込みコンテキストで開始します。
警告: ネイティブ イメージは LoadFrom コンテキストで調査されません。ネイティブ イメージは、Assembly.Load() を使用するなどの既定の読み込みコンテキストでのみ調査されます。
ログ: アプリケーション構成ファイル D:\Program Files (x86)\Microsoft Visual Studio 14.0\Common7\IDE\T4VSHostProcess.exe.Config を使用します。
ログ: ホスト構成ファイル  を使用します。
ログ: C:\Windows\Microsoft.NET\Framework\v4.0.30319\config\machine.config からのコンピューター構成ファイルを使用します。
ログ: 新しい URL file:///C:/Users/アカウント名/AppData/Local/Temp/twhymmjk.dll をダウンロードしようとしています。

       HResult=-2147024894
       Message=ファイルまたはアセンブリ 'file:///C:\Users\アカウント名\AppData\Local\Temp\twhymmjk.dll'、またはその依存関係の 1 つが読み込めませんでした。指定されたファイルが見つかりません。
       Source=mscorlib
       StackTrace:
            場所 System.Reflection.RuntimeAssembly._nLoad(AssemblyName fileName, String codeBase, Evidence assemblySecurity, RuntimeAssembly locationHint, StackCrawlMark& stackMark, IntPtr pPrivHostBinder, Boolean throwOnFileNotFound, Boolean forIntrospection, Boolean suppressSecurityChecks)
            場所 System.Reflection.RuntimeAssembly.nLoad(AssemblyName fileName, String codeBase, Evidence assemblySecurity, RuntimeAssembly locationHint, StackCrawlMark& stackMark, IntPtr pPrivHostBinder, Boolean throwOnFileNotFound, Boolean forIntrospection, Boolean suppressSecurityChecks)
            場所 System.Reflection.RuntimeAssembly.InternalLoadAssemblyName(AssemblyName assemblyRef, Evidence assemblySecurity, RuntimeAssembly reqAssembly, StackCrawlMark& stackMark, IntPtr pPrivHostBinder, Boolean throwOnFileNotFound, Boolean forIntrospection, Boolean suppressSecurityChecks)
            場所 System.Reflection.Assembly.Load(AssemblyName assemblyRef, Evidence assemblySecurity)
            場所 System.CodeDom.Compiler.CompilerResults.get_CompiledAssembly()
            場所 Microsoft.VisualStudio.TextTemplating9E8AB61775526334C6D46ECC568F9EA900D98271D962C359E9D3B07FCA9DECDD048F8A79972FBF5F11EAA24C870BA3F8B009EF637F402FB1D214E97D4F72BED5.GeneratedTextTransformation.TransformText() 場所 D:\usr\vs2015\XORveR\XORveR_FP\Resources\Strings.en-US.tt:行 17
       InnerException: 

orz...

T4 でのコンパイラでは、クエリ式使ったソースだからなのか?
もしくはstatic変数をコードで設定するようなクラスだからなのか?
ともかく、そのアセンブリは生成できない模様。

コンパイラのオプションで v4.0 とか色々指定してみたけど、ダメでした。
コンパイル時の参照アセンブリとかも指定しないからダメなのかも?
T4 実行時の名前空間には System.Linq.Expressions も追加してみたり。

そもそも T4 スクリプトの修正はとっても面倒そう。

対策案2

ハッと気づく私。
単体テストでチェックすれば良かっただけでした。

using System;
using System.Reflection;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace XORveR.I18n.Tests {
    [TestClass()]
    public class MessageAttributeTests {
        [TestMethod()]
        public void MessageAttributeTest() {
            Type type = typeof(Strings);
            Type att = typeof(MessageAttribute);
            PropertyInfo Message = att.GetProperty("Message");
            PropertyInfo IsSeparate = att.GetProperty("IsSeparate");
            foreach (FieldInfo field in typeof(Strings).GetFields()) {
                var mesatt = field.GetCustomAttribute(att);
                var message = (string)Message.GetValue(mesatt);
                var isseparate = (bool)IsSeparate.GetValue(mesatt);
                var value = field.GetValue(null);
                Assert.AreEqual(field.GetValue(null), field.Name);
            }
        }
    }
}

全力で空回りしている感が強いです。

では!

いらいらして標準でない文字列リソース管理をやった。全く後悔していない。

DotNet T4 Refrection wpf

まえがき

現在開発中のアプリは、標準の国際化手順はガン無視して、文字列リソースは json で提供します。
しかし、文字列リソースを識別する識別子、ぶっちゃけ Properties\Resources.Designer.cs の代わりになる機構が無ければ管理がそりゃもう面倒です。

本題

そこで、逆転の発想。
Properties\Resources.Designer.cs の代わりにしている Strings.cs を元に、ニュートラルカルチャ用の json を作ってしまいましょう。
というのが、今回のお題です。

Strings.cs

Strings.cs は以下のようになっています。
MessageAttribute クラス。これはフィールドに設定する属性で、これがキモとなります。
ニュートラル言語は英語で書いています。Google先生頼りです。

using System;

namespace XORveR.I18n {
    [AttributeUsage(AttributeTargets.Field, AllowMultiple = false, Inherited = false)]
    public class MessageAttribute : Attribute {
        private string message;
        private bool isseparate;
        public MessageAttribute(string message, bool isseparate = false) {
            this.message = message;
            this.isseparate = isseparate;
        }
        public string Message { get { return this.message; } }
        public bool IsSeparate { get { return this.isseparate; } }
    }

    public static class Strings {




        // FabricTransform use
        [Message("must transform whole block", true)]
        public const string Cryptography_MustTransformWholeBlock = "Cryptography_MustTransformWholeBlock";
        [Message("transform beyond end of buffer")]
        public const string Cryptography_TransformBeyondEndOfBuffer = "Cryptography_TransformBeyondEndOfBuffer";

        // company name
        [Message("XORveR.com", true)]
        public const string Company_Name = "Company_Name";
    }
}

json

Strings.cs から作成される json は以下の感じです。

{




  "Cryptography_MustTransformWholeBlock": "must transform whole block",
  "Cryptography_TransformBeyondEndOfBuffer": "transform beyond end of buffer",

  "Company_Name": "XORveR.com"
}

どーうやって変換するの?

勘の良い読者なら分かるかと思いますが、

  1. コード生成と T4 テキスト テンプレート
  2. CodeDomProvider による動的コンパイル
  3. リフレクション

の合せ技です。

Strings.en-US.tt

こいつが変換の主役です。
基本的には手動でツール実行しなければなりませんが、AutoT4 拡張機能 を入れてやれば、ビルドの度に自動で Strings.en-US.json を作成してくれます。

<#@ template debug="true" hostspecific="true" language="C#" #>
<#@ assembly name="System.Core" #>
<#@ import namespace="System.CodeDom.Compiler" #>
<#@ import namespace="System.IO" #>
<#@ import namespace="System.Reflection" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ output extension=".json" encoding="utf-8" #>
<# 
    var filename = this.Host.ResolvePath("Strings.cs");
    var list = new List<string>();

    CodeDomProvider cscp = CodeDomProvider.CreateProvider("CSharp");
    var source = File.ReadAllText(filename);
    var param = new CompilerParameters();
    param.GenerateInMemory = true;
    var cr = cscp.CompileAssemblyFromSource(param, source);
    Assembly asm = cr.CompiledAssembly;

    Type type = asm.GetType("XORveR.I18n.Strings");
    Type att = asm.GetType("XORveR.I18n.MessageAttribute");
    PropertyInfo Message = att.GetProperty("Message");
    PropertyInfo IsSeparate = att.GetProperty("IsSeparate");
    foreach (FieldInfo field in type.GetFields()) {
        // 本当は、プロパティとかの順番は無保証・・・
        var mesatt = field.GetCustomAttribute(att);
        var message = (string)Message.GetValue(mesatt);
        var isseparate = (bool)IsSeparate.GetValue(mesatt);
        if (isseparate) {
            list.Add("");
        }
        if (field.Name.Equals("Company_Name")) {
            list.Add("  \"" + field.Name + "\": \"" + message.Replace("\"", "\\\"") + "\"");
        } else {
            list.Add("  \"" + field.Name + "\": \"" + message.Replace("\"", "\\\"") + "\",");
        }
    }
#>
{
<#
    foreach (string line in list) {
#>
<#= line #>
<#
    }
#>
}

json による国際化については、日を改めて紹介する予定です。

では!

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

DotNet UnitTest wpf

まえがき

イベントハンドラを 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) は全パス確認成功となります。

では!

Linqのクエリ式を使ったプロパティ名の記述の、ひどい使い方について

DotNet wpf

まえがき

プロパティ名の記述をクエリ式でタイプセーフに行う手法*1*2はもう有名*3です。

しかしコーディングしていて、はたと困りました。

困ったこと

スタティックなプロパティ(なんて使うなよ!)をどうやってタイプセーフに書けばいいのん?

現状認識

まずは今の実装の説明から。

        public static string PropertyName<TObj, TProp>(
            this TObj instance, Expression<Func<TObj, TProp>> pne
        ) {
            return ((MemberExpression)((LambdaExpression)pne).Body).Member.Name;
        }

こんな感じでなんの工夫もなく、元記事をパクっています。これを、やはり何の変哲もなく

Texts.PropertyName(o => o.CurrentSettingFolder)

とかやりたいわけです。

ええ、Texts の CurrentSettingFolder が staticでさえなければ。

ひどい使い方

anyObject.PropertyName(o => Texts.CurrentSettingFolder)

これで解決!ォィ


では!