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での値の検証を反映するという考えからも外れてしまいます…。)

2018年1月24日水曜日

MPLAB Xでアセンブリリストを見る

私が初めてPICを触ったころ、それは10年以上前になりますが、その時はまだCコンパイラは数百ドルする有料版しかありませんでした。当時中学生だった私は、当然そのような高価なものは買えるわけもなく、一生懸命PICのアセンブリを覚えてプログラムを書いていました。

時代は変わり、PIC以外にもたくさんのマイコンが趣味レベルで使われるようになり、開発環境もC言語が主流になりました。PICも一足遅れて(実用レベルの)Cコンパイラを無償公開し、今ではMicrochipが買収したHi-Tech社のCコンパイラがXCコンパイラとしてPICの基幹コンパイラになっています。

今ではC言語向けに間接参照などの機能を強化したPICがたくさん出てきており、アセンブリで開発する理由は皆無ですが、それでもときどき「このコードはどのような形でアセンブリに展開されるのだろうか」と気になることがあります。

例えばEEPROMアクセス。
EEPROMは、不意な書き込みを防止するため、アンロックシーケンスと呼ばれる一定の手順で命令を実行しないと書き込みできないようになっています。
        BCF     INTCON, GIE
        MOVLW   55h
        MOVWF   EECON2
        MOVLW   AAh
        MOVWF   EECON2
        BSF     EECON1, WR
        BSF     INTCON, GIE
これが代表的なPIC16シリーズにおけるEEPROM書き込みシーケンスの一部です。EECON2には0x55を書き込んだ2サイクル後に0xAAを書き込み、さらに次のサイクルでEECON1のWRビットをセットしなければいけないため、C言語で書いていてもこの部分だけはインラインアセンブリで書かなければならなくなることがあります。
ですが、C言語で書いても次のように書けば上記のアセンブリに展開されることは容易に想像が付きます。
INTCONbits.GIE = 0;
EECON2 = 0x55;
EECON2 = 0xAA;
EECON1bits.WR = 1;
INTCONbits.GIE = 1;
でもまあ、C言語をどうアセンブリに展開するかはコンパイラの勝手なので、こういう書き方をすれば絶対に展開されるとは限りません。こういう時にどのような形でアセンブリ展開されるか見てみたくなりますよね。

アセンブリリストは…っとその前に、まずはお約束の環境を書いておきます。
  • MPLAB X IDE v4.05 
  • XC8 v1.45

アセンブリリストはメニューの
Window → Debugging → Output → Disassembly Listing File
で開くことができます。
ですがおそらく、最初は次のようなファイルが開かれて、アセンブリリストは出てこないはずです。

悩むほど難しい英語ではないですが、一応拙訳を付けておきます。
アセンブリリストの生成は無効になっています。
下記の情報に従ってプロジェクトをビルドしてください。
  1. プロジェクトのプロパティを開いてください。
  2. 「Conf: [現在の設定]」ノードの下にある「Loading」を選択してください。
  3. 「Load Symbols when Programming or building for production」にチェックを入れてください。
このエディタ上でダブルクリックをすると上記の設定ができます。
最後の1行にしれっと書いてありますが、エディタ上をダブルクリックすればワンタッチでダイアログの当該部分が表示される超便利機能があります。
上のほうにある「Load Symbols when Programming or building for production」にチェックを入れて、OKを押してプロジェクトをリビルドしましょう。
ご覧の通り、ばっちりアセンブリリストが表示されました。

実はXC8には組み込み関数でeeprom_writeとeeprom_readというのがあるので、直接レジスタを叩かなくてもEEPROMの読み書きはできるのですが、その部分のコードも展開されていることが分かります。
PICの違いによるレジスタ名の違いで、EECONxではなくNVMCONxになっていますが、5607行目からアンロックシーケンスが始まっていることが分かります。
書き込みを開始してから完了までの間は割り込みを禁止したままなんですね。CPUは動作しているのでそのようにする理由もあまり無い気もしますが…(割り込み中にEEPROMを読み出すのなら話は別ですが)。

2018年1月17日水曜日

LivetにおけるWPFでのダイアログ表示のいろは

いっつもLivetを使っているはずなのに、いろいろなクラスの名前をすっかり忘れてしまってその都度ググることになってしまっているので、備忘録的にまとめます。

基本

TriggerAction

画面遷移は基本的にTriggerActionを使います。その名の通り、何かしらの引き金によって発動するアクションです。
TriggerActionを発動させる方法はいくつかありますが、イベントで発動させる方法とViewModelから発動させる方法を覚えておけば困らないでしょう。前者はEventTrigger(System.Windows.Interactivityの機能)、後者はMessenger(Livetの機能)を使います。

EventTrigger

