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をどうしたらいいのか、というところでとても悩むことになるかと思います。
まあその辺はまたの機会に。

2018年1月13日土曜日

eventをasync/awaitに変換する

今回のテーマはいわゆるEAPからTAPへの変換です。

Webアクセスなど、長い時間がかかることが見込まれる処理では、あるスレッドがそれにつきっきりになるとほかの処理が一切できず、ユーザーからはソフトがハングアップ(フリーズ)しているように見えてしまいます。そのような状態を防ぐために、非同期処理が必要になってきます。

EAP(Event-based Asynchronous Pattern)は、非同期プログラミングにおいて、処理の開始を行うメソッドと、終了時に呼ばれるイベントからなるパターンです。
例えば、WebClientクラスはこのパターンをサポートしており、Webからのダウンロードを非同期で実行することができます。
WebClient wc = new WebClient();

wc.DownloadStringCompleted += (s, e) => {
    Console.WriteLine(e.Result);
};

wc.DownloadStringAsync(new Uri("https://www.google.co.jp/"));
これはGoogleのトップページのソースを画面に表示するプログラムをEAPで実装したものです。イベントを使っているため、ダウンロードを開始する前にダウンロード後の処理を綴ったハンドラをイベントに登録しておき、その後に処理を開始するという書き方をしなければなりません。プログラムが上から下へ流れないので非常に見にくくなってしまいます。

一方で、TAP(Task-based Asynchronous Pattern)はTaskクラス/Task<T>クラスを使用した非同期パターンです。ライブラリとしてはこの説明だけなのですが、C#では言語機能でもこの非同期パターンに追従していて、C#5.0でasync/await構文が導入されました。これは非同期処理を同期処理であるかのような見た目で実行できる構文で、TAPとセットでこの構文を使うことによって非同期処理の記述がかなりすっきりできるようになりました。
WebClientクラスはこのパターンもサポートしており、上記のプログラムはTAPだと次のように書き換えられます。
WebClient wc = new WebClient();

string result = await wc.DownloadStringTaskAsync("https://www.google.co.jp/");
Console.WriteLine(result);
見てくださいこのシンプルさ。文字列をダウンロードして画面に出力する、というシンプルな流れのみを記述していますが、実際ちゃんと非同期で動作します。人類が古くから慣れ親しんでいる同期パターンと同じように非同期なプログラムを記述できる素晴らしい機能です。


さて、復習はこれくらいで本題に入ります。
WebClientのようにTAPに対応してくれているクラスならいいのですが、世の中にはそうではないクラスもたくさんあります。古いライブラリだったり、そもそももうちょっと汎用的なプログラムでEAPと呼ばれるものでは無かったり。

ズバリ、TaskCompletionSource<T>クラスを使います。
static async Task Main(string[] args)
{
    string result = await DownloadStringTaskAsync("https://www.google.co.jp/");
    Console.WriteLine(result);
}

static Task<string> DownloadStringTaskAsync(string url)
{
    var tcs = new TaskCompletionSource<string>();
    var wc = new WebClient();
    wc.DownloadStringCompleted += (sender, e) => {
        if(e.Error != null)
            tcs.TrySetException(e.Error);
        else if(e.Cancelled)
            tcs.TrySetCanceled();
        else
            tcs.TrySetResult(e.Result);

        wc.Dispose();
    };
    wc.DownloadStringAsync(new Uri(url));
    return tcs.Task;
}
結論から言うとこんな感じです。
TaskCompletionSourceクラスにはTaskプロパティがあり、このプロパティをawaitすることで、TaskCompletionSourceの終了待ちをすることができます。
ではどうやったらTaskCompletionSourceが終了するのかというと、TrySetResultが呼ばれたときになります。これを呼び出すことでawaitでの待機が終了し、パラメーターで渡していた値が結果として返されます。
エラーが発生したり、キャンセルが発生したときは、それぞれTrySetExceptionTrySetCanceledを呼び出せばOKです。


余談ですが、私がこれを欲しくなったのは、SerialPortクラスを触っているときでした。
そもそもシリアルポートは単なる全二重通信ですから、本来はWebClientのように「データを要求して、そのレスポンスが返ってくる」という使い方に限定されるようなものではありません。しかし、「何かしらのコマンドを送るとそれに対応したレスポンスがある」のような実装を行うシチュエーションはそれなりにあり、そのような場合ではTAPでクライアントを作ったほうがスッキリします。
というわけで、SerialPortクラスをTAPでラッピングしようとしたときに使ったのがこの手法でした。

2017年11月26日日曜日

ListView Extensions ver.1.0.0リリース

さて、ついに正式リリースしましたListView Extensions ver.1.0.0。

ListView Extensions - Nuget

beta4からはほとんど変わっていないはずです。そのままだと使いにくいWPFのListViewを使いやすくすることを目的に、View、ViewModel、Modelの全領域で便利な機能を提供するライブラリです。

ListView Extensionsの使用例はこんな感じです。


見た目はただのWPFのListViewに見えますが、よくよく見ると「Name」ヘッダーの上部にソートしていることを表す「▲」が表示されています。ListViewと言えばこのように名前、読み、年齢など様々なプロパティを持つデータをソートするときに使い、さらに、ユーザーが好きなプロパティでソートする機能を提供するようなものをイメージするはずです。Windowsのエクスプローラーだってそうですよね。ファイル名でソートしたり、更新日時でソートしたり、ファイルの種類でソートしたりすることがあるはずです。
ただ、WPFは標準でそのような機能をサポートしていません。自分でそれを実装しようとするととてつもなく手間がかかりますので、それをやってくれるライブラリが欲しくなります。そこで生まれたのがListView Extensionsというわけです。

せっかく正式リリースにしたので使い方をなぞってみます。

1. 使い方

実はbeta-1をリリースしたときにもある程度書いてあります。


ViewにはWPF標準のListView、ViewModelにはListViewViewModel、ModelにはSortableObservableCollectionを使うのが基本になります。それ以外の使い方はこのライブラリは想定していません。
SortableObservableCollectionは名前の通りソート可能コレクションです。必ずしもソートされているとは限らず、Sortメソッドを呼び出した時点でその時に指定したプロパティ名でソートします。いかにもListViewのソートに沿った機能ですね。

