2018年5月26日土曜日

オブジェクト指向とは何か

オブジェクト指向の「オブジェクト」とは「object型」のことではない
私のブログを読んでくださっている方なら、オブジェクト指向言語には慣れ親しんでいるかと思います。でも、「オブジェクト指向」をちゃんと説明できますか?

「オブジェクト指向」の疑問

オブジェクト指向の初学者は、まず最初にオブジェクト指向の3要素を学ぶと思います。
  • カプセル化
  • 継承
  • 多相性(ポリモーフィズム)
言い方に多少の差はあれど、だいたいこんな感じかと思います。
復習しておくと、カプセル化は内部の動作手順を隠蔽し、許可された手法のみを使って操作をできるようにすること、継承はクラス内容を引き継ぎつつ追加の機能を付けたすこと、多相性はメソッドを呼んだ時のふるまい等を実行時の型に合わせて決定することですね。
C++やJava、C#などの代表的なオブジェクト指向言語では、このような要素に対して「クラス」「継承」「メソッドのオーバーライド」などといった具体的な機能が備わっており、これらの要素と言語機能の習得を並行して行うと理解がよく深まるかと思います。

ですが、オブジェクト指向に関する勉強がこれで終わっていないでしょうか。
これはオブジェクト指向の特徴を挙げただけであり、そもそもなぜ「オブジェクト指向」という名前かということには何も触れていません。

そして、そのままオブジェクト指向言語を習得していくと、
  • ありとあらゆるクラス・型の最上位にはobject型がある(すべてのクラス・型はobject型を継承している)
  • ありとあらゆるメソッドは何かしらのクラス(=object型を継承したもの)に属さなければならず、オブジェクトへの操作として定義される
みたいな言語機能を見かけて、「ああ、ありとあらゆる操作がobject型に対する操作だからオブジェクト指向言語なんだな」みたいな納得感を持ってしまっている人も少なからずいるでしょう。私もそうでした。

そもそも、上の箇条書きで出てきた「オブジェクト」って何ですか?クラスのこと?それともインスタンスのことでしょうか。なんとなく意味も分からず、クラスやインスタンスとの違いも明確にしないまま適当に横文字をしゃべっているように見えません?かといって、objectを辞書で引くと「モノ」という訳が出てくるだけで、そんな抽象的な名詞では何も理解が進むことはありません。

だいたい、「オブジェクト言語」ではなく「オブジェクト指向言語」と呼ばれるのもなぜなのでしょうか。上の箇条書き程度の理解では、全然「指向」の意味がわかりませんよね。「指向」すなわち「oriented」を辞書で引くと「~の方向を向いている」とか「~を中心に考える」と言った訳が出てきますが、「オブジェクト」が何かよくわからないのに、そっちを向いているとかそれが中心だとか言われてもわけわかりません。

モジュール化

さて、「オブジェクト指向」とは何かについて紐解いていく前に、ここで一旦「モジュール化」の話をしましょう。

コンピューターが生まれたばかりの頃はごく小規模なプログラムが動いていましたが、コンピューターが発達するにつれて非常に大規模で複雑なプログラムが必要になってきました。
ですが、各プログラムの部分が複雑に絡み合っていては全貌が見えにくくなり、また、複数人での分担してプログラムを書くこともしにくくなります。当然、管理も難しくなって、機能の変更や追加があった時や不具合があった時に太刀打ちできなくなってしまいます。
なので、プログラムを何らかの基準で分割し、整理する必要が出てくるのです。こういうのを一般にモジュール化と言います。

ではどこで分割するのが良いでしょうか。
デイビッド・パーナスは、自身の論文で次のように述べています。
We have tried to demonstrate by these examples that it is almost always incorrect to begin the decomposition of a system into modules on the basis of a flowchart. We propose instead that one begins with a list of difficult design decisions or design decisions which are likely to change. Each module is then designed to hide such a decision from the others. (Parnas D.L. (December 1972).)

(拙訳)私たちは、フローチャートに基づいてシステムのモジュール化をするのはおおむね不適切だということをこれらの例を用いて示した。その代わりに、難しい設計上の決定や、変更されやすい設計上の決定に基づいて行うことを提案する。これらのモジュールは、それらの決定を他から隠すような設計となる。
フローチャートに基づいてモジュール化するというのは、大きなプログラムの流れをフローチャートで書いてから、その個々の中身をモジュールとして実装する、というような意味のようです。長~い関数を書いてしまう人がその次のステップとしてやってしまいそうなことですね。
モジュール化をするときはそういうやり方をするのではなく、難しいことや変更されやすいことの単位で行うことのほうが効果的だそうです。

では、難しいことって何でしょう。 変わりやすいことって何でしょう。

難しいことや変わりやすいことというのは、往々にして何かの「内部表現」であることが多いです。例えば、内部的にはUTF8を使うのか、Shift-JISを使って処理をするのかとか、リストは配列を使うのか、連結リストを使うのか、などといったことが挙げられます。
なぜならば、やることと言うのはプログラムそのものの存在意味なのでそうそう変わるものではないからです。その実現手段や、内部でのデータ表現の方法はいくらでも変えることができますし、状況が変われば変わり得るでしょう。

そうすると、プログラムを「内」と「外」で分けたくなってくるわけです。内部表現は隠蔽し、外部とのやり取りは定められたインターフェースに則って行う、という形が、モジュール化をする上で大変うまく行く方法だということです。

"モノ視点" で分割する

じゃあどうやって内と外を分けるかと言ったら、1つの方法として、「モノ視点」というのが挙げられるでしょう。

例えば自動車。自動車は運転手から見れば「アクセルペダル」「ブレーキペダル」「シフトレバー」「ハンドル」などの決められたインターフェースがあり、それを外(自動車の駆動機構やエンジン等が入っている場所の外という意味)から操作することで、運転手の意図したとおりに加速、減速、曲がるなどといった動作を出力してくれます。ここで自動車の内部の状態(スロットルがどのくらい開いている、燃料の流量はどれくらい、エンジンの回転数は、温度は、シャフトの回転数は…などなど)は運転手がいちいち気を使う必要が無いですよね。

もうわかったでしょうか。

「オブジェクト指向プログラミング」というのは、「モノを中心に内と外を分けるプログラミング」ということなのです。「オブジェクト」が指しているのはインスタンスでもクラスでもなく、「モノ」なのです。
「モノ」と対になる言葉 として「コト」があります。上記の「フローチャートに基づいて分割する方法」は大まかな処理の流れ(コト)に着目して分割するということですが、オブジェクト指向言語はモノの単位で内外を分割してモジュール化をしようという発想が原点にある言語、という意味なのです。

なので、「オブジェクト指向言語ってどんな言語?」と聞かれたら、「モノ視点で内外を分けてモジュール化する言語」と答えれば良いでしょう。

モノ視点でオブジェクト指向3要素を見る

さて、冒頭で紹介したオブジェクト指向の3要素ですが、決してあれを軽視しているわけではありません。当然、「モノ視点で内外を分けてモジュール化する」という原点からいろいろと考えを深めていくと、それらの3要素に行きつくわけです。

カプセル化

これは「内外に分ける」ということですね。オブジェクト指向ならモノ視点です。
外からインターフェースを介してモノの内部状態を変化させるのがカプセル化です。

継承

これは「モノの分類」に当たります。そもそも「class」という言葉が「分類」ですしね。
例えば、自動車は普通車や大型車などに分類でき、大型車はバスやトラックなどに分類でき、例えばバスならば「自動車の基本機能」+「大型車の追加機能」+「バスの追加機能」みたいな関係性があることが言えます。
モノを階層構造で分類し、その差分を表現するのが継承になります。

多相性(ポリモーフィズム)

これは「分類が同じでも実体は別々になり得る」という意味です。
例えば同じ「ヒト」であっても一人ひとり個性があって違いますよね。その場合、同じ働きかけ(外からの操作)をしてもふるまい方(結果の出力)は異なるでしょう。
分類としてのふるまいではなく、モノそれぞれでふるまうことができるというのが多相性ということになります。

オブジェクト指向以外のモノの見方

