質問者
仮想化されたアイテムからプロパティの変更を ViewModel 側へ通知したい

質問
-
いつもお世話になっております、おのでらです。1か月ほど試行錯誤していたのですが、どうしても解決できそうにありませんでしたのでこちらで質問させていただきたいと思います。(ちょっと長文になってしまいます。私の知識不足な点もあるかもしれないのでその際はご指摘ください)
◆環境
・Visual Studio 2010
・.NET Framework 4 (Client プロファイルじゃないほう)
・WPF アプリケーション
・C#現在複数のアイテムを選択可能な TreeView を作成しようと思っております。国外のサイトを調べるといくつかサンプルは見つかるのですが、いずれも TreeView のアイテムを仮想化すると大抵のサンプルは選択状態が不定になるなど目的の挙動になってくれませんでした。(または DataBinding を使ってないなど)
やりたいことを箇条書きにまとめると以下のようになります。
◆やりたいこと・「TreeView」コントロールで複数のノードを選択できるようにしたい
・TreeView のアイテムは以下の条件で仮想化する (これは必須です)
・VirtualizingStackPanel.IsVirtualizing="true"
・VirtualizingStackPanel.VirtualizationMode="Recycling"
・アイテムの選択状態を TreeViewItem の「IsSelected2 依存関係プロパティ」として定義 (もともとの IsSelected プロパティとぶつかるので今は IsSelected2 で回避してます。あとで調整)。このプロパティはコントロール側の操作(マウスクリック、タッチ、カーソルキー等)によって選択未選択を制御する。(ViewModel 以外での処理を想定。ListBoxItem のように ViewModel 関係なしに動作するように)
・選択しているアイテム一覧「SelectedItems」プロパティを TreeView の依存関係プロパティとして追加する
・仮想化されたアイテムのプロパティを変更して ViewModel 側へプロパティ変更通知を行う
・複数選択は以下のような操作で行う(基本 ListBox を想定していただければわかりやすいかと思います)
・Ctrl+マウス左ボタンによる追加選択、解除
・Shift+マウス左ボタンによる「アンカーアイテム」からの範囲選択
・Ctrl+カーソルキーによるアイテムの「アンカー移動」 (ただ、WPF だとスクロールするっぽいのでこれはなくてもいいです)
・Shift+カーソルキーによる追加選択、解除
・マウス左ボタン、またはカーソルキーによるシングル選択+他のアイテムの選択全解除
◆問題点アイテムの仮想化を行っていない状態では常にすべての TreeViewItem が可視化されているので(とりあえず Expand は考えなくてもいいです)、全てのアイテムに対してコントロール側ですべてのアイテムに対してプロパティの変更を行うことができます。
例えば、アイテムが100個あったとしてすべての選択を解除させたい場合は
for (int i = 0; i < tree.Items.Count; i++) { var item = (TreeViewItem)tree.ItemContainerGenerator.ContainerFromIndex(i); SetIsSelected2(item, false); // ←下記に記載 } public static void SetIsSelected2(DependencyObject obj, bool value) { obj.SetValue(IsSelected2Property, value); }
のように「ItemContainerGenerator.ContainerFromIndex」メソッドですべてのアイテムを検索できます。
しかし、アイテムが仮想化されている場合「ItemContainerGenerator.ContainerFromIndex」で取得できるアイテムは「見えている範囲+α」分のアイテムしか取得できないため (そもそも TreeViewItem 自体作成されていない+リサイクルされるので順不同)、すべてのアイテムに対してプロパティを設定できず、さらに ViewModel 側へ選択解除を通知する方法がありません。
◆調べてみたこと○ ItemContainerPattern と VirtualizedItemPattern を使う
WPF 4 ではこれらが追加されて仮想化された要素へのアクセスが可能らしいのですが UI オートメーション関連のクラスなので正直このクラスを使うべきかは迷っています。TreeViewAutomationPeer から使えないかどうかいろいろ試行していますが、どうしてもこの2つのパターンへ結びつける方法が見つけられず今は断念中です。(AutomationElement が取得できればいいんですが、それを取得する AutomationUtilities がテスト用のクラスなんですよね・・・)
○ VirtualizingPanel.BringIndexIntoView で無理やり可視化する見えないアイテムへアクセスできないのであれば、無理やり見えるようにし TreeViewItem を作成してプロパティにアクセスする方法です。ただ文章のごとくアイテムにアクセスするには VirtualizingPanel をスクロールさせないといけないので、プロパティを設定させるたびにスクロールするのが見た目でわかるのでアウトです。(スクロールの描画を停止できればこれはありなのかもしれませんがその方法は見つかっていません)
※ほかにもいろいろ試していますが細かいので省略します
◆現時点でできればいいもの・仮想化されたアイテムにバインドされている ViewModel のプロパティを「コントロール側」から通知して変更する
TreeView の複数のアイテムの選択なので基本的にはコントロール側で操作することによってコントロール側のプロパティが変更され ViewModel 側に通知される仕組みを想定しています。(ViewModel 側でアンカーアイテム、カレントアイテム、選択アイテム一覧を持つようなことはしない)
例えば 100個のアイテムが存在するとし、可視化されているアイテムが 10個であるときに「Ctrl+A」キーでアイテム全選択、シングルクリックで 99個のアイテムの選択解除を想定しているので、仮想化されているアイテムからも通知できるようにしたいと思っています。
「仮想化されたアイテムから」と書いていますが、TreeView からでもアイテムにバインドしたすべてのアイテムに通知する方法があるのであればそちらでも構いません。ただし、TreeView や TreeViewItem のイベントを View (Window クラスなど) や ViewModel には記述しない方向で考えています。ListBox のアイテム選択を想定していただければわかりやすいかと思います(ListBox であればバインドしていようがいていまいがコントロールだけで複数選択を実現しているため)。
・また TreeView.Items から直接 ViewModel のクラスインスタンスにアクセスできますが、これを行うと ViewModel に依存してしまうのでこれは回避したいです。(インターフェースもありますがこれもインターフェース依存になるので不可)
◆現在実装しているプログラムの内容 (参考程度に)・現在関連処理はすべて「public class TreeViewExtensions : DependencyObject」クラス内に記述
・キー操作やマウスイベントは TreeView から「MouseDown」「KeyDown」「SelectedItemChanged」イベントを使用。
・以下の依存関係プロパティ実装
・TreeView
・bool : EnableMultiSelect (複数選択を有効無効にするかだけのフラグ)
・IList : SelectedItems (複数の選択アイテム)
・TreeViewItem : AnchorItem (アンカー位置設定用)
・TreeViewItem
・bool : IsSelected2 (もともとの IsSelected プロパティとぶつかるので今は避けています)
・TreeViewItem.IsSelected2 が変更されたときに PropertyChangedCallback でイベントを発行し TreeView.SelectedItems から対象アイテムを追加、削除しています。
・「MouseDown」イベントや「KeyDown」イベントで選択対象アイテムの TreeViewItem.IsSelected2 を変更。「Shift 選択」や「Ctrl+A 選択」では見えないアイテムを「VirtualizingPanel.BringIndexIntoView」で無理やり可視化しているがこれはこれで問題。
もしこれに似たような場面に遭遇して解決された方、また解決でなくても解決に向かう糸口でも構いませんので情報をお持ちの方がいらっしゃるのであればご回答の方お待ちしております。よろしくお願いします。