さて、Viewにはあらゆる機能が凝縮されていますので、これを見るのが手っ取り早いです。
まずはXAMLにListViewViewModelの名前空間を追加しておきましょう。
xmlns:lv="http://schemas.eh500-kintarou.com/ListViewExtensions"
Viewに対応するXAMLはこんな感じです。
<ListView ItemsSource="{Binding People}" >
    <ListView.Resources>
        <lv:SortingConditionConverter x:Key="ConditionToDirectionConverter" />
    </ListView.Resources>
    <ListView.View>
        <GridView>
            <GridViewColumn Width="120" DisplayMemberBinding="{Binding Name}">
                <GridViewColumnHeader Command="{Binding People.SortByPropertyCommand}" CommandParameter="Name" >
                    <lv:SortedHeader Content="Name" SortingDirection="{Binding People.SortingCondition, Mode=OneWay, Converter={StaticResource ConditionToDirectionConverter}, ConverterParameter='Name'}" />
                </GridViewColumnHeader>
            </GridViewColumn>
            <GridViewColumn Width="150" DisplayMemberBinding="{Binding Pronunciation}" >
                <GridViewColumnHeader Command="{Binding People.SortByPropertyCommand}" CommandParameter="Pronunciation" >
                    <lv:SortedHeader Content="Pronunciation" SortingDirection="{Binding People.SortingCondition, Mode=OneWay, Converter={StaticResource ConditionToDirectionConverter}, ConverterParameter='Pronunciation'}" />
                </GridViewColumnHeader>
            </GridViewColumn>
            <GridViewColumn Width="70" DisplayMemberBinding="{Binding Age}" >
                <GridViewColumnHeader Command="{Binding People.SortByPropertyCommand}" CommandParameter="Age" >
                    <lv:SortedHeader Content="Age" SortingDirection="{Binding People.SortingCondition, Mode=OneWay, Converter={StaticResource ConditionToDirectionConverter}, ConverterParameter='Age'}" />
                </GridViewColumnHeader>
            </GridViewColumn>
            <GridViewColumn Width="120" DisplayMemberBinding="{Binding Birthday}" >
                <GridViewColumnHeader Command="{Binding People.SortByPropertyCommand}" CommandParameter="Birthday" >
                    <lv:SortedHeader Content="Birthday" SortingDirection="{Binding People.SortingCondition, Mode=OneWay, Converter={StaticResource ConditionToDirectionConverter}, ConverterParameter='Birthday'}" />
                </GridViewColumnHeader>
            </GridViewColumn>
            <GridViewColumn Width="100" DisplayMemberBinding="{Binding Height}" >
                <GridViewColumnHeader Command="{Binding People.SortByPropertyCommand}" CommandParameter="Height_cm" >
                    <lv:SortedHeader Content="Height" SortingDirection="{Binding People.SortingCondition, Mode=OneWay, Converter={StaticResource ConditionToDirectionConverter}, ConverterParameter='Height_cm'}" />
                </GridViewColumnHeader>
            </GridViewColumn>
        </GridView>
    </ListView.View>
    <ListView.ItemContainerStyle>
        <Style TargetType="ListViewItem">
            <Setter Property="ContextMenu">
                <Setter.Value>
                    <ContextMenu>
                        <MenuItem Header="Increment the age" Command="{Binding IncrementAgeCommand}" />
                        <MenuItem Header="Decrement the age" Command="{Binding DecrementAgeCommand}" />
                    </ContextMenu>
                </Setter.Value>
            </Setter>
            <!--<Setter Property="lv:DoubleClickBehavior.Command" Value="{Binding DoubleClickCommand}" />-->
            <Setter Property="lv:DoubleClickBehavior.MethodTarget" Value="{Binding}" />
            <Setter Property="lv:DoubleClickBehavior.MethodName" Value="DoubleClicked" />
        </Style>
    </ListView.ItemContainerStyle>
    <i:Interaction.Triggers>
        <l:InteractionMessageTrigger Messenger="{Binding Messenger}" MessageKey="SelectedItemsMirroring" >
            <lv:ListViewSelectedItemsAction Source="{Binding People.SelectedItemsSetter}" />
        </l:InteractionMessageTrigger>
    </i:Interaction.Triggers>
</ListView>

ちなみに、「People」がViewModelでのListViewViewModelのインスタンスになります。

順に機能を見ていきます。

a. ソート

ListViewの機能のよく使う機能としてリストのソートがありますが、WPFでは標準でサポートされていません。それを行います。

ヘッダーをクリックして並び替える

ListViewのヘッダーをクリックするとCommandが発生します。ですので、そこでソートをする機能を呼び出します。
/// <summary>
/// プロパティ名をパラメーターに与えてソートするコマンド
/// </summary>
public ICommand SortByPropertyCommand { get; }
ListViewViewModelにはプロパティ名でソートするコマンドが備わっています。ですので、ヘッダーのクリックに合わせてこのコマンドを呼ぶようにしましょう。プロパティ名はパラメーターで渡してあげる必要があります。

ヘッダーにソートを示す▲▼を表示する

これはWPFの標準の機能がないので、ListView Extensionsが提供するSortedHeaderコントロールをGridViewColumnHeaderContentとして使います。GridViewColumnHeaderはContentControlを継承しているので、任意のコントロールをContent表示することができるのです。

SortedHeaderコントロールもContentControlクラスを継承しており、1個だけプロパティが追加されています。
/// <summary>
/// ソートの方向
/// </summary>
public SortingDirection SortingDirection
{
    get { return (SortingDirection)GetValue(SortingDirectionProperty); }
    set { SetValue(SortingDirectionProperty, value); }
}
SortingDirectionもListView Extensionsが提供するEnumで、Ascending、Descending、Noneの3つの状態があります。これに合わせて▲▼を表示してくれるようになります。

しかし、ListViewViewModelはソート状態をソート方向、プロパティ名の2つの値で保持しています。それらをひっくるめて、SortingConditionというプロパティがあります。
/// <summary>
/// 現在のソート条件
/// </summary>
public SortingCondition SortingCondition
{
    get { ... }
    private set
    {
        ...
    }
}
ですので、これから各ヘッダーに対応したSortingDirectionに変換するValueConverterが必要になります。

ご安心ください。そのConverterもListView Extensionsには装備されています。
SortingConditionConverterがそれで、パラメーターにプロパティ名を与えてやればそのヘッダーのSortingDirectionを返してくれます。

というわけでこのようなXAMLが出来上がります。
<GridViewColumn Width="120" DisplayMemberBinding="{Binding Name}">
    <GridViewColumnHeader Command="{Binding People.SortByPropertyCommand}" CommandParameter="Name" >
        <lv:SortedHeader Content="Name" SortingDirection="{Binding People.SortingCondition, Mode=OneWay, Converter={StaticResource ConditionToDirectionConverter}, ConverterParameter='Name'}" />
    </GridViewColumnHeader>
</GridViewColumn>
DisplayMemberBindingでプロパティ名、ヘッダーをクリックされたときのCommandParameterでプロパティ名、SortingConditionをSortingDirectionに変換するValueConverterのパラメーターでプロパティ名が必要で、計3回書く必要があります。SortedHeaderのContentもプロパティ名になる場合は4回出てきます。コピペミス等に気を付けてください。

b. アイテムのダブルクリック

これがなかなかやろうとすると難しいところです。
左クリックは通常選択程度の意味を成しませんからそんなに自分でコントロールしようと思うことはないですし、右クリックはたいていコンテキストメニューですからContextMenuプロパティにメニューを設定してやればいいです。しかし、ダブルクリックはアプリケーションを実行したり、編集画面を出したりと何かしらの処理を行う系になります。すなわち、ViewModelのメソッドやコマンドを呼び出したい何かになるわけです。