さて、ここまで説明してきたオブジェクト指向ですが、欠点が無いわけではありません。

例えばオブジェクト指向を扱ったことがある人ならば、「分類の視点」について悩みを持ったことがあるでしょう。
自動車を大型車、普通車という車種で分類することもできるかもしれませんが、例えばトヨタ車、ホンダ車、日産車…という分類もできるはずです。一つの見方で使うだけならばそれでいいのですが、場合によって見方を変えなければいけないような場合では困ります。
が、多重継承にも様々な問題があり、多重継承構造を認めていないオブジェクト指向言語も少なくありません。

そこで、サブジェクト指向言語というものが開発されました。
「見方によって階層構造が変わってくるから、階層を別に定義できるようにしよう」という発想で、「主題」=「subject」で分割しようという言語です。
Hyper/Jなどという言語があるそうです。
ただ、サブジェクト指向はいろいろな問題があったようです。

そういった背景もあってか、オブジェクト指向のように1つの階層構造をとりあえず作って、様々な階層に横断的に適用させる記述をできるような言語も開発されました。それをアスペクト指向言語と言います。

例えばログの出力コードのような全く階層構造が違うようなクラスに横断的にかかわるようなコードはいろいろな場所に散在しがちです。
こういった関心ごとを分離してまとめるのがアスペクト指向なわけです。

ま、正直この辺りは私はあまり詳しくないのですが。





さて、一通りの説明が終わりましたがいかがでしたでしょうか。

実際にプログラムを書くと、「変わりやすいところを隠蔽」「モノ単位で分割」とか言葉で言うのは簡単でも実際にやるのはなかなか難しいことに気がつくでしょう。ただ、このブログで基本的なオブジェクト指向の考え方はわかっているはずですから、迷ったら原点に立ち返っていろいろと考えていきましょう。

ではでは。

2018年5月23日水曜日

ESP8266にDTRとRTSで自動書き込みをする

ESP8266を搭載したESP-WROOM-02が日本の組み込み市場に出てきてから約3年になりますが、あまりこれに関する記事を見なかったので(少しはあるようです)。
やっぱり組み込み系の記事を見ていると圧倒的にソフト屋さんのブログが多く、ハード屋さんが全然いないんだなって。

ESP8266のリセット回路

ESP8266のリセット回路はこのようになっています。IO0ピンをLにした状態で立ち上げる(リセットする)とブートローダーが起動し、UARTからプログラムが書きこめるようになります。すなわち、IO0と~RESETのスイッチを両方押して、~RESETを離してからIO0を離して書き込みボタンを押すという操作が必要になります。
ですが、書き込みをするたびにスイッチ2つを操作しないといけないのはいささか不便です。可能ならばArduino IDE上で書き込みボタンを押したらそのまま勝手に書き込まれてほしいものです。

そんな誰もが考えることは、ちゃんとSDKの提供者たちも考えてくれていて、その解決策が提供されています。
ズバリ、DTR端子とRTS端子を使います。

シリアル通信のフロー制御

DTR端子とRTS端子を使うと言いましたが、TXDやRXDとは違ってあまり聞きなれない端子です。これはいったい何なのでしょう。

UARTなどのシリアル通信では、送信側が送信したいタイミングでデータの送信をできてしまいます。ですので、受信側が受信する準備ができていない場合、勝手にデータを送信されても取りこぼしてしまいます。そのようなことがあっては不都合な場合、相手方に「受け入れ可能だよ」と言うことを伝える制御を行います。その制御のことをフロー制御と言います。
信号線名 名称 説明
TXD Transmit Data 送信データ
RXD Receive Data 受信データ
TXDを受ける。
DTR Data Terminal Rady 端末準備完了
機器の電源が入りポートが開かれると1になる。
DSR Data Set Ready データセットレディ
DTRを受ける。
RTS Request To Send 送信要求
自分の受信バッファーに十分な空きがあり、データの受け入れが可能にになると1となる。
CTS Clear To Send 送信可能
RTSを受ける。
SG Signal Ground 信号線の接地ライン

フロー制御ありのシリアル通信は、通常このように接続します。DTRで相手にこちらの端末が生きていることを伝え、RTSでデータ受け入れ可能であることを伝えます。
このことから、RTSが1になるときは必ずDTRも1になっているということがわかります。こちらの端末が通信できる状態じゃないのにデータ受け入れ可能になるわけはないですから。
FT232などのUSB-シリアル変換ICにもこのフロー制御用のピンは出ており、ソフトウェアから任意に出力を変更可能です。ただし、FT232ではDTRとRTSの論理が逆転しているので注意が必要です。
FT232R Datasheet - FTDI Chip より引用)

DTRとRTSを使ったリセット回路

さて、これを踏まえてDTRとRTSを使ったリセット回路を作る必要がありますが、大前提として「通常使用時(ESP8266の通常動作時)にシリアル通信をしてもリセット指令が出ない」というのが最低限の要件として挙げられます。
フロー制御を使っていないと言いつつも別用途でそれらの線を結線してしまっているわけですから、例えばPCで汎用ターミナルソフトを使ってESP8266とシリアル通信したときに、そのソフトが気を利かせてフロー制御を行ったがゆえに偶然リセットがかかっては困ります。
そこで先ほどの赤字を思い出してください。RTSが1になるときは必ずDTRも1になっているので(RTS,DTR)=(1,0)は通常のフロー制御ではありえないということになります。
なので、この状態をリセットに割り当てればいいわけです。
その条件をもとにトランジスタ1個で作った回路がこちらになります。FT232に合わせてDTRとRTSの論理を逆転させています。
(~RTS,~DTR)=(0,1)のときのみ~RESETが0になり、他の時は1になります。
そのほか、~RTS=IO0になりますので、~DTR=0にした状態で~RTSを操作することでIO0を変更することもできます。
これで無事にリセットシーケンスが送れるようになりました。めでたしめでたし。

ただ、1つ問題があります。
IO0を出力に使えません。また同じ系統の問題として、手動用のスイッチと共存させることができません。そうすると、RTSとIO0の間に1段トランジスタを挟みたくなりますよね。それならばせっかくなので対称な回路にしてしまいましょう。
これでよく見かける回路になりました。完成です。

真理値表を書くとこんな感じになります。
~RTS ~DTR ~RESET IO0
L L H H
L H L H
H L H L
H H H H
(~RTS,~DTR)=(L,H)のときのみ~RESETがLになり、他のパターンでは~RESET=1となります。そのほか、リセットにかかわらずIO0を制御する方法も確保できており、出力はプルアップとなっているためスイッチとの共存も可能です。
トランジスタ2石で完璧なリセット回路が出来上がりました。

自動書き込みを行う

さて、回路が出来上がったら実際に書き込みます。
Arduino IDEのツールメニューからReset Methodを"nodemcu" (NodeMCU)に指定してあげれば書き込みの直前でDTRとRTSを操作してくれます。
実際に書き込みの時のRESET端子とIO0端子を観察した画像がこちらです。青が~RESET、黄色がIO0です。最初にリセットをかけてから、その入れ替わりでIO0をLにしています。たいていRESET後はクロックが安定するまで(数~十数ms程度)マイコンは立ち上がりませんので、 RESETを終了させるのと同時にIO0をLにしても十分間に合うわけですね。


書き込みが始まるとIO0の黄色線がガタガタ震え始めています。
これは実際にマイコンとデータのやり取りを始めてRTSが変化しているためですね。フロー制御が行われているだけですが、目論見通り~RESETは安定してHを保てています。

これで幸せなESPライフが送れるようになりました。

2018年4月29日日曜日

自然なソート順序

突然ですが、Windowsのエクスプローラーのソートってなかなか気が利くと思いません?
ファイル名でソートを掛けると、上記の画像のような順に並び変わります。
それでは、これを通常のソートを掛けるとどうなるでしょう。
foreach(var s in new[] { "test01.txt", "test1.txt", "test3.txt", "test10.txt", }.OrderBy(p => p))
    Console.WriteLine(s);
あれ?エクスプローラーだとtest10.txtよりtest3.txtのほうが前に来ていたけどC#のOrderByだと逆転しているぞ?バグかな?