こちらは、例えばダイアログ表示ではなくても、Livetのメソッド直接バインディング機能などを使うときに多用することになるでしょう。
<Button Content="Push me" VerticalAlignment="Center" HorizontalAlignment="Center" Width="100" Height="30" >
    <i:Interaction.Triggers>
        <i:EventTrigger EventName="Click">
            <l:LivetCallMethodAction MethodTarget="{Binding}" MethodName="ButtonPushed" />
        </i:EventTrigger>
    </i:Interaction.Triggers>
</Button>
例えばこんなやつです。ボタンを押すとEventTriggerでClickイベントを拾い、LivetCallMethodActionを発動させるコードです。基本的にレイアウトなどの動きが無いコードになりがちなマークアップ言語に一気に彩りが生まれる記法ですので是非押さえておいてください。

Messenger

LivetにはMessengerという機能があります。ViewModelはMessengerというインスタンスを持っており、MessengerでInteractionMessageクラスのメッセージを送信すると、それをViewで受け取れる(=TriggerActionとして動作する)というものです。
標準のWPFにはこのようにViewModelからViewに何かを働きかける機能がかなり少なく、Livet以外のMVVMライブラリでもたいてい似たような機能は実装されているようです。
<i:Interaction.Triggers>
    <l:InteractionMessageTrigger Messenger="{Binding Messenger}" MessageKey="MethodCaller">
        <l:LivetCallMethodAction MethodTarget="{Binding}" MethodName="MessageRaised" />
    </l:InteractionMessageTrigger>
</i:Interaction.Triggers>
これは上記のEventTriggerの例になぞらえて、代わりにMessengerでLivetCallMethodActionを発動させるプログラムです。EventTriggerがInteractionMessageTriggerに代わっただけですので難なく理解できるかと思います。
ちなみに、ViewModelではこのように呼び出します。
Messenger.Raise(new InteractionMessage("MethodCaller"));
InteractionMessageクラスでキーを指定します。これによって、単一のコントロール内に複数のInteractionMessageTriggerを持っていても使い分けることができます。

InteractionMessage

上記のMessengerの項でも少し出てきましたが、MessengerとはViewModelからViewに情報を受け渡すものです。上記の例ではRaiseしかしていないのでMessengerKeyしか渡しませんでしたが、例えばダイアログボックスならば表示テキストやキャプション、アイコンの種類などを伝える必要があるでしょう。そのような場合は、InteractionMessageを継承した派生クラスを作ってパラメーターを持たせるような仕様になっています。それに対応して、TriggerActionの派生クラスであるInteractionMessageActionを継承した派生クラスも作成し、そこで独自のInteractionMessageを受け取れば良いです。

Messengerの例

というわけで、まずはMessengerを使ってダイアログボックスを表示する例を示します。
<i:Interaction.Triggers>
    <l:InteractionMessageTrigger Messenger="{Binding Messenger}" MessageKey="MessageBox">
        <l:InformationDialogInteractionMessageAction />
    </l:InteractionMessageTrigger>
</i:Interaction.Triggers>
Messenger.Raise(new InformationMessage("Message Text", "Caption", MessageBoxImage.Information, "MessageBox"));
ViewとViewModelはこんな感じになります。
InformataionDialogInteractionMessageActionは単一ボタン(=OKボタンのみ)のメッセージボックスを表示するTriggerActionです。
これに対応するInteractionMessageはInformationMessageで、これにメッセージ内容、キャプション内容、アイコン、メッセージキーを含めてMessengerに乗せることでダイアログボックスを表示させることができます。

EventTriggerの例

ところで、ボタンが押されたらそのままメッセージボックスを表示したくなることがあります。ですが、上記のMessengerの例を見ているとViewModelからViewにメッセージを送る必要があるので、ボタン(View)—(メソッド呼び出し)→Messenger発動(ViewModel)—(Messenger)→ダイアログ表示(View)といった具合にViewとViewModelを往復する必要が出てきてしまいます。これではあまりスマートではありません。

ですがそこは流石Livet、ちゃんと方法が用意されています。
InteractionMessageActionクラスにはDirectInteractionMessageプロパティが用意されており、ここに直接InteractionMessageを渡してあげればいい…と思いきや、ここに渡すのはDirectInteractionMessageクラスです。これについては細かくは後述します。
DirectInteractionMessageにはMessageプロパティがあり、こちらにInteractionMessageを渡せばいいです。
さらに、それぞれのクラスのDirectInteractionMessageプロパティ、MessageプロパティはContentPropertyに設定されているので、プロパティを明示しなくても設定できるのでネストが深くなるのも最小限に抑えることができます。
<Button Content="Push me" VerticalAlignment="Center" HorizontalAlignment="Center" Width="100" Height="30" >
    <i:Interaction.Triggers>
        <i:EventTrigger EventName="Click" >
            <l:InformationDialogInteractionMessageAction>
                <l:DirectInteractionMessage>
                    <l:InformationMessage Caption="Caption" Text="Message text" Image="Information" />
                </l:DirectInteractionMessage>
            </l:InformationDialogInteractionMessageAction>
        </i:EventTrigger>
    </i:Interaction.Triggers>