そこで、DoubleClickBehaviorです。この添付ビヘイビアはメソッド直接バインディングとコマンドのバインディングの両方を用意しています。Viewを見ればわかるかと思います。メソッドやコマンドは当然ですがアイテムのViewModelに書きます。

c. 選択

これもなかなか厄介です。ListViewのSelectedItemSelectedIndexは単一選択しか想定されていないうえに、SelectedItemsはgetのみでバインディングしようとするとうまくいきません。
ListViewItemはIsSelectedプロパティを持っているからこれを使えばいいのではと思うとそれも違います。ListViewItemは表示していない部分のインスタンスは消えてしまいますので、選択されているアイテムが表示範囲外に行くとIsSelectedは期待通りに動作してくれません。

実はListViewでの選択はこのSelectedItemsプロパティがすべて握っていて、このリストに対してAddやRemoveなどの編集操作をすると選択アイテムがそれに応じて増減します。さらに、このリストはICollectionChangedも実装していて、これで選択の変化を捉えることも可能です。
これはどう見てもコードビハインドで使うためのメソッドです。ですが、何とかそれをViewModelで使えるようにしました。手順としては次の通りになります。

SelectedItemsへの参照をViewModelに渡す

まずはこれを行う必要があります。これはListView Extensionsが提供しているListViewSelectedItemsActionを使います。ListViewViewModelはSelectedItemsSetterプロパティを持っており、ListViewSelectedItemsActionはこれにSelectedItemsの参照を渡してくれます。
<i:Interaction.Triggers>
    <l:InteractionMessageTrigger Messenger="{Binding Messenger}" MessageKey="SelectedItemsMirroring" >
        <lv:ListViewSelectedItemsAction Source="{Binding People.SelectedItemsSetter}" />
    </l:InteractionMessageTrigger>
</i:Interaction.Triggers>
今回はLivetを使っているので、LivetのInteractionMessageTriggerを使っています。別のMVVMフレームワークでも、何かしらViewModelからViewのアクションを動かす機構を持っているはずなのでそれを使ってください。
タイミングは、例えばContentRenderedなどのListViewインスタンス生成直後に呼ばれるイベントのタイミングでコピーをすべきでしょう。そのタイミングでViewModelからViewにListViewSelectedItemsActionを動かすように呼び出します。
Messenger.Raise(new InteractionMessage("SelectedItemsMirroring"));

選択の読み書き

SelectedItemsは非ジェネリックのコレクションで非常に使いにくいです。ですので、ListViewViewModelのSelectedItemsSetterはSet-Onlyプロパティにしており、読み取りはできないようにしています。

選択の状態を読み取るには、SelectedItemsSetterをミラーリングしているSelectedItemsプロパティを使ってください。
/// <summary>
/// 選択中の項目をIListじゃ使いにくいから使いやすくミラーリングしたクラス
/// </summary>
public ReadOnlyObservableCollection<TViewModel> SelectedItems
{
    get
    {
        ...
    }
}
逆に、選択をするにはViewModelに用意されたメソッドを使います。
/// <summary>
/// 指定したアイテムを選択します
/// </summary>
/// <param name="item">選択するアイテム</param>
public void SelectItem(TViewModel item);

/// <summary>
/// 指定したインデックスの要素を選択します。
/// </summary>
/// <param name="index">選択するインデックス</param>
public void SelectAt(int index);

/// <summary>
/// 指定した要素の選択を解除します
/// </summary>
/// <param name="item">選択を解除する要素</param>
public void UnselectItem(TViewModel item);

/// <summary>
/// 指定したインデックスの要素の選択を解除します
/// </summary>
/// <param name="index">インデックス</param>
public void UnselectAt(int index);

/// <summary>
/// 選択されているアイテムかを調べます
/// </summary>
/// <param name="item">選択されているかどうか調べたい要素</param>
/// <returns>選択されていればtrue</returns>
public bool IsSelectedItem(TViewModel item);

/// <summary>
/// 選択されているアイテムかを調べます
/// </summary>
/// <param name="item">選択されているかどうか調べたい要素</param>
/// <returns>選択されていればtrue</returns>
public bool IsSelectedAt(int index);

/// <summary>
/// 指定したアイテムの選択を反転します
/// </summary>
/// <param name="item">選択を反転するアイテム</param>
public void ToggleItemSelection(TViewModel item);

/// <summary>
/// 指定したインデックスの要素の選択を反転します。
/// </summary>
/// <param name="index">インデックス</param>
public void ToggleItemSelectionAt(int index);
まあ、メソッドの使い方は見ればわかるでしょう。

選択に対する操作

さらにもう一歩先の機能として、選択に対する操作も用意しています。
リスト系のソフトだと、選択しているアイテムを削除したり、選択しているアイテムを上や下へ動かすという用途もたくさん出てくるでしょう。ListViewViewModelにはそのような操作をするコマンドも用意されています。
/// <summary>
/// 選択中の項目を削除するコマンド
/// </summary>
public ICommand RemoveSelectedItemCommand { get; }

/// <summary>
/// 選択中の項目を上へ移動するコマンド
/// </summary>
public ICommand MoveUpSelectedItemCommand { get; }

/// <summary>
/// 選択中の項目を上へ移動するコマンド
/// </summary>
public ICommand MoveDownSelectedItemCommand { get; }

/// <summary>
/// 選択を反転するコマンド
/// </summary>
public ICommand ToggleSelectionCommand { get; }
適当なボタンなどを作って、必要に応じてこのコマンドを呼び出してあげれば良いでしょう。

選択に関する制約

ListView.SelectedItemsプロパティを見てもらっても分かりますが、ListViewは「選択されているアイテム」で選択を管理しており、例えばインデックスなどでは管理されていません。これは何を意味しているかというと、ListViewの要素として同じインスタンスを複数登録すると選択が正常に動作しないということです。

ですが、通常はListViewの要素はアイテムのViewModelになります。ViewModelのインスタンスをあらゆるアイテムに対して生成しておけば同じインスタンスが複数登録されることはないでしょう。

これは、ListViewViewModelのコンストラクタで制御します。
public ListViewViewModel(ISortableObservableCollection<TModel> source, Func<TModel, TViewModel> converter, Dispatcher dispatcher) { ... }
第2引数のconverterは、Modelでのコレクションの要素をViewModelに変換するためのデリゲートです。ここでは、必ず
person => new PersonViewModel(person)

のように新しいインスタンスを作るように実装しておけばいいだけです。

d. スレッド安全性

ListView Extensionsはスレッドセーフに設計されています。
複数スレッドからの同時アクセスなどがあってもデータの整合性は維持できます。ただ、固有のデッドロックの問題等もありますので、十分注意して使ってください。
そのあたりは、beta4のときの記事に詳しく書いてあります。