そんなことはありません。
OrderByは文字列を辞書順に並び替えます。もっと正確に言えば、文字コードの若い順から辞書順に並び替えます。ですので、1文字ずつ文字コードを比較していって、test10.txtの1とtest3.txtの3を見て、前者のほうが若番なので辞書的に前だと判断しているのです。

ですが、実際にファイル名を並び替えた人からしたらtest3.txtとtest10.txtは前者のほうが前に来てほしいですよね。これが「Natural Sort Order(自然なソート順序)」です。


さて、このようなソートをする方法ですが、ググるといろいろな方法が出てきます。丸投げしたいのならば、Nugetを漁ったりStrCmpLogicalW関数をP/Invokeで使うのも手でしょう。ですが、私はあまり気に入った実装が無かったので自分で実装してみることにしました。

まずは下準備として、文字列を数字と文字の境界で区切るメソッドを作ります。
internal static IEnumerable<string> SplitBy(this string Source, Func<char, char, bool> BorderSelector)
{
    int start = 0;
    for(int i = 0; i < (Source.Length - 1); i++) {
        if(BorderSelector(Source[i], Source[i + 1])) {
            yield return Source.Substring(start, i + 1 - start);
            start = i + 1;
        }
    }
    yield return Source.Substring(start, Source.Length - start);                
}
文字列の前後2文字を渡して境界かどうかを判定するデリゲートを渡せば、そこで区切った文字列を返してくれます。ちゃんと遅延評価も行えるように気を配って実装しました。
これを使って、IComparer<string>を実装します。
public class NaturalComparer : IComparer<string>, System.Collections.IComparer
{
    static Func<char, char, bool> NumberCharBorder = (p, n) => (('0' <= p && p <= '9') && !('0' <= n && n <= '9')) || (!('0' <= p && p <= '9') && ('0' <= n && n <= '9'));

    public int Compare(string x, string y)
    {
        using(var xe = x.SplitBy(NumberCharBorder).GetEnumerator())
        using(var ye = y.SplitBy(NumberCharBorder).GetEnumerator()) {
            while(true) {
                var xHasNext = xe.MoveNext();
                var yHasNext = ye.MoveNext();

                if(xHasNext && yHasNext) {
                    int ret = (ulong.TryParse(xe.Current, out ulong xi) && ulong.TryParse(ye.Current, out ulong yi)) ?
                        Comparer<ulong>.Default.Compare(xi, yi) :
                        Comparer<string>.Default.Compare(xe.Current, ye.Current);

                    if(ret != 0) return ret;
                } else
                    return (xHasNext ? 1 : 0) - (yHasNext ? 1 : 0);
            }
        }
    }

    int System.Collections.IComparer.Compare(object x, object y)
    {
        try {
            return Compare((string)x, (string)y);
        }
        catch(InvalidCastException e) {
            throw new ArgumentException(e.Message);
        }
    }

    public static NaturalComparer Default
    {
        get
        {
            if(_Default == null)
                _Default = new NaturalComparer();
            return _Default;
        }
    }
    static NaturalComparer _Default = null;
}
xとyを先ほどのSplitByで分割し、それを同時に列挙していっています。
MoveNext()Currentを自前で呼ぶのはできれば避けたかったのですが、 例えばZipで同時に列挙すると、xとyの分割したときの長さが分からなくなってしまうので、やむを得ず手動で列挙することにしました。
分割さえできれば後は簡単です。分割項が両方とも数字(ulong)として読めるならば数字として比較し、それ以外なら文字列として比較します。比較結果が一致しなかったら、その大小関係を返し、一致したら次の分割項へと移っていきます。分割項が片側無くなった場合は、まだ残っているほう(元の文字列が長いほう)を大として返します。
 絵にするとこんな感じです。文字列(青線)と数字(橙線)に分け、それぞれで大小比較をします。例2のように片側がすでに末尾に達して、もう片側は末尾に達していなかった場合は、後者を大としています。

C#のLINQのOrderByにはIComparer<T>を受け取るオーバーロードがあるので、これに渡してあげればこの自然順ソートができます。
が、せっかくなので、親切に拡張メソッドも作っておいてあげましょう。
public static IOrderedEnumerable<T> NaturallyOrderBy<T>(this IEnumerable<T> Source, Func<T, string> KeySelector)
{
    return Source.OrderBy(KeySelector, NaturalComparer.Default);
}

public static IOrderedEnumerable<string> NaturallyOrderBy(this IEnumerable<string> Source)
{
    return Source.OrderBy(p => p, NaturalComparer.Default);
}

public static IOrderedEnumerable<T> NaturallyOrderByDescending<T>(this IEnumerable<T> Source, Func<T, string> KeySelector)
{
    return Source.OrderByDescending(KeySelector, NaturalComparer.Default);
}

public static IOrderedEnumerable<string> NaturallyOrderByDescending(this IEnumerable<string> Source)
{
    return Source.OrderByDescending(p => p, NaturalComparer.Default);
}

public static IOrderedEnumerable<T> NaturallyThenBy<T>(this IOrderedEnumerable<T> Source, Func<T, string> KeySelector)
{
    return Source.ThenBy(KeySelector, NaturalComparer.Default);
}

public static IOrderedEnumerable<string> NaturallyThenBy(this IOrderedEnumerable<string> Source)
{
    return Source.ThenBy(p => p, NaturalComparer.Default);
}

public static IOrderedEnumerable<T> NaturallyThenByDescending<T>(this IOrderedEnumerable<T> Source, Func<T, string> KeySelector)
{
    return Source.ThenByDescending(KeySelector, NaturalComparer.Default);
}

public static IOrderedEnumerable<string> NaturallyThenByDescending(this IOrderedEnumerable<string> Source)
{
    return Source.ThenByDescending(p => p, NaturalComparer.Default);
}
単純にOrderByなどのメソッドをラップしているだけです。このNaturallyOrderByにはstringの配列を渡すことが圧倒的に多いはずですので、KeySelectorを省略できるオーバーロードも用意してあげました。

今回のこのプログラムでは正規表現も使っていませんし、比較時の列挙も1回きりです。相当なパフォーマンスで動作してくれると思います。
これで、自分の作るソフトの中でも自然順ソートを気軽に使えるようになりました。



余談1
Windowsエクスプローラーがこのソート順序に対応したのはXPくらいからだったと思います。少なくともWindows98のときは対応しておらず、ちゃんとファイル名の数字の桁数をそろえて(桁数が少ないものは0埋めして)やらないとソートできなかった記憶があります。
StrCmpLogicalWがあってもStrCmpLogicalAが無いことからしても、Windows 9x系統では実装されていなかったんでしょうね。

余談2
英語圏の人が思いつく「自然なソート順序」はこれくらいかもしれませんが、日本語圏にいると「全角/半角を区別せずにソート」とか「漢数字も数字としてソート」とかいろいろ思いつくと思います。
もっと極端な例を言えば「前編」「後編」が付くファイル名があれば「前編」のほうが前に来てほしいですが、文字コードでは前>後です(おそらく音読みの「ゼン」「コウ」で五十音順に並べているため)のでそのようにはなりません。ただ、前原さんと後藤さんだったら後藤さんのほうが前に来てほしいですし、単純に「前」を前に持ってくればいいというわけでもありません。
そういう意味からしても「自然なソート順序」というのは非常に曖昧な定義で、網羅的に実装するのには無理があるでしょう。どこで決着をつけるかはプログラマーのさじ加減かと思います。

2018年4月7日土曜日

アンマネージドリソースをDisposeパターンで管理する

モダンな言語や環境はガベージコレクタ(GC)が付いているのが当たり前になった時代です。GCによってどこからも参照されなくなったインスタンスは適当なタイミングで解放されるため、「メモリを解放する」という本来ならばものすっごく神経を使う作業から人類は解放されました。それはそれで便利で歓迎されることなのですが、かと言ってありとあらゆるリソースの解放をGC任せにできるわけでもありません。