</Button>
こんな感じです。InformataionDialogInteractionMessageActionに直接InformationMessageを渡すことができています。これによって、View単体で(ViewModelを介在せずに)ダイアログを表示することができます。

ダイアログの戻り値

さて、上記は単一ボタンのダイアログを表示していたので戻り値は特に必要ありませんでした。ですが、例えば「はい」「いいえ」で聞くダイアログなどは戻り値を知る必要があります。

Messengerの例

実はこの戻り値はMessengerが持つようにできています。ですので、Messengerの例は特に上記の例とほぼ同じコードになります。
<i:Interaction.Triggers>
    <l:InteractionMessageTrigger Messenger="{Binding Messenger}" MessageKey="MessageBox">
        <l:ConfirmationDialogInteractionMessageAction />
    </l:InteractionMessageTrigger>
</i:Interaction.Triggers>
var msg = new ConfirmationMessage("Is this a MessageBox?", "Caption", MessageBoxImage.Question, MessageBoxButton.YesNoCancel, "MessageBox");
Messenger.Raise(msg);
if(msg.Response == null) {
    // Cancel clicked
} if(msg.Response.Value) {
    // Yes clicked
} else {
    // No clicked
}
bool?型のResponseプロパティに結果が入るので、Messaenger.Raiseから制御が返ってきた後にそれを見れば問題ありません。

EventTriggerの例

さて、ここでまた問題が発生します。当然、ダイアログの戻り値はソフトウェアの制御側(=ViewModel)で受け取りたいですが、EventTriggerで直接TriggerActionを呼び出してしまった場合、ViewModelは介在しないのでそのMessageを受け取るところがありません。

ですがそこは流石Livet、ちゃんと方法が用意されています。
先ほど無駄に挟んでいたように見えたDirectInteractionMessageクラスですが、実は答えはここにあります。これがCallbackMethodTarget/CallbackMethodNameプロパティ及びCallbackCommandプロパティを持っていて、ここでViewModelのメソッドやコマンドをバインディングすることによってViewModelが戻り値を受け取ることができるようになります。
<Button Content="Push me" VerticalAlignment="Center" HorizontalAlignment="Center" Width="100" Height="30" >
    <i:Interaction.Triggers>
        <i:EventTrigger EventName="Click" >
            <l:ConfirmationDialogInteractionMessageAction>
                <l:DirectInteractionMessage CallbackMethodTarget="{Binding}" CallbackMethodName="MessageBoxClosed">
                    <l:ConfirmationMessage Caption="Is this a MessageBox?" Text="Message text" Image="Question" Button="YesNoCancel" />
                </l:DirectInteractionMessage>
            </l:ConfirmationDialogInteractionMessageAction>
        </i:EventTrigger>
    </i:Interaction.Triggers>
</Button>
public void MessageBoxClosed(ConfirmationMessage msg)
{
    if(msg.Response == null) {
        // Cancel clicked
    }
    if(msg.Response.Value) {
        // Yes clicked
    } else {
        // No clicked
    }
}
ViewModelではDirectInteractionMessageに渡していたInteractionMessageをパラメーターにしたメソッドを用意しておく必要があります。これで、ボタンからメッセージボックスを表示し、その結果をViewModelで受け取ることができます。

クラス名対応表

さて、Messaengerとかはもうわかっているからいいよという人はここだけ見てください。
用途 TriggerAction InteractionMessage 備考
メッセージボックス(単一ボタン) InformationDialogInteractionMessageAction InformationMessage
メッセージボックス(複数ボタン) ConfirmationDialogInteractionMessageAction ConfirmationMessage
ファイルを開くダイアログ OpenFileDialogInteractionMessageAction OpeningFileSelectionMessage
ファイルを保存ダイアログ SaveFileDialogInteractionMessageAction SavingFileSelectionMessage
フォルダ選択ダイアログ FolderBrowserDialogInteractionMessageAction FolderSelectionMessage Livet.Extensionsが必要
任意のウィンドウ TransitionInteractionMessageAction TransitionMessage
Livetが持っているダイアログ表示関係のTriggerAction/InteractionMessageはこれで全部のはずです。ほかにも、Windowを最大化/最小化/閉じるなどを実現するWindowInteractionMessageAction/WindowActionMessageなどもありますが、まあこれは自分で調べて使ってみてください。

最後の任意のウィンドウ表示はなかなかに曲者かと思います。特に、これで開くウィンドウに対応するViewModelをどうしたらいいのか、というところでとても悩むことになるかと思います。
まあその辺はまたの機会に。