WPF、怒りのツリー外DataContext伝播

  • 37
    いいね
  • 0
    コメント
この記事は最終更新日から1年以上が経過しています。

この記事は XAML Advent Calender 2014の 12/10分の記事です。
自分のブログが復活していないので復活次第そちらへも同じものを書きます。

そもそもどういうこと?


WPFerには常識ですが、FrameworkElementに親子関係があればDataContextが継承されます。親のDataContextが変われば子のDataContextも自動で変わります。この機能のおかげで大分XAMLの記述量少なくなっていますよね。

alt

この機能は基本的に「VisualTree(LogicalTreeの方かもしんない)上で親子関係があるFrameworkElement同士」でしか機能しません。FrameworkElementじゃないとそもそもDataContextプロパティがありません。
てはではこれはどうなっているんでしょうか?

alt

添付プロパティであるInteraction.Triggersも、トリガーであるEventTriggerも、トリガーアクションであるInvokeCommandActionもどれも全然FrameworkElementじゃありません。どころか起点となるInteraction.Triggersは添付プロパティなのでそもそもVisualTree上にあるものではありません。しかしこのコードは正常に動作し、MainWindowViewModelのTestCommandがきちんと呼び出されます。
実はこれにはWPFのアンドキュメントな仕様が関係しています。

Freezableの魔法


トリガーの基底クラスはTriggerBaseです。そしてトリガーアクションの基底クラスはTriggerActionです。
(どちらもBlendSDKのSystem.Windows.Interactivity.dllに含まれます)

どちらもFrameworkElementではなく、ですのでDataContextプロパティ自体を持ちません。

TriggerBaseとTriggerActionの基底クラスは実はFreezableです。

Freezable クラス - MSDN

実はこのFreezableであることがこの摩訶不思議なDataContextの伝播の肝になります。

Leveraging Freezables to Provide an Inheritance Context for Bindings - Dr.WPF

仕組みはまったくのブラックボックスで調べても調べてもわかりませんが、FreezableであるとまるでVisualTree(LogicalTreeの方かもしんない)上にありDataContextが存在するかのようにバインドが可能になるのです。Brushとかも基底はFreezableで、同じ仕組みでバインドを実現しています。
(なお昔調べたところSilverlightでは何かのコレクション(名前覚えていない)に放り込むことで似た仕組みを実現していたのでもしかしたらStoreAppもそうなんじゃないかな?Blend SDKっぽいもの見ればたぶんわかります。僕はStoreAppに興味ないのでスルー)

この仕組みが大変やっかいなのは、MSのドキュメントのどこをどう探してもこの仕様が載っていない事です。プログラマたるもの、破壊的変更がなされる余地を多く残すアンドキュメントな仕様には頼りたくないものなのですが・・。

Livet固有のMessageシステム


さて私がAuthorを務めるLivetはこのFreezableによるバインドの仕組みを使ってPrismやMVVM Light Toolkitにはない独自のMessenger機能を持っています。

LivetのMessenger


Messenger自体は以下の図のように、ViewModel側からViewを参照せずに、ViewModel起点でViewになんらかの処理を起こさせる機構の事です。(この図ではPrismのMessengerについて説明しています)

Messengerの仕組み
もう内容が古くなってしまってどうしてくれようかと思っている私の@IT記事から抜粋

PrismやMVVM Lightのものと違いLivetのMessengerは、View側でダイレクトにMessageの定義ができます。

Livet-DirectInteractionMessage

この例に出てくるConfirmationDialogInteractionMessageActionは、通常ViewModelから発信されたMessageを受け取ってユーザーに確認ダイアログを表示し、ViewModel側のメソッドを呼び出すトリガーアクションです。そういったPrismのようなシナリオの他にLivetのトリガーアクションではView側で直接Messageを定義するというシナリオを用意してあります。
DirectInteractionMessageという部分で直接メッセージを定義しています。このView側での直接メッセージ定義のメリットはこういった確認ダイアログの表示の際などに威力を発揮します。たとえばこれをPrismで実現する場合を考えてみましょう。

Prismでの確認ダイアログの表示

見ての通り、たかだか確認ダイアログを表示するのにやり取りが多すぎです。LivetではViewに直接メッセージを定義できるおかげでこうなります。

Livetでの確認ダイアログの表示

見ての通りシンプルになります。ViewModel側にわけのわからない粒度のメソッドが沢山できることもなくなるわけです。
素晴らしいですね!(@neuecc風自画自賛芸)

XAML以外でFreezableを使用することで起こる問題

さて、XAMLに書けるものならなんでもバインドしたくなるのがWPFer、いや、XAMLerの性(サガ)。我々を苦しめてやまないこの性ですが、当然私もそれに躍らされて結果Messageはバインド可能なオブジェクトになりました。さきほどの例でもすでにバインドしていますね。

かといってMessageをFrameworkElementにするなんていう悪手は使いたくありません。MessageはViewModelでも扱うものですし、ただでさえ小さいUIElementを大量に表示するのが苦手なXAML系テクノロジーではこれはどうみても悪手です。しかしMessageはトリガーアクションでもトリガーでもありません。じゃあ、ということで先ほどのFreezableを使用していたわけです。つまりLivetのMessageはすべてFreezableでした。この機能を盛り込んだのはもういつかわからないくらい昔のことです。(0.9x系のころからあったようななかったような)

それがですね。。。。。。。。。。。

@Grabacr07 さん記事
バックグラウンド スレッドで UI 要素を作るとメモリリークする (WPF) - grabacr.nét

@karno さん記事
バックグラウンドスレッドでUI要素を作るともっと問題は深刻かもしれない。(WPF) - LOGarithm

この怒り、どこにぶつければ良いのでしょうか?
FreezableをFreezeすれば別スレッドからも使えるとかいう仕様はどこいったんでしょうか?

どうやらお二人にはLivetのMessageにもあるこの問題でご迷惑をおかけしたみたいです。

Livetでの対策

最初は上の記事を前職での忙しい最中にチラ見しただけで勘違いしていて、FreezableさえMessageから切り離せばいいと思っていたんです。
その方針でブランチコミットまでしています。
(DataContextプロパティをDirectInteractionMessage以下にはやし、手動継承するやり方でやりました)

LivetCommit - Removed Freezable classes from InteractionMessage. - Github

ですがよく見たらDependencyObjectすべてで起こる問題じゃないですか!
DependencyObjectであることってのはバインドできるオブジェクトの絶対条件であり、これはつまりLivetのバインドできるMessageをUIスレッド以外から生成する道は詰んだという事を示します。
(なおMessageをUIスレッド以外から作らなければ現状Livetでそれは回避可能です)

なのでまずMessageは別スレッドから生成しようとしても必ずUIスレッド上で動作する。あるいはMVVMの本来のあり方どおりにModelにUIスレッド以外をすべて押し込め、ViewModel以上はUIスレッド上で動くという制約を入れることの妥当性について検討している最中となっております。

次点のアプローチとしては、ViewModelで使用するMessageとViewで使用するMessageはクラスを分けるというやり方も検討しています。(ただこのやり方だときっと開発者がViewModelにある別のDependecyObjectをリークさせて不毛なんだろうな・・)

(MVVMの考え方からすればWPFというPresentationPlatformの制約はViewModelまでを使って吸収されるべきです。そして実際ViewModelでDependencyObjectを使用することは決して少なくありません。なのでViewModelまでをUIスレッドに固定するという考え方は突拍子もないように聞こえますが妥当です。反論はこちらを読まれた後にどうぞ)

この投稿は XAML Advent Calendar 201410日目の記事です。