そこで、.NET FrameworkにはIDisposableというインターフェイスを用意されていて、もうこれ以上このインスタンスは使わないから明示的にリソースを解放したいと思ったときにはDisposeメソッドを呼べばいいようになっています。
このインターフェイスは様々なクラスで使用されており、例えばイベントの購読停止だったり、ファイルのロックの解除だったりといった終了処理が行われます。また、言語面でも優遇されており、using構文を使うことで自動的にtry-finally構文に展開され確実にDisposeメソッドを呼ぶことができるようにもなります。

逆に、自動で解放されないリソースを使用する場合、それを使うクラスでIDisposableインターフェイスを実装しなければなりません。
IDisposeインターフェイスがDisposeメソッド1つのみしか持たないので「そのDisposeメソッドの中で解放処理をすればいいんでしょ?」と思ってしまいたくなりますが、実は話はそんなに単純ではありません。Disposeパターンと呼ばれるもうちょっとしっかりした実装が必要になってくるので、この記事では自動で解放されないリソースをサンプルとして使いつつその実装のしかたを見ていきたいと思います。

Disposeパターン

さて、 Disposeパターンはどういう書き方をするのか、ひな形を暗記しなければならないのかと思ったそこのあなた、心配いりません。IntelliSenseがひな形を用意してくれています。
これに従ってひな形を作るとこのようなコードが自動生成されます。
public class ExcelApp : IDisposable
{
    #region IDisposable Support
    private bool disposedValue = false; // 重複する呼び出しを検出するには

    protected virtual void Dispose(bool disposing)
    {
        if(!disposedValue) {
            if(disposing) {
                // TODO: マネージ状態を破棄します (マネージ オブジェクト)。
            }

            // TODO: アンマネージ リソース (アンマネージ オブジェクト) を解放し、下のファイナライザーをオーバーライドします。
            // TODO: 大きなフィールドを null に設定します。

            disposedValue = true;
        }
    }

    // TODO: 上の Dispose(bool disposing) にアンマネージ リソースを解放するコードが含まれる場合にのみ、ファイナライザーをオーバーライドします。
    // ~ExcelApp() {
    //   // このコードを変更しないでください。クリーンアップ コードを上の Dispose(bool disposing) に記述します。
    //   Dispose(false);
    // }

    // このコードは、破棄可能なパターンを正しく実装できるように追加されました。
    public void Dispose()
    {
        // このコードを変更しないでください。クリーンアップ コードを上の Dispose(bool disposing) に記述します。
        Dispose(true);
        // TODO: 上のファイナライザーがオーバーライドされる場合は、次の行のコメントを解除してください。
        // GC.SuppressFinalize(this);
    }
    #endregion
}
コメントの翻訳がガバガバで多少見苦しいですが、目を瞑ってあげましょう。

まず注目すべきは、インターフェースで規定されたDispose()メソッド以外にDispose(bool disposing)メソッドが用意されています。Dispose()メソッドからはDispose(bool disposing)メソッドを呼び出しているだけになっていますね。

実は、リソース解放処理を行うのはDispose(bool disposing)メソッドのほうになっています。この引数のdisposingは「Dispose()メソッドの呼び出しによってリソース解放処理を行うのかどうか」を示す引数です。

GCがこのクラスを回収に来た時、すなわちファイナライザが呼ばれたときは、そのクラス内で使っているGCが回収できるリソース(マネージドリソース)を敢えて解放する必要はありません。なぜならば、それらもGCが回収するからです。ですが、わざわざDispose()メソッドを呼び出してリソースを解放しようとするときは、内部的に使っているマネージドリソースもしっかり解放してあげないと、当然そのタイミングではGCが回収しに来ないためリソース解放漏れになります。

逆に、アンマネージドリソースは、Dispose()が呼び出されたときでもファイナライザが呼び出されたときでも確実に解放する必要があります。ですので、if(disposing)のブロックの外に解放処理を書き、ファイナライザをコメントアウト解除する必要があります。
また、ファイナライザに呼ばれるより先にDispose()メソッドが呼ばれた場合は、解放処理が重複してしまいますので、GC.SuppressFinalize(this);を呼び出してファイナライザの作動を抑制する必要があります。

あとは、disposedValueフィールドですでにこのクラスが破棄されたかどうかを保持しておりますので、もしもDispose後にこのクラスのメソッドやプロパティにアクセスされたときはObjectDisposedExceptionを投げるようにしてあげましょう。

サンプルコード:COMによるC#からのExcel操作

さて、Disposeパターンの実装がわかったところで、自動で解放されないリソースをDisposeパターンで記述するサンプルコードを書いてみましょう。
自動で解放されないリソースの代表格としてCOMがあります。「C# Excel」とかでググるといくらでもCOMを使った記事が出てきますが、その記事の数だけ「ソフトを終了したのにタスクマネージャーを開いたらEXCEL.EXEが残ったままだ」というコメントが見られることからも、リソースの解放が重要になってくるリソースです。
using Excel = Microsoft.Office.Interop.Excel;

public class ExcelApp : IDisposable
{
    Excel.Application excel = null;
    Excel.Workbook workbook = null;
    
    public ExcelApp(string filename)
    {
        excel = new Excel.Application();
        excel.Visible = true;

        workbook = excel.Workbooks.Open(filename);
    }

    #region IDisposable Support

    private bool disposedValue = false;

    protected virtual void Dispose(bool disposing)
    {
        if(!disposedValue) {
            if(disposing) {
                // TODO: Managed Objectの破棄
            }

            if(workbook != null) {
                workbook.Close();
                System.Runtime.InteropServices.Marshal.ReleaseComObject(workbook);
                workbook = null;
            }

            if(excel != null) {
                excel.Quit();
                System.Runtime.InteropServices.Marshal.ReleaseComObject(excel);
                excel = null;
            }

            disposedValue = true;
        }
    }

    ~ExcelApp()
    {
        Dispose(false);
    }

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    #endregion
}
さて、Disposeパターンに従って書くとこのような感じになります。
COMはアンマネージドリソースですので、ファイナライザやSupposeFinalizeメソッドのコメントアウトを解除する必要があります。

ときどきSystem.Runtime.InteropServices.Marshal.ReleaseComObject(obj);の必要性やについての議論や、ひどい場合は「これを書かないからEXCEL.EXEプロセスが残ってしまう」といった誤った記述がされているサイトがありますが注意してください。
Workbook.Close()でワークブックを閉じて、Application.Quit()でアプリケーションを閉じさえすればEXCEL.EXEプロセスは正常に終了されます。Marshal.ReleaseComObjectは、.NETからのCOMオブジェクト(=Excelを操作するためのインターフェイス)を解放するためのメソッドで、これの有無にかかわらずExcelはしっかりと終了してくれます。
リソースの解放をしたらそれ以降はExcelの操作をしないわけですから、COMオブジェクトを解放しておくべきでしょう。

こうすることで
using(var excel = new ExcelApp(@"test.xlsx")) {

}
このように書くだけで、usingブロックを抜けたときにExcelがしっかりと終了されます。この場合はDispose()メソッドが呼ばれることによりExcelの終了処理が行われていることがわかります。

他にも、敢えてDispose()を呼び出さないようなコードを書いても、ちゃんとアプリケーション終了時にGCがファイナライザを呼び出してExcelが終了されることが分かります。 このDisposeパターンは確実にアンマネージドリソースを解放する方法として有効でしょう。

※上記のDisposeパターンのプログラムは、Disposeパターンの説明をするために書いているものでExcelを終了させるうえで完璧ではない点に注意してください。例えば、Excelを開いている間にファイルに変更を加え、保存せずにDisposeメソッドが呼ばれるとExcelのメッセージボックスで終了処理がブロックされます。
利用者は、各自アレンジして使ってください。

2018年3月24日土曜日

ListView Extensions ver.1.0.1リリース

ListView Extensions ver.1.0.1をリリースしました。
NuGet Gallery | ListViewExtensions 1.0.1
変更点は以下の2点です。
  • Obsoleteに指定していた非同期アクセス非サポートのクラスを削除した
  • SyncedObservableCollection<T>が実装する非ジェネリックインターフェイス(IList, ICollectionなど)を明示的なインターフェイスの実装にした