2. サンプルプログラム

さて、機能の概略は説明しましたが、実際のプログラムは見てみないとわかりにくいところもあるかと思います。ですので、細かい部分はサンプルプログラムを参考にして使ってください。

ListViewExtensionsSample ver.1.0.0

 実はbeta1の時に公開したサンプルプログラムとほとんど変わっていません。ただ、Model側にてスレッド安全性を確かめるための激しいプログラムにしています。
void AddLoop()
{
    Task.Run(async () => {
        while(true) {
            await Task.Delay(TimeSpan.FromSeconds(1));
            
            Parallel.ForEach(Enumerable.Range(0, 200), p => {
                if(p % 2 == 0 && People.Count > 0) {
                    lock(People.SyncRoot)
                        People.RemoveAt(random.Next(People.Count));
                } else
                    Add();
            });                
        }
    });
}

1秒おきに複数スレッドから100回のデータの追加と100回のデータ削除を行っています。もしもスレッド安全性に問題があれば、このコードをしばらく実行していれば不具合が起きるでしょう。

3. 最後に

(少なくとも私としては)結構使いやすいライブラリになったのではないかなと自負しています。WPFのListViewで不便さを感じている皆さん、ぜひご活用ください!

定義に従って行列式を求める

ここでいう行列式の定義とはこれです。 \[{\rm det}A=\sum_{\sigma \in S_n} {\rm sgn}(\sigma)\prod_{i=1}^n a_{i \sigma(i)}\] ここで言う$\sigma$は置換で、すべての置換の総和で行列式を定義したものですね。ちなみに${\rm sgn}$は偶置換、奇置換それぞれで1,-1を取る関数です。詳しくは線形代数の教科書でも見てください。

この定義、何が恐ろしいって計算量が${\rm O}(n \times n!)$なんですよね。すべての置換パターンで積を計算しなければならないので、n個の数の順列分の計算量が生まれるというわけです。行列の次数が上がるにつれて指数オーダーよりも激しく計算量が増えていきます。
ただ、それぞれの置換パターンは独立していますので並列処理も可能です。ですので、ベンチマークに使いやすいわけです。

主たる課題は下記のとおりです。
  • n次の行列のときnつの数の積を取るためオーバーフローしやすい(=多倍長整数型などを作る必要がある)
  • すべての置換パターンを求める必要がある
  • 並列処理の効率化(クリティカルセクション等々)
大学1年生のときC言語で計算するプログラムを書いて遊んでいましたが、なかなか大変でした。でも、C#は様々なライブラリがあり、並列処理も簡単に書けます。というわけで、C#で書いてみました。
コンピューターでの小数には「精度」が伴ってしまいますので、行列の要素は整数に限定してプログラムを作っていきます。

多倍長整数

C#にはintやlongなどの型があります。それぞれ32bit、64bitとビット数は決まっており、それより巨大な計算結果になるような演算をすると正確な値が得られなくなってしまいます。

そこで、必要に応じてビット数を拡張し、特にビット数の縛りなくメモリが許す限りどんな大きな整数でも扱えるような機構が欲しくなります。C#のような言語ならばそういったクラスや構造体を作れば良でしょう。
というのは誰もが思うことで、すでにそういうのが用意されています。

BigInteger 構造体

使うときに参照を追加してやる必要がありますが、それだけで簡単に多倍長整数で計算してくれます。演算子もオーバーロードされており、特に違和感なく計算ができるところがとても良いです。

ですが、さすがにLINQのメソッドまでは用意されておりませんので、今回のプログラムで使う分を実装しておきましょう。

public static class BigIntegerEnumerable
{
    public static IEnumerable<BigInteger> Range(BigInteger start, BigInteger count)
    {
        for(BigInteger i = 0; i < count; i++)
            yield return start + i;
    }
}

public static class BigIntegerUtil
{
    public static BigInteger Factorial(int start) => Enumerable.Range(1, start).Aggregate(new BigInteger(1), (total, now) => total * now);

    public static BigInteger Sum(this IEnumerable<BigInteger> source) => source.Aggregate(BigInteger.Zero, (now, next) => now + next);

    public static BigInteger Product(this IEnumerable<BigInteger> source) => source.Aggregate(BigInteger.One, (now, next) => now * next);
}

置換は(1, 2, ..., n)と並んでいる数列を入れ替えていくという話なので、Enumerable.Rangeみたいなメソッドがあるととても便利です。
また、多倍長整数を使うモチベーションとして「階乗」というのは非常に大きいはずですが、なぜかBigInteger構造体には階乗を求めるメソッドが無いので実装しておきます。あとは総和と総乗を取るのでSumとProductという拡張メソッドを作っておきます。Aggregate大活躍です。

順列と置換

例えば(1,2,3)の3つの数の順列は、(1,2,3),(1,3,2),(2,1,3),(2,3,1),(3,1,2),(3,2,1)の6パターンあります。そして、順列を小さい順?に並べるとこの順序になるはずです。
じゃあ任意の数列に対してi番目の並び替えは何か、というのを求めるメソッドを用意します。

static int[] Permutation(int[] source, BigInteger index)
{
    if(source.Length <= 1)
        return source.ToArray();

    var list = source.ToList();
    var div = (int)BigInteger.DivRem(index, BigIntegerUtil.Factorial(source.Length - 1), out BigInteger remainder);

    var first = list[div];
    list.RemoveAt(div);

    return new int[] { first }.Concat(Permutation(list.ToArray(), remainder)).ToArray();
}

今の配列の一番前に来るべき数を決めて、残りの項目を1つの数列として再帰的にこの関数を呼び出すことでindex番目の並び順を求めています。

ただ、このindexと置換の回数は直接的な相関はありません(たぶん)。ですので、置換の回数を求めるメソッドも用意しました。

static int ReplaceCount(int[] array1, int[] array2)
{
    if(array1.Length != array2.Length)
        throw new ArgumentException("Invalid array lengthes");

    int count = 0;            

    for(int i = 0; i < array1.Length; i++) {
        if(array1[i] != array2[i]) {
            var nextIndex = array2.ToList().IndexOf(array1[i], i + 1);
            if(nextIndex < 0)
                throw new InvalidOperationException("Cannot find same value.");

            var swap = array2[i];
            array2[i] = array2[nextIndex];
            array2[nextIndex] = swap;
            count++;
        }
    }

    return count;
}

やってることは選択ソートみたいなもんです。選択ソートについて置換の回数を見ているので、別のアルゴリズムを使えば回数は異なる可能性があります。ただ、偶奇性は一意に定まります。

並列実装

ここまで来れば、定義に従って行列式を求めるだけです。

Random rnd = new Random();
int[,] matrix = new int[order, order];

for(int i = 0; i < order; i++) {
    for(int j = 0; j < order; j++)
        matrix[i, j] = rnd.Next(10);
}