どちらも破壊的な変更ですので、今までコンパイルが通っていたコードがアップデートによって通らなくなる可能性があります。ただ、その影響範囲は小さいはずです。
そもそもこの変更でコンパイルが通らなくなるようなコードは悪いコードなので悔い改めてください。


ところで、「明示的なインターフェイスの実装(explicit interface implementation)」 というC#の機能、しっかり知りませんでした…。恥ずかしい///
明示的なインターフェイスの実装 (C# プログラミング ガイド)
このような機能が必要になる状況として、上記のサイトでは、同じシグネチャのメソッドを持っている別々のインターフェイスを両方とも実装するクラスで、それぞれ別々の実装を行いたいときというのが説明されています。
それはそれで納得はいくのですが、どちらかと言えば今回重要になってくるのはこの機能の副作用?です。

明示的なインターフェイスの実装では、インスタンスをそれぞれのインターフェイスにキャストしない限りそれぞれのメソッドにアクセスできないという制約が発生します。当然ですね、別々の実装があるのですから、呼び出し時にはそれを絞り込むための制約が必要です。
これを活用することで、別にシグネチャが重複していなくても、IListやICollectionなどの非ジェネリックメソッドを積極的には呼び出して欲しくないときに、非ジェネリックインターフェイスを明示的に実装することで、呼び出し側も非ジェネリックインターフェイスにキャストしないと呼び出せなくすることができます。
例えば、List<T>が非ジェネリックのIListも実装しているにもかかわらず、Visual StudioのIntelliSenseに非ジェネリックメソッドが出てこないのはこういうことだったのですね。

ちなみに、明示的なインターフェイスの実装ではアクセス修飾子(publicなど)は付けられません。明示的なインターフェイスの実装である時点でインターフェイスの実装だというのがわかっているので自動的にpublicになるようです。
通常、クラスではアクセス修飾子を省略するとprivateになるのでちょっと違和感はありますね。

話がだいぶそれてしまいましたが、この点を修正したListView Extensions 1.0.1をよろしくお願いします。

2018年2月25日日曜日

MVVMにおけるViewModelとModelのプロパティ同期 - NotifyPropertyHelper 1.0.0-beta1

MVVMスタイルでWPFアプリケーションなどを作っていると、ViewModelとModelにやたら同じプロパティが出てきます。

それもそのはず、ViewModelはViewの実装上の制約を吸収する層なので、画面の状態(表示内容など)自体はModelもプロパティとして持っています。そうでなければMVVMではありません。詳しくは以前書いた記事を読んでください。
MVVMとは何か
ですが、その通りに実装すると同じ名前のプロパティがViewModelとModelで大量に出てきてしまうという問題もありました。
MVVMのサンプルプログラム - TwitterViewer  
こちらのページで紹介しているサンプルプログラムは基本的にはTwitterからツイートを持ってきて画面に表示するだけですので、ModelのプロパティもGet-Onlyのプロパティばかりです。ですが、もっと動的に内容が動くような状況の場合は、もっとしっかりViewModelにModelの変更通知を受けてプロパティをコピーするコードや、逆にViewの操作で変化したプロパティをModelにコピーするコードを書かなければなりません。

問題提起

サンプルプログラム

例えば、指定したURLのHTMLを取得するアプリケーションを作るとします。

このアプリケーションはURLを入力しGoボタンを押すとそのサイトのHTMLを取得して画面に表示するシンプルなものです。これをMVVMで作ると次のようなコードになります。

【View】
<Window x:Class="Frontend.Views.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
        xmlns:ei="http://schemas.microsoft.com/expression/2010/interactions"
        xmlns:l="http://schemas.livet-mvvm.net/2011/wpf"
        xmlns:v="clr-namespace:Frontend.Views"
        xmlns:vm="clr-namespace:Frontend.ViewModels"
        Title="MainWindow" Height="350" Width="525">
    
    <Window.DataContext>
        <vm:MainWindowViewModel/>
    </Window.DataContext>

    <i:Interaction.Triggers>
        <!--WindowのContentRenderedイベントのタイミングでViewModelのInitializeメソッドが呼ばれます-->
        <i:EventTrigger EventName="ContentRendered">
            <l:LivetCallMethodAction MethodTarget="{Binding}" MethodName="Initialize"/>
        </i:EventTrigger>

        <!--Windowが閉じたタイミングでViewModelのDisposeメソッドが呼ばれます-->
        <i:EventTrigger EventName="Closed">
            <l:DataContextDisposeAction/>
        </i:EventTrigger>

        <!--WindowのCloseキャンセル処理に対応する場合は、WindowCloseCancelBehaviorの使用を検討してください-->

    </i:Interaction.Triggers>
    
    <Grid>
        <Grid>
            <Grid.RowDefinitions>
                <RowDefinition Height="Auto" />
                <RowDefinition Height="1*" />
            </Grid.RowDefinitions>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="Auto" />
                <ColumnDefinition Width="1*" />
                <ColumnDefinition Width="Auto" />
            </Grid.ColumnDefinitions>
            <TextBlock Grid.Row="0" Grid.Column="0" Text="URL: " VerticalAlignment="Center" />
            <TextBox Grid.Row="0" Grid.Column="1" Text="{Binding Url, UpdateSourceTrigger=PropertyChanged}" />
            <Button Grid.Row="0" Grid.Column="2" Content="Go" Width="50" Command="{Binding GoCommand}" />
            <TextBox Grid.Row="1" Grid.ColumnSpan="3" IsReadOnly="True" Text="{Binding Html}"
                     ScrollViewer.HorizontalScrollBarVisibility="Visible" ScrollViewer.VerticalScrollBarVisibility="Visible" />
        </Grid>
    </Grid>
</Window>

【ViewModel】
public class MainWindowViewModel : ViewModel
{
    Model model;

    public void Initialize()
    {
        model = Model.GetInstance();

        GoCommand = new ViewModelCommand(() => model.Go(), () => !string.IsNullOrEmpty(Url));
        GoCommand.RaiseCanExecuteChanged();

        this.PropertyChanged += This_PropertyChanged;
        model.PropertyChanged += Model_PropertyChanged;

        this.Url = model.Url;
        this.Html = model.Html;
    }

    private void This_PropertyChanged(object sender, PropertyChangedEventArgs e)
    {
        switch(e.PropertyName) {
            case nameof(Url):
                model.Url = this.Url;
                GoCommand.RaiseCanExecuteChanged();
                break;
        }
    }

    private void Model_PropertyChanged(object sender, PropertyChangedEventArgs e)
    {
        switch(e.PropertyName) {
            case nameof(Model.Html):
                this.Html = model.Html;
                break;
        }
    }

    #region Url変更通知プロパティ

    public string Url
    {
        get { return _Url; }
        set
        {
            if(_Url == value)
                return;
            _Url = value;
            RaisePropertyChanged(nameof(Url));
        }
    }
    private string _Url;

    #endregion

    #region Html変更通知プロパティ

    public string Html
    {
        get { return _Html; }
        set
        {
            if(_Html == value)
                return;
            _Html = value;
            RaisePropertyChanged(nameof(Html));
        }
    }
    private string _Html;

    #endregion

    #region GoCommand変更通知プロパティ

    public ViewModelCommand GoCommand
    {
        get { return _GoCommand; }
        set
        {
            if(_GoCommand == value)
                return;
            _GoCommand = value;
            RaisePropertyChanged(nameof(GoCommand));
        }
    }
    private ViewModelCommand _GoCommand;

    #endregion

    protected override void Dispose(bool disposing)
    {
        if(disposing) {
            this.PropertyChanged -= This_PropertyChanged;
            model.PropertyChanged -= Model_PropertyChanged;
        }

        base.Dispose(disposing);
    }
}

【Model】
public class Model : NotificationObject
{
    #region Singleton

    static Model Instance;
    public static Model GetInstance()
    {
        if(Instance == null)
            Instance = new Model();
        return Instance;
    }

    #endregion

    private Model()
    {
        Url = @"https://www.google.co.jp/";
    }

    #region Url変更通知プロパティ

    public string Url
    {
        get { return _Url; }
        set
        {
            if(_Url == value)
                return;
            _Url = value;
            RaisePropertyChanged(nameof(Url));
        }
    }
    private string _Url;

    #endregion

    #region Html変更通知プロパティ

    public string Html
    {
        get { return _Html; }
        set
        {
            if(_Html == value)
                return;
            _Html = value;
            RaisePropertyChanged(nameof(Html));
        }
    }
    private string _Html;

    #endregion

    public async void Go()
    {
        using(WebClient wc = new WebClient()) {
            try {
                Html = await wc.DownloadStringTaskAsync(Url);
            }
            catch(Exception e) {
                Html = e.Message;
            }
        }
    }
}
例によってLivetを使っています。

大まかな流れとしては

ViewでURLを入力→ViewModel→Modelへ入力値が伝搬する

ViewでGoボタンを押す→ViewModel→Modelへメソッドの呼び出しが伝搬する

ModelがGoメソッド内でUrlプロパティの値を使ってHTMLをダウンロードし、Htmlプロパティにセットする

ModelがHtmlプロパティが変化したことをイベントで通知する

Model→ViewModel→ViewへHtmlプロパティが伝搬する。この時、ViewModelはViewの制約である「単一スレッドでしか動作をしない」を受け、UIスレッドでHtmlプロパティの値を更新する。

ViewにHTMLが表示される

となります。まさに厳密なMVVMです。ViewModelはViewの実装上の制約を吸収するためのみの仕事に徹し、ModelはViewの実装上の制約などは何も気にせずのびのびと.NETの機能を使っています。

さて、ですが実際にこのコードを書いていると、問題点も見えてきます。

ViewModelとModelのプロパティ同期関係のコードが散在する

当然、ModelとViewModelを同期するのはViewModelの仕事となるわけですが、この同期作業は割と面倒です。
やることとしては、
  1. 初期化時に値を同期する作業
  2. Modelのプロパティ変化時にViewModelのプロパティを更新する作業
  3. ViewModelのプロパティ変化時にModelのプロパティを更新する作業
  4. View消滅時にModelの監視をやめる作業
の3つになります。
実際に書いたコードがこんな感じになります。

この画像を見れば一目瞭然です。あちこちにプロパティ同期のコードが散らばっています。
ViewModelの仕事がプロパティ同期だけならまだいいのですが、他にもCommandの管理など、ModelとViewを繋ぐ仕事は沢山あります。そうすると、それぞれの仕事があちこちに散在してとても見にくいプログラムになりますし、あとからプロパティを増やすときなんかは変更忘れの原因にもなってしまいます。

正直言ってこんなのやってられません。何かもっと簡単に同期作業ができるライブラリが必要です。
ですが、たいていのMVVMライブラリの関心ごとは「ViewとViewModelをいかにして連携させるか」です。ViewModelとModelは通常のC#の言語機能を用いて連携できますから、勝手にやってくださいどうぞというスタンスになってしまうのもある程度しかたのないことなのでしょうが…。

NotifyPropertyHelper

さて、というわけで、ModelとViewModelの連携に的を絞ったライブラリを書いてみました。

1. プロパティの同期

流れとしては、
  1. 同期元の同期したいプロパティにPropertySync属性を付ける
  2. PropertySyncServiceで同期元と同期先のクラスを指定する
  3. 同期元がDisposeされたときにPropertySyncServiceをDisposeする
だけになります。あまり変わらないじゃん!って思うかもしれませんが、プロパティを追加したときに、プロパティの追加と、その追加したプロパティに属性を追加するだけですものは結構でかいです。

上記のViewModelを書き換えてみます。

public class MainWindowViewModel : ViewModel
{
    Model model;
    PropertySyncService propsync;

    public void Initialize()
    {
        model = Model.GetInstance();

        GoCommand = new ViewModelCommand(() => model.Go(), () => !string.IsNullOrEmpty(Url));
        GoCommand.RaiseCanExecuteChanged();

        this.PropertyChanged += This_PropertyChanged;

        propsync = new PropertySyncService(this, model);
    }

    private void This_PropertyChanged(object sender, PropertyChangedEventArgs e)
    {
        switch(e.PropertyName) {
            case nameof(Url):
                GoCommand.RaiseCanExecuteChanged();
                break;
        }
    }

    #region Url変更通知プロパティ

    [PropertySync(PropertySyncMode.TwoWay, Direction.TargetToSource)]
    public string Url
    {
        get { return _Url; }
        set
        {
            if(_Url == value)
                return;
            _Url = value;
            RaisePropertyChanged(nameof(Url));
        }
    }
    private string _Url;

    #endregion

    #region Html変更通知プロパティ

    [PropertySync(PropertySyncMode.OneWayToSource, Direction.TargetToSource)]
    public string Html
    {
        get { return _Html; }
        set
        {
            if(_Html == value)
                return;
            _Html = value;
            RaisePropertyChanged(nameof(Html));
        }
    }
    private string _Html;

    #endregion

    #region GoCommand変更通知プロパティ
    private ViewModelCommand _GoCommand;

    public ViewModelCommand GoCommand
    {
        get
        { return _GoCommand; }
        set
        { 
            if(_GoCommand == value)
                return;
            _GoCommand = value;
            RaisePropertyChanged(nameof(GoCommand));
        }
    }
    #endregion

    protected override void Dispose(bool disposing)
    {
        if(disposing) {
            propsync.Dispose();
        }

        base.Dispose(disposing);
    }
}
同期したいプロパティにはPropertySyncという属性を付加します。
public PropertySyncAttribute(PropertySyncMode Mode, Direction InitializeCopyDirection, Type PropertyConverter = null);
public PropertySyncAttribute(string TargetPropertyName, PropertySyncMode Mode, Direction InitializeCopyDirection, Type PropertyConverter = null);
PropertySync属性はこのような2つのコンストラクタを持っています。前者はTargetPropertyNameが省略されていますが、省略された場合は同名のプロパティを当たります。
PropertySyncModeは同期方向で、InitializeCopyDirectionが初期化時(PropertySyncServiceインスタンス生成時)にコピーをする方向です。注意点としては、同期元、同期先ともに、これらの同期方向に支障が無いようなアクセサビリティ・型のプロパティにする必要があるということです。
PropertyConverterはプロパティを変換するクラスです。後述します。

後は、クラスの生成時(コンストラクタ等)でPropertySyncServiceのインスタンスを生成し、そこで同期元と同期先のインスタンスを設定してあげます。不要になったらDisposeを呼び出すことで、同期処理を打ち切ります。

2. 型の違うプロパティの同期

ときどき型の違うプロパティを同期したくなります。
例えば、ModelではReadOnlyObservableCollectionだけど、ViewModelはUIスレッドに同期したReadOnlyObservableCollectionにしたいときなどです。
そのようなときは、IPropertyConverter<TSource, TTarget>を継承した変換クラスを作り、PropertySync属性のコンストラクタに指定してあげるとそれが実現できます。

例えば、ListViewExtensionsではModel用にSortableObservableCollection、ViewModel用にListViewViewModelを用意していますが、それらを変換するには次のようなコードを書けばよいでしょう。
class PersonListViewConverter : IPropertyConverter<ListViewViewModel<PersonViewModel, Person>, SortableObservableCollection<Person>>
{
    public ListViewViewModel<PersonViewModel, Person> ConvertToSource(ListViewViewModel<PersonViewModel, Person> OldSourceValue, SortableObservableCollection<Person> NewTargetValue)
    {
        if(OldSourceValue != null)
            OldSourceValue.Dispose();

        if(NewTargetValue == null)
            return null;
        else
            return new ListViewViewModel<PersonViewModel, Person>(NewTargetValue, person => new PersonViewModel(person), DispatcherHelper.UIDispatcher);
    }

    public SortableObservableCollection<Person> ConvertToTarget(SortableObservableCollection<Person> OldTargeteValue, ListViewViewModel<PersonViewModel, Person> NewSourceValue)
    {
        throw new InvalidOperationException("Not defined.");
    }
}
2つのConvertToSourceとConvertToTargetの2つのメソッドは古い値も引き渡してくれるため、Disposeが必要なインスタンスに対しても適切な措置を取ることができます。

3.  ReadOnlySynchronizationContextCollection

さて、今回のブログ記事のテーマとはちょっと違うのですが、ViewModelとModelの連携を取るためのライブラリということで、このようなコレクションも用意しています。

WPFではUIが単一スレッドからしかアクセスできません。そのような制約の吸収はViewModelがやることです。外から変更させる気が無いリストの場合は、ModelがReadOnlyCollectionでリストを公開し、それをViewModelがUIのスレッドに合わせながらUIに橋渡ししてあげる必要があります。それをやってくれるのがReadOnlySyncronizationContextCollectionです。

使い方は使ってみればわかると思います(雑)。

ライセンス

以下の各項目をお守りください
  • このライブラリを利用する方は自己責任でお願いします。いかなる問題が起きても作者は責任を負いません。
  • このソフトを悪用しないでください。
  • このソフトウェアを無断で単体での転載、再配布しないでください。ただし、このライブラリを参照しているソフトウェアと一緒に配布する場合を除きます。
  • 作者は使用方法やバグに関するサポートをする義務を負いません。
  • 有償アプリケーションには使用してはならない。
  • 完成したソフトウェアのどこか(ヘルプ、バージョン情報など)と、ReadMeなどのドキュメンテーションに私のライブラリを使用したことを明記すること。ただし、作者(私)がこのライブラリを自分のソフトで使用するときはその限りではない。

公開

Nugetにて公開しています。
NotifyPropertyHelper - Nuget
なお、 プレリリース版ですので、VisualStudioからの検索時にはプレリリースもヒットするオプションを指定するようにしてください。

2018年2月20日火曜日

WPFにおけるINotifyDataErrorInfoを使った値の検証 属性版

2年ちょっと前にINotifyDataErrorInfoを使った値の検証の方法を記事にしました。

これはViewModelを継承したクラスにエラー情報を蓄積する機能を持たせ、任意のタイミングでエラーを登録したり削除したりすることができるというものでした。
実際、それでも使い物にはなるのですが、しっかりとMVVMを実装しようとすると妙に不便に思えてきました。

簡単な値の検証(例えばnullじゃなければ受け付ける、など)ならまだしも、複雑な判定ロジックになるとModel側にその判定機能を持たせたくなります。なぜならば、入力値を受けて何かしらの処理を行うのはModelの仕事だからです。何かしらの処理を行う前には、渡されたパラメーターが正しいものかどうかを判定し、間違っていれば何かしらのエラー通知(例外を吐く等々)をするかと思いますが、値の検証をViewModelでやっていたとすると、ViewModelとModelの両方に複雑な判定ロジックを持たせるメリットがありません。
入力された値の正当性も状態の1つだと考えると、Modelがその状態を持っていても変ではありません。すなわち、ViewModelは入力された値をそのままModelに横流ししておき、Modelがその横流しされた値の正当性を検証して状態として公開し、それをViewModelが監視してUIに何かしらの表示を行うといった流れになれば、正当性の判断をすべてModelがすることができますし、ViewModelもそれを監視して入力欄のエラー表示やボタンの有効/無効などを切り替えればいいわけです。

そうすると、前回の記事で書いたクラスでは多少の不満が出てきます。ViewModelがModelの正当性を表すプロパティを監視し、そのプロパティの変化に合わせてエラーをセットするロジックをViewModelに書くことになります。それ自体はMVVMの考え方には反しないのですが、「あるプロパティの値の正当性を他のとあるプロパティが示している」というシンプルな関係なのに、なぜわざわざプロパティの変更通知を購読し、値が変化したタイミングでメソッドを呼び出して…なんて面倒くさい処理を書かなきゃいけないのかと。実際書いてみると、誰もが「もっとシンプルにプロパティの関係性を書きたい!」と思うはずです。

そこで、そのような機能を実現するViewModelを作ってみました。
public abstract class NotifyDataErrorViewModel : ViewModel, INotifyDataErrorInfo
{
    Dictionary<string, NotifyErrorAttribute[]> properties;

    public NotifyDataErrorViewModel()
    {
        properties = this
            .GetType()
            .GetProperties()
            .Select(p => new KeyValuePair<string, NotifyErrorAttribute[]>(
                p.Name,
                p.GetCustomAttributes(typeof(NotifyErrorAttribute), true)
                    .Cast<NotifyErrorAttribute>()
                    .Where(p1 => p1.IsValidProperty(this))
                    .ToArray()))
            .Where(p => p.Value.Length > 0)
            .ToDictionary(p => p.Key, p => p.Value);

        this.PropertyChanged += This_PropertyChanged;

        foreach(var p in properties.Where(p => p.Value.Any(p1 => !p1.GetValidityValue(this))).Select(p => p.Key))
            RaiseErrorsChanged(p);
    }

    private void This_PropertyChanged(object sender, PropertyChangedEventArgs e)
    {
        foreach(var p in properties.Where(p => p.Value.Any(p1 => p1.ValidityPropertyName == e.PropertyName)))
            RaiseErrorsChanged(p.Key);
    }

    /// <summary>
    /// エラーがあるかどうかを示すプロパティ
    /// </summary>
    public bool HasErrors => properties.Values.SelectMany(p => p).Any(p => !p.GetValidityValue(this));

    /// <summary>
    /// エラー情報を返すメソッド
    /// </summary>
    /// <param name="propertyName">エラーがあるか調べたいプロパティ名</param>
    /// <returns>エラーがあればエラーテキストの配列、無ければ空の配列</returns>
    public IEnumerable GetErrors(string propertyName)
    {
        if(properties.ContainsKey(propertyName))
            return properties[propertyName].Where(p => !p.GetValidityValue(this)).Select(p => p.ErrorText).ToArray();
        else
            return Enumerable.Empty<string>();
    }

    /// <summary>
    /// エラー状態が変わった時に発生するイベント
    /// </summary>
    public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;

    /// <summary>
    /// ErrorsChangedイベントを発生させるメソッド
    /// </summary>
    /// <param name="PropertyName">プロパティ名</param>
    protected void RaiseErrorsChanged(string PropertyName)
    {
        ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(PropertyName));
    }
}
ViewModelを継承した抽象クラスで、INotifyDataErrorInfoを実装しています。
ここまでは前回と同じなのですが、エラーを登録/削除するメソッドは用意していません。さて、ではどうやってエラーの登録/削除をするのでしょう。