for(int j = 0; j < order; j++) {
    for(int i = 0; i < order; i++)
        Console.Write($"{matrix[i, j]} ");
    Console.WriteLine();
}

Dictionary<int, BigInteger> threaddet = new Dictionary<int, BigInteger>();
object threadlock = new object();

Stopwatch sw = new Stopwatch();
sw.Start();

var source = Enumerable.Range(0, order).ToArray();
Parallel.ForEach(BigIntegerEnumerable.Range(0, BigIntegerUtil.Factorial(order)), patternIndex => {
    var pattern = Permutation(source, patternIndex);
    var res = source.Zip(pattern, (r, c) => (BigInteger)matrix[r, c]).Product() * (ReplaceCount(source, pattern) % 2 == 0 ? 1 : -1);

    var id = System.Threading.Thread.CurrentThread.ManagedThreadId;
    if(!threaddet.ContainsKey(id)) {
        lock(threadlock)
            threaddet.Add(id, 0);
    }
    threaddet[id] += res;
});

var det = threaddet.Values.Sum();

sw.Stop();

Console.WriteLine($"det = {det}");
Console.WriteLine($"Elapsed: {sw.Elapsed.TotalSeconds}s");

まずはorder次の正方行列をランダムに生成します。わかりやすく各要素は0~9の整数にしています。
元の行列が決まったらorderの階乗だけ置換パターンがあるのでそれをParallel.ForEachで並列処理をしています。それぞれのパターンについて要素を取ってきて積を取ってから符号をつけます。
積が求まったら総和を取らなければなりませんが、和を取るためにはスレッド間での同期をとらなければなりません。ですので、それは一番最後に回すことにしています。具体的にはそれぞれのスレッドで計算した分の総和のみを求めており、すべてのスレッドの処理が終わったら各スレッドの総和を足し合わせる、という処理になっています。

スレッドの総和を求めるためには、スレッドIDをキーとしたDictionaryを用意して求めます。キーの追加時はロックをしないと、おそらく内部的なリストの「最後尾」にキーを追加というタイミングで複数スレッド間での競合が起きバグります。

私のPCはi7-4790Kを搭載していますが、11次の行列で2分半ちょっと計算にかかりました。行列を計算し始めてからOctaveを起動して行列を打ち込んで行列式を求めてもまだ定義に従った計算では求まらない、といったくらいのペースになります。

昔、C言語で作った時はえらく苦労をした記憶がありますが、C#だとこんなにすんなりできてしまいました。
やはり、並列化が体系化されていたり、演算子オーバーロード等で非常に使いやすい多倍長整数型が用意されていたり、あとはLINQで配列を簡単に扱えたりするのはとても大きいですね。

もうちょっとアルゴリズムが改良できるぞ、とかあったら是非教えてください。
特に置換パターンを求めたり置換回数を求めるあたりはまだ何かやりようがある気がしています。あ、「掃き出し法を使うと速いぞ」とか言うのは無しで。あくまでも定義に従った計算方式ということで。

2017年11月18日土曜日

MVVMのサンプルプログラム - TwitterViewer

前回の記事でMVVMとは何ぞやという説明をしましたが、今回はMVVMで実装する例としてTwitterのサンプルプログラムでも作ってみようかと思います。MVVMライブラリはLivetを使っています。

具体的なイメージを最初に持ってもらうために、完成形を最初に示しておきます。


短縮URLやRTの展開などはしていませんが、なかなかそれっぽいクライアントに見えるかと思います。

1. メインウィンドウのView

メインウィンドウのXAMLはこんな感じです。

<Window x:Class="TwitterViewer.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:TwitterViewer.Views"
        xmlns:vm="clr-namespace:TwitterViewer.ViewModels"
        Title="MainWindow" Height="1000" Width="500">
    
    <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.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="1*" />
        </Grid.RowDefinitions>
        <Button Grid.Row="0" Content="Load" >
            <i:Interaction.Triggers>
                <i:EventTrigger EventName="Click" >
                    <l:LivetCallMethodAction MethodTarget="{Binding}" MethodName="LoadButtonClicked" />
                </i:EventTrigger>
            </i:Interaction.Triggers>
        </Button>
        <ListBox Grid.Row="1" ItemsSource="{Binding Statuses}" ScrollViewer.HorizontalScrollBarVisibility="Disabled" ScrollViewer.CanContentScroll="False" >
            <ListBox.ItemTemplate>
                <DataTemplate DataType="{x:Type vm:TwitterStatusViewModel}">
                    <DockPanel HorizontalAlignment="Stretch">
                        <DockPanel.Background>
                            <LinearGradientBrush StartPoint="0,0" EndPoint="0,1" >
                                <GradientStop Color="White" Offset="0" />
                                <GradientStop Color="#FFF0F0F0" Offset="1" />
                            </LinearGradientBrush>
                        </DockPanel.Background>
                        <Image DockPanel.Dock="Left" Source="{Binding IconSource}" Width="50" VerticalAlignment="Top" />
                        <DockPanel DockPanel.Dock="Top">
                            <StackPanel DockPanel.Dock="Left" Orientation="Horizontal">
                                <TextBlock Text="@" Foreground="Blue" FontSize="12" />
                                <TextBlock Text="{Binding ScreenName}" Foreground="Blue" FontSize="12" />
                                <TextBlock Text="{Binding UserName}" Foreground="Blue" FontSize="12" FontWeight="Bold" Margin="5,0,0,0" />
                            </StackPanel>
                            <TextBlock DockPanel.Dock="Right" Text="{Binding CreatedAt}" Foreground="DarkGray" FontSize="12" HorizontalAlignment="Right"/>
                        </DockPanel>
                        <TextBlock DockPanel.Dock="Bottom">
                            via 
                            <Hyperlink ToolTip="{Binding ViaLink}" >
                                <TextBlock Text="{Binding ViaName}" />
                                <i:Interaction.Triggers>
                                    <i:EventTrigger EventName="Click">
                                        <l:LivetCallMethodAction MethodTarget="{Binding}" MethodName="ViaLinkClicked" />
                                    </i:EventTrigger>
                                </i:Interaction.Triggers>
                            </Hyperlink>
                        </TextBlock>
                        <TextBlock Text="{Binding Text}" FontSize="14" TextWrapping="Wrap" Margin="0,5,0,5" />
                    </DockPanel>
                </DataTemplate>
            </ListBox.ItemTemplate>
            <ListBox.ItemContainerStyle>
                <Style TargetType="ListBoxItem">
                    <Setter Property="HorizontalContentAlignment" Value="Stretch"></Setter>
                </Style>
            </ListBox.ItemContainerStyle>
        </ListBox>
    </Grid>
</Window>

このクライアントはツイートを画面に表示する以外に、読み込みボタンとviaクライアントのクリックという2つの操作がユーザーでできるようになっています。
これはLivetのメソッド直接バインディング機能を使って、クリック時にViewModelのメソッドを呼び出すようにしています。

表示はListBoxのカスタマイズで行っています。
個々の要素(ツイート)の表示部分をテンプレートとしてListBox.ItemTemplateに渡しています。
若干複雑に見えるかもしれませんが、上の画像を見ながら追っていけば特段難しくないかと思います。

2. メインウィンドウのViewModel

メインウィンドウのViewModelはこんな感じです。

public class MainWindowViewModel : ViewModel
{
    Model model;

    public MainWindowViewModel()
    {
        model = Model.GetInstance();

        Statuses = model.Statuses.ToSyncedSynchronizationContextCollection(p => new TwitterStatusViewModel(p), System.Threading.SynchronizationContext.Current);
    }

    public void Initialize()
    {
    }

    public async void LoadButtonClicked()
    {
        await model.LoadHomeTimeline();
    }

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

    public SynchronizationContextCollection<TwitterStatusViewModel> Statuses
    {
        get { return _Statuses; }
        set
        {
            if(_Statuses == value)
                return;
            _Statuses = value;
            RaisePropertyChanged(nameof(Statuses));
        }
    }
    private SynchronizationContextCollection<TwitterStatusViewModel> _Statuses;

    #endregion
}

Loadボタンが押されたときにそれを受け取るメソッドと、ツイートの表示内容を示すコレクションを用意しています。

SynchronizationContextCollectionはStatefulModelというライブラリのものです。「特定のコンテキストでコレクションの変更通知を発行するスレッドセーフなコレクション」で、WPFの制約である「単一スレッドでしかUIを触ることができない」を吸収してくれる便利なコレクションです。

ちなみに、Statusesプロパティはmodel.Statuses.ToSyncedSynchronizationContextCollectionメソッドで生成しています。ここでmodel.Statusesという変更通知コレクション(詳しくはModelの項を見てください)のイベントを拾えるようリスナーを登録しています。ViewModel側のStausesではModel側のイベントを拾って自身もそれに同期し変更通知イベントを発生させますが、その際にUIスレッドに転送してイベントを発生させてくれます。まさにMVVMのためのコレクション機構ですね。

3. メインウィンドウのModel

Modelはこちらです。

public class Model : NotificationObject
{
    #region Singleton

    static Model instance;

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

    #endregion

    TwitterContext twitterContext;

    private Model()
    {
        Statuses = new SortedObservableCollection<TwitterStatus, DateTime>(s => s.CreatedAt, true);

        SingleUserAuthorizer auth = new SingleUserAuthorizer()
        {
            CredentialStore = new SingleUserInMemoryCredentialStore()
            {
                ConsumerKey = "****",
                ConsumerSecret = "****",
                AccessToken = "****",
                AccessTokenSecret = "****",
            }
        };
        twitterContext = new TwitterContext(auth);
    }

    public async Task LoadHomeTimeline()
    {
        await Task.Run(() => {
            foreach(var status in twitterContext.Status.Where(p => p.Type == StatusType.Home && p.Count == 100)) {
                if(!Statuses.Any(p => p.StatusID == status.StatusID))
                    Statuses.Add(new TwitterStatus(status));
            }
        });
    }
    
    #region Statuses変更通知プロパティ

    public SortedObservableCollection<TwitterStatus, DateTime> Statuses
    {
        get { return _Statuses; }
        set
        { 
            if(_Statuses == value)
                return;
            _Statuses = value;
            RaisePropertyChanged(nameof(Statuses));
        }
    }
    private SortedObservableCollection<TwitterStatus, DateTime> _Statuses;

    #endregion        
}

TwitterにアクセスするのにはLinq2Twitterを使っています。
SortedObservableCollectionはStatefulModelの提供するクラスの1つです。タイムラインは常に時間順でソートされるべきですので、そうなるようにSortedなコレクションを使っています。ViewModel側でこれをウォッチしてUIのスレッドで変更通知をViewに伝えるのです。

ちなみに、このModelはシングルトンパターンを取っています。
ここはちょっとしたこだわりですが、ViewModelがModelを作るわけではないので、その気持ちを込めてこういう構造にしています。


4. ツイートのViewModel

さて、ここまで来てメインウィンドウに対応するViewModel、Model以外にもTwitterStatusViewModelとTwitterStatusというクラスがあることに気づいたことでしょう。個々のツイートに対応するViewがありますから、それに対応するViewModelを作るのは自然かと思います。

public class TwitterStatusViewModel : ViewModel
{
    TwitterStatus Source;

    public TwitterStatusViewModel(TwitterStatus source)
    {
        Source = source;

        Text = Source.Text;
        IconSource = Source.IconSource;
        ScreenName = Source.ScreenName;
        UserName = Source.UserName;
        CreatedAt = Source.CreatedAt;
        ViaName= Source.ViaName;
        ViaLink = Source.ViaLink;
    }

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

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

    #endregion

    #region IconSource変更通知プロパティ
    private string _IconSource;

    public string IconSource
    {
        get
        { return _IconSource; }
        set
        {
            if(_IconSource == value)
                return;
            _IconSource = value;
            RaisePropertyChanged(nameof(IconSource));
        }
    }
    #endregion

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

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

    #endregion

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

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

    #endregion
    
    #region CreatedAt変更通知プロパティ

    public DateTime CreatedAt
    {
        get { return _CreatedAt; }
        set
        { 
            if(_CreatedAt == value)
                return;
            _CreatedAt = value;
            RaisePropertyChanged(nameof(CreatedAt));
        }
    }
    private DateTime _CreatedAt;

    #endregion
    
    #region ViaName変更通知プロパティ

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

    #endregion

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

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

    #endregion

    public void ViaLinkClicked()
    {
        Source.OpenViaLink();
    }
}


やたら長いですが、大部分が変更通知プロパティです。
反射的に変更通知ができるように作ってしまいましたが、ツイートは一度つぶやくと後から変更できないので、Immutableに作っても良かってもよかったかもしれません。

今回は面倒なので実装しませんでしたが、例えば、ツイート時刻の相対表記(何秒前のツイートかを表示するもの)だったら定期的に表示内容を変えなければならないので、そういうものはModelの変更通知を監視し、変更されたら自身を更新するように作り替えねばなりません。

ちなみに、一番最後にはviaのリンクをクリックされたときに呼ばれるメソッドがありますが、これはそのままModelに横流ししています。

5. ツイートのModel

最後にツイートのModelです。

public class TwitterStatus : NotificationObject
{
    static Regex regViaUrl = new Regex("href=\"([^\"]+)\"", RegexOptions.Compiled | RegexOptions.IgnoreCase);
    static Regex regViaName = new Regex(">([^<]+)<", RegexOptions.Compiled | RegexOptions.IgnoreCase);