その答えは属性です。
public class NotifyErrorAttribute : Attribute
{
    public NotifyErrorAttribute(string ValidityPropertyName, string ErrorText)
    {
        this.ValidityPropertyName = string.IsNullOrEmpty(ValidityPropertyName) ? throw new ArgumentNullException(nameof(ValidityPropertyName)) : ValidityPropertyName;
        this.ErrorText = string.IsNullOrEmpty(ErrorText) ? throw new ArgumentNullException(nameof(ErrorText)) : ErrorText;
    }

    public string ValidityPropertyName { get; }

    public string ErrorText { get; }

    /// <summary>
    /// この属性で与えられたプロパティ名が、指定したオブジェクトで値の正当性を示しているプロパティとして正当かを検証します。
    /// </summary>
    /// <param name="Source">オブジェクト</param>
    /// <returns>指定したオブジェクトがbool型の読み取り可能なこの名前のプロパティを持っていた場合true、そうでなければfalse</returns>
    internal bool IsValidProperty(object Source)
    {
        var property = Source.GetType().GetProperty(ValidityPropertyName);
        return property != null && property.CanRead && property.PropertyType == typeof(bool);
    }

    /// <summary>
    /// 値の正当性を取得します。
    /// </summary>
    /// <param name="Source">オブジェクト</param>
    /// <returns>値の正当性</returns>
    internal bool GetValidityValue(object Source)
    {
        return (bool)Source.GetType().GetProperty(ValidityPropertyName).GetValue(Source);
    }
値の正当性情報を付加したいViewModelのプロパティにこの属性を付け、値の正当性を示しているプロパティの名前とエラーが起きたときのメッセージを渡してあげます。そうすることで、NotifyDataErrorViewModelクラスは値の正当性を示しているプロパティを監視して、エラー状態が変化すると自動的にErrorsChangedイベントを発生させます。
NotifyErrorAttributeを1つのプロパティに複数付けた場合、1つでもエラーが起こるとエラーとみなします。GetErrorsメソッドが返すエラーメッセージもそのエラーの内容によって変わるので、エラーになる条件が複数ある場合なんかは属性をいくつか付けるといいでしょう。


さて、せっかくですので以前の記事の時に作ったサンプルプログラムと同じような動きをするサンプルプログラムを今回のクラスを使って作ってみました。
Modelは次の通りです。
public class Model : NotificationObject
{
    #region Singleton