    public TwitterStatus(Status Source)
    {
        Text = Source.Text;
        StatusID = Source.StatusID;
        IconSource = Source.User.ProfileImageUrlHttps;
        ScreenName = Source.User.ScreenNameResponse;
        UserName = Source.User.Name;
        CreatedAt = Source.CreatedAt.ToLocalTime();

        Match m = regViaName.Match(Source.Source);
        ViaName = m.Success ? m.Groups[1].Value : Source.Source;

        m = regViaUrl.Match(Source.Source);
        ViaLink = m.Success ? m.Groups[1].Value : string.Empty;
    }

    public string Text { get; }

    public ulong StatusID { get; }

    public string IconSource { get; }

    public string ScreenName { get; }

    public string UserName { get; }

    public DateTime CreatedAt { get; }

    public string ViaName { get; }

    public string ViaLink { get; }

    public void OpenViaLink()
    {
        try {
            System.Diagnostics.Process.Start(ViaLink);
        }
        catch(Win32Exception) {
            throw;
        }
        catch(FileNotFoundException) {
            throw;
        }
    }
}

基本的にViewModelに対応させていますが、viaのURLとクライアント名を分離するためのロジックをコンストラクタにちょろっと入れました。

また、viaのリンクを開くロジックもここに書いています。まあ、Process.Startを呼ぶだけなのですが。
ですが、何かしらの理由で開けないことがあるかと思います。例えばTwitterの仕様変更でURLが含まれなくなったり、WindowsにURLを開くアプリケーションが関連付けられていなかったり。そういう場合はProcess.Startメソッドが例外を吐いてきますが、その処理はModel側でやるべきです(今回はめんどいんで結局再スローしちゃっています)。 「URLが開けませんでした」みたいなエラーメッセージは改めてModel側からイベントを発生させてそれをViewModel経由で表示させるべきですね。ViewModelはあくまでもUIの制約を吸収するためだけの層です。URLが開けなかったというのはUIの制約じゃないですよね。ということは、それをViewModelに押し付けるのは間違っています。

まとめ

今回は初めて連載っぽい記事にしましたがいかがでしたでしょうか。
ここまできて気づいた方も多いかと思いますが、ViewModelとModelは同じようなプロパティやメソッドがたくさん登場してきます。当然です。なぜならば、ViewModelはViewの制約を吸収するための層なので、制約がない部分は素通りさせるべきなのです。なので、素通りする部分にとっては一層余計に挟んでいるような状態になってしまい、同じようなコードが生まれてしまうのです。

プログラマーと言えばコピペコードを嫌う生物ですから、このようなスタイルに懐疑の念を抱く人は多いかと思います。知ったうえで「俺はこっちのほうがいい」と思ったのなら、MVVMになっていないスタイルでプログラムを書いてもいいでしょう。ただし、非同期プログラミングで泣いても私は知りません。

少なくとも、ここまで読んでくださった方たちはMVVMが何かというのはわかってきたかと思います。

MVVMは問題領域の切り分け方です。UIの制約はViewModelに押し付けて、後はすべてModelが引き受ける、それだけです。なので、おそらくはWPFでMVVMを着崩していくよりかは、MVVMの形をキープしながら様々な便利なライブラリなどのアクセサリを身に着けていくのが一番スマートなやり方なのではないかなと私は思います。

MVVMとは何か

MVVMとは何か、それはWPFプログラミングを始めた人は誰しもが気になることでしょう。
私はあまりWindows以外のプラットフォームは触らないのですが、最近はAndroidでもMVVMが流行ってるとかなんとか。

そのせいか、MVVM+適当なワードでググるといくらでもQi○taなどの記事がヒットします。ですが、多くは「データバインディングを使えばMVVM」くらいの浅はかな理解で、MVVMとは何かがまるでわかっていないようです。
まあ「初心者」というのは誰もが通る道ですからそういうのをはなから否定する気はないですが、情報汚染されているのは悲しいので私の理解の範囲を記事にしておこうと思います。

まず忘れてほしいこと

MVVMとは何かというのを調べ始めるとまず見つかることですが、それはMVVMの本質ではないというのをいくつか挙げていきます。私の今回の記事を読むうえでまずは次の3点のことを忘れてください。

1. データバインディングをするのがMVVMである

MVVM初学者は真っ先にこれにぶち当たるかもしれませんが、データバインディングはあくまでもViewとViewModelのデータのやり取りの手段に過ぎません。データバインディングをするのがMVVMではありません。
似たようなものとして「ユーザーインターフェースとビジネスロジックの疎結合を実現するのがMVVMである」というのも大嘘です。

確かにWin32APIやWinFormsなどをいじっていた人からしたら、今までUIとビジネスロジックを一体的に記述していたわけですから、それは大きなモチベーションに見えるでしょう。それぞれを分離して設計できれば、設計を分担するのもやりやすいですし、再利用性、可読性も上がります。何しろ手続き型言語であるC言語やC#などでUIを設計するそれらのフレームワークに比べて、XAMLとかいうマークアップ言語でUIが設計でき、ビジネスロジックをC#で書ければそれはたいそうなメリットに見えるでしょう。私も当初はそうでした。

でも考えてください。それならばView-Modelで良いんです。UIとビジネスロジックを分離するって、それってView-Modelですよね。で、ModelとViewの接続にはデータバインディングを使うと。
実際、この手のモチベーションだけでMVVMを書こうとすると、ViewModelが実質Model的な役割になって、結局のところView-Modelになってしまいます。それじゃあMVVMじゃないんです。

この記事の下のほうへ行くと見えてくると思いますが、MVVMは「UIとビジネスロジックの疎結合」なんていうのは当たり前なんです。それだけだと不都合が起こるから、ViewModelが介在しているんです。

2. MVVMはコードビハインドを書かない

これは結果論としては間違っていないとは思いますが、違います。
何も考えずにコードビハインドを書くとたいていそれがビジネスロジックになりますから、UIとビジネスロジックの分離すらできていない状態になります。それは"論外"以前の問題です。
正確には「WPFはコードビハインドを書かなくて済む」なのです。データバインディング、コマンド、ビヘイビアなどの様々な「Viewとやり取りする方法」があるため、結果としてコードビハインドを書く必要が無いケースがほとんどなのです。

ですので、WPF初心者は「コードビハインドを書かない」をテーマにいろいろ調べてみるといいと思います。いろいろな手段が出てきていい勉強になるでしょう。ですが、あくまでもそれが目的ではないのです。

3. Modelはビジネスロジックなので、UIに依存しない

これは見方によっては間違ってはいないのですが、間違った意味にも取れるので書いておきます。そして、多くの人は間違った意味に取るように思います。
「依存しない」の意味を正しく理解しているかがキーです。

「UIに依存しない」というのは「UIのフレームワークで生まれた制約を知らなくていい」というニュアンスの意味なのです。UIのフレームワークの制約を知らなくていいだけなので、ModelはUIの存在を強く意識します