    static Model Instance;

    public static Model GetInstance()
    {
        if(Instance == null)
            Instance = new Model();
        return Instance;
    }

    #endregion

    private Model()
    {
        this.PropertyChanged += This_PropertyChanged;

        CheckValidity();
    }

    private void This_PropertyChanged(object sender, PropertyChangedEventArgs e)
    {
        switch(e.PropertyName) {
            case nameof(Number):
            case nameof(Digits):
                CheckValidity();
                break;
        }
    }

    void CheckValidity()
    {
        NumberIsANumber = int.TryParse(Number, out int number);
        DigitsIsANumber = int.TryParse(Digits, out int digits);

        if(NumberIsANumber && DigitsIsANumber) {
            if(number == 0)
                NumberHasTheDigits = digits == 1;
            else
                NumberHasTheDigits = (int)Math.Log10(Math.Abs(number)) + 1 == digits;
        } else
            NumberHasTheDigits = true;
    }

    #region Number変更通知プロパティ

    public string Number
    {
        get { return _Number; }
        set
        {
            if(_Number == value)
                return;
            _Number = value;
            RaisePropertyChanged(nameof(Number));
        }
    }
    private string _Number;

    #endregion

    #region Digits変更通知プロパティ

    public string Digits
    {
        get { return _Digits; }
        set
        {
            if(_Digits == value)
                return;
            _Digits = value;
            RaisePropertyChanged(nameof(Digits));
        }
    }
    private string _Digits;

    #endregion

    #region NumberHasTheDigits変更通知プロパティ

    public bool NumberHasTheDigits
    {
        get { return _NumberHasTheDigits; }
        set
        {
            if(_NumberHasTheDigits == value)
                return;
            _NumberHasTheDigits = value;
            RaisePropertyChanged(nameof(NumberHasTheDigits));
        }
    }
    private bool _NumberHasTheDigits;

    #endregion

    #region DigitsIsANumber変更通知プロパティ

    public bool DigitsIsANumber
    {
        get { return _DigitsIsANumber; }
        set
        {
            if(_DigitsIsANumber == value)
                return;
            _DigitsIsANumber = value;
            RaisePropertyChanged(nameof(DigitsIsANumber));
        }
    }
    private bool _DigitsIsANumber;

    #endregion

    #region NumberIsANumber変更通知プロパティ

    public bool NumberIsANumber
    {
        get { return _NumberIsANumber; }
        set
        {
            if(_NumberIsANumber == value)
                return;
            _NumberIsANumber = value;
            RaisePropertyChanged(nameof(NumberIsANumber));
        }
    }
    private bool _NumberIsANumber;

    #endregion
}
NumberとDigitsというプロパティを持っており、これらの値が変更されるたびにCheckValidityメソッドを呼び出しています。ここからNumberHasTheDigitsプロパティ、DigitsIsANumberプロパティ、NumberIsANumberプロパティの3つを変更しています。NumberIsANumberはなんか哲学的な名前になってしまっていますが、xxxxIsANumberのプロパティがxxxxのプロパティが値であるかどうか、NumberHasTheDigitsはNumberがDigitsの桁数を持っているかを判定するフラグです。ただ、NumberとDigitsのどちらかのみが値だった場合にfalseにしてしまうと芸がない(というか、xxxxIsANumberプロパティの存在価値がなくなる)ので、片側が入力されている状態ではtrueにしてみました。

肝心のViewModelはこちらです。
public class MainWindowViewModel : NotifyDataErrorViewModel
{
    Model model;

    public void Initialize()
    {
        model = Model.GetInstance();

        this.PropertyChanged += This_PropertyChanged;
        model.PropertyChanged += Model_PropertyChanged;
    }

    private void This_PropertyChanged(object sender, PropertyChangedEventArgs e)
    {
        switch(e.PropertyName) {
            case nameof(Number):
                model.Number = Number;
                break;
            case nameof(Digits):
                model.Digits = Digits;
                break;
            case nameof(NumberHasTheDigits):
                model.NumberHasTheDigits = NumberHasTheDigits;
                break;
            case nameof(DigitsIsANumber):
                model.DigitsIsANumber = DigitsIsANumber;
                break;
            case nameof(NumberIsANumber):
                model.NumberIsANumber = NumberIsANumber;
                break;
        }
    }

    private void Model_PropertyChanged(object sender, PropertyChangedEventArgs e)
    {
        switch(e.PropertyName) {
            case nameof(Model.Number):
                Number = model.Number;
                break;
            case nameof(Model.Digits):
                Digits = model.Digits;
                break;
            case nameof(Model.NumberHasTheDigits):
                NumberHasTheDigits = model.NumberHasTheDigits;
                break;
            case nameof(Model.DigitsIsANumber):
                DigitsIsANumber = model.DigitsIsANumber;
                break;
            case nameof(Model.NumberIsANumber):
                NumberIsANumber = model.NumberIsANumber;
                break;
        }
    }

    #region Number変更通知プロパティ

    [NotifyError(nameof(NumberHasTheDigits), "The Number does not match to the Digits.")]
    [NotifyError(nameof(NumberIsANumber), "The Number is not a number.")]
    public string Number
    {
        get { return _Number; }
        set
        {
            if(_Number == value)
                return;
            _Number = value;
            RaisePropertyChanged(nameof(Number));
        }
    }
    private string _Number;

    #endregion

    #region Digits変更通知プロパティ

    [NotifyError(nameof(NumberHasTheDigits), "The Digits does not match to the Number.")]
    [NotifyError(nameof(DigitsIsANumber), "The Digits is not a number.")]
    public string Digits
    {
        get { return _Digits; }
        set
        {
            if(_Digits == value)
                return;
            _Digits = value;
            RaisePropertyChanged(nameof(Digits));
        }
    }
    private string _Digits;

    #endregion

    #region NumberHasTheDigits変更通知プロパティ

    public bool NumberHasTheDigits
    {
        get { return _NumberHasTheDigits; }
        set
        {
            if(_NumberHasTheDigits == value)
                return;
            _NumberHasTheDigits = value;
            RaisePropertyChanged(nameof(NumberHasTheDigits));
        }
    }
    private bool _NumberHasTheDigits;

    #endregion

    #region DigitsIsANumber変更通知プロパティ

    public bool DigitsIsANumber
    {
        get { return _DigitsIsANumber; }
        set
        {
            if(_DigitsIsANumber == value)
                return;
            _DigitsIsANumber = value;
            RaisePropertyChanged(nameof(DigitsIsANumber));
        }
    }
    private bool _DigitsIsANumber;

    #endregion

    #region NumberIsANumber変更通知プロパティ

    public bool NumberIsANumber
    {
        get { return _NumberIsANumber; }
        set
        {
            if(_NumberIsANumber == value)
                return;
            _NumberIsANumber = value;
            RaisePropertyChanged(nameof(NumberIsANumber));
        }
    }
    private bool _NumberIsANumber;

    #endregion
}
前半にあるのは自身とModel両方の値の変化をウォッチして同期するプログラムです。
NumberとDigitsにはNotifyError属性を付けて、どのプロパティが自身の正当性を示しているかを記述しています。これだけでコントロールに正当性表示が行われるなんて夢の酔うでしょ?

ん?DataAnnotations
…知らね…そんなの…。
(マジレスするとDataAnnotationsはCustomValidationAttributeが任意のメソッドを判定に指定できてしまうため、INotifyPropertyChangedの監視とリフレクションだけじゃ対応しきれずここまでスマートな実装はできないのです…。また、Modelでの値の検証を反映するという考えからも外れてしまいます…。)