これだけだと何を言っているのかよくわからないので、メール送信ソフトを作っている場面を想像しながら説明します。
例えば送信ボタンを押したとき、ボタンのコマンドが発動します。コマンドはViewModelに記述しますが、そのViewModel内でTextプロパティやToAddressプロパティ、Subjectプロパティなどの値を取得し、
mail.Send(ToAddress, Subject, Text);
などといったメソッドを呼ぶなどといった処理を記述しがちかと思います。なおこのMailクラスは当然Modelにあるものとします。

これは大間違いなのです。

これだとModelは「ライブラリ」です。このスタイルではView-Model-Libraryになってしまいます。Modelとライブラリの関係なんてUIフレームワークの側からなんかは知ったこっちゃないですよね。ですのでこれはView-Modelと何ら変わらないのです。
なぜこういう書き方をしてしまうかというと、MVVM初心者はViewModelが何のためにあるのかがわかっていないのです。そのため、「ViewとModelをつなぐもの」という説明からViewの範囲をUI、Modelの範囲をビジネスロジックライブラリに限定してしまい、上記のような誤った実装をしてしまうのです。

ではViewModelは何のためにあるのか。実は、ViewModelとはView(=UI)の制約を吸収するためだけの層なのです。なので、Viewがあれば必然的にViewModelも出来上がります。そして残りの領域がすべてModelになるわけです。

「ModelはUIに依存しない」によく似た表現として、「ViewとModelがあって、その間を取り持つのがViewModel」 というのがありますが、これも大いに誤解を生む表現です。正しくは「ViewとViewModelがあって、それ以外の領域がModel」なのです。
「ありき」なのはViewとModelではなく、ViewとViewModelなのです。「Modelはビジネスロジック」といったようにModelの範囲を限定している時点でそれはもう間違っているのです。

WPFの制約

ここからは、UIをWPFに限定してお話をしていきます。
さて、先ほど「UIの制約を吸収するのがViewModel」と言いました。では、WPFにおけるUIの制約とは何なのでしょうか。下記に2例示します。

1. UIは単一スレッドでしか操作できない

WPFはUIを操作できるスレッドは1つしかありません。
複数のスレッドからUIをいじれるようにしようとすると排他制御などで逆に効率が落ちるらしく、それならばと単一スレッドでしか使えないという縛りを入れているようです。
なので、UI以外のスレッドからアクセスしようとしたら例外が吐かれます。

これを吸収するのがViewModelの役目です。
ViewModelは、Modelで何かしらのプロパティの値が変更されたら、それをUIのスレッドでViewに変更通知をします。それ以下のこともそれ以上のこともしません。

MVVMライブラリは、ViewModelの基底クラスでこの処理を行っています。PropertyChangedイベントは必ずUIのスレッドで発生するように作ってあります。ですので初心者は「WPFは単一スレッドでしかUIを触れない」という制約すら知らずに済むのです。MVVMが成功している証です。

ここで注意しなければならないのは、コレクションの変更通知はプロパティ変更通知とはまた別ということです。
ObservableCollectionは何もしなければコレクションを更新したスレッドからそのままCollectionChangedイベントが発生しますので、UIスレッド以外から更新すると例外が飛びます。ですので、ObservableCollectionもコレクションとUIを結びつけるものとしてViewModelとしてUIのスレッドでCollectionChangedイベントを発生させるようなものを作ってやる必要があるのです。
//BindingOperations.EnableCollectionSynchronizationメソッドというものがありこれを使えばUIスレッドに転送できますが、なんでこのような取ってつけたような実装にしたのか理解に苦しみます…。

2. リスト系コントロールは表示されている分しか要素のインスタンスを持たない 

これはどういうことかというと、リストの要素を操作しようとしたときに実体(インスタンス)が無い場合があるということです。
理解するために次のようなコードを書いてみます。

public partial class TestUserControl : UserControl
{
    static int counter = 0;

    public TestUserControl()
    {
        Console.WriteLine(counter++);
        InitializeComponent();
    }
}

まずはこんなコントロールを作ってみます。このコントロールのインスタンスが作られた回数を標準出力に書き出すだけのコントロールです。

<Window x:Class="WpfListControlTest.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfListControlTest"
        mc:Ignorable="d"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
        <ListBox Name="list">
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <local:TestUserControl Height="20" />
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>
    </Grid>
</Window>

こちらがMainWindowです。Listの要素としてTestUserControlを表示しています。

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();

        for(int i = 0; i < 100; i++)
            list.Items.Add(new object());
    }
}


めんどいのでMainWindowのコードビハインドでアイテムを100個追加してみます。

直観的に考えたらソフトを起動したときに100個のTestUserControlが生成されそうな気がします。


14個しかインスタンスが生成されていません。
これは表示されている分で、それ以上のインスタンスは生成されていないのです。

何往復かスクロールしてみます。


100個しかアイテムが無いはずなのに269回インスタンスが作られています。
表示外に外れたインスタンスは消去され、再び表示されたらまたインスタンスから作り直しているんですね。
こうすることで、確かにヒープに負荷はかかりますが何万個とアイテムを持ったリストなどの生成を可能としているのです。

もしもこれにViewModelを用意しなかったらModelでViewをいじろうとしても表示外だったらぬるぽが発生するのです。
そこで、ViewModelでワンクッションを置くことでModelからはいつでもすべての表示項目のViewModelをいじれるようにしてあげます。項目が表示されればViewのインスタンスが生成され、同時にViewModelにバインディングすることであたかも隠れていた項目が今表示されたかのようにふるまうことができるのです。

これもUIの都合ですので、それを吸収するのがViewModelの役目です。

WPFの制約まとめ


どうでしょう、案外WPFにはいろいろな制約があることが分かったと思います。ほかにもいろいろあるかもしれませんが、これだけでMVVMの必要性を説明するのには十分だと思います。

Modelにとっては、UIは単一スレッドでしか触れないことや表示されていない項目のコントロールのインスタンスが存在しないことなんて知ったこっちゃありません。それを吸収するのがViewModelの役割なのです。なので、「これとこれを表示するUI(TextBoxやListViewなど)がある」とか「ボタンが押された」などといったUIの存在自体やUIの主体となる動作はModelは意識する必要があるのです。

このような目的から、Modelから見たViewModelはViewのように見えなければなりません。ViewModelはViewの制約をなくすためのラッパーなのですから、当然機能としてはただのViewと同等になるわけです。

MVVMとは

MVVMの根底には確かにUIとビジネスロジックの分離があります。でも、単純にViewとModelに分けようとすると、WPFの場合はViewに様々な制約があり、Modelがその制約に引きずられたものになってしまいます。そうすると、Modelと言っておきながらWPFに深く入り込んだものが出来上がり、分離とは言い難い形になってしまうのです。
そこで、Viewの制約を吸収してあげる層、すなわちViewModelを作って制約なくModelからViewを操作できるようにしてあげようというのがModel-View-ViewModelこと、MVVMなのです。

概念だけ説明されてもわからないよ、という人もいるかと思いますので、次回は実際にソフトを作りながら解説していきます。