MVVM とデータ仮想化の比較
-
24-09-2019 - |
質問
ViewModel インスタンスのツリーにバインドされた TreeView があります。問題は、モデル データが低速のリポジトリから取得されているため、データの仮想化が必要であることです。ノードの下にあるサブ ViewModel のリストは、親ツリー ビュー ノードが展開されている場合にのみロードされ、折りたたまれている場合はアンロードされる必要があります。
MVVM の原則を遵守しながらこれを実装するにはどうすればよいでしょうか?サブノードをロードまたはアンロードする必要があることを ViewModel に通知するにはどうすればよいでしょうか?それは、ツリービューの存在について何も知らずにノードが展開または折りたたまれたときですか?
なんだかMVVMとデータ仮想化が相性が悪いような気がします。データ仮想化では、一般に ViewModel は UI の現在の状態について多くのことを知る必要があり、UI の非常に多くの側面を制御する必要があるためです。別の例を挙げてみましょう。
データ仮想化を備えたリストビュー。ListView のスクロールサムの長さはモデル内の項目の数に依存するため、ViewModel はこれを制御する必要があります。また、ユーザーがスクロールするとき、ViewModel はリポジトリからモデル データの適切な部分をロードできるように、ユーザーがどの位置までスクロールしたか、リストビューの大きさ (現在収まるアイテムの数) を知る必要があります。
解決
これを解決する簡単な方法は、アイテムのフェッチ/作成アルゴリズムとともにアイテムへの弱い参照を維持する「仮想化コレクション」実装を使用することです。このコレクションのコードはかなり複雑で、ロードされたデータの範囲を効率的に追跡するために必要なインターフェイスとデータ構造がすべて含まれていますが、インデックスに基づいて仮想化されたクラスの API の一部を次に示します。
public class VirtualizingCollection<T>
: IList<T>, ICollection<T>, IEnumerable<T>,
IList, ICollection, IEnumerable,
INotifyPropertyChanged, INotifyCollectionChanged
{
protected abstract void FetchItems(int requestedIndex, int gapStartIndex, int gapEndIndex);
protected void RecordFetchedItems(int startIndex, int count, IEnumerable items) ...
protected void RecordInsertOrDelete(int startIndex, int countPlusOrMinus) ...
protected virtual void OnCollectionChanged(CollectionChangedEventArgs e) ...
protected virtual void Cleanup();
}
ここでの内部データ構造は、データ範囲のバランスの取れたツリーであり、各データ範囲には開始インデックスと弱参照の配列が含まれています。
このクラスは、実際にデータをロードするためのロジックを提供するためにサブクラス化されるように設計されています。その仕組みは次のとおりです。
- サブクラスのコンストラクターでは、
RecordInsertOrDelete
初期コレクション サイズを設定するために呼び出されます - アイテムにアクセスするとき
IList/ICollection/IEnumerable
, 、ツリーはデータ項目を見つけるために使用されます。ツリー内で弱い参照が見つかり、その弱い参照が依然としてライフ オブジェクトを指している場合は、そのオブジェクトが返されます。そうでない場合は、そのオブジェクトがロードされて返されます。 - 項目をロードする必要がある場合、次/前のすでにロードされている項目のデータ構造を前方および後方に検索し、次に要約を検索することによってインデックス範囲が計算されます。
FetchItems
が呼び出されて、サブクラスが項目をロードできるようになります。 - サブクラス内
FetchItems
実装では、項目がフェッチされてから、RecordFetchedItems
範囲のツリーを新しい項目で更新するために呼び出されます。ツリーの過度の成長を防ぐために、隣接するノードをマージするには、ある程度の複雑さが必要です。 - サブクラスが外部データ変更の通知を受け取ると、サブクラスは呼び出すことができます。
RecordInsertOrDelete
インデックス追跡を更新します。これにより開始インデックスが更新されます。挿入の場合は範囲が分割される可能性があり、削除の場合は 1 つ以上の範囲を小さく再作成する必要がある場合があります。アイテムが追加または削除されるときに、これと同じアルゴリズムが内部で使用されます。IList
そしてIList<T>
インターフェース。 - の
Cleanup
メソッドがバックグラウンドで呼び出され、範囲のツリーを段階的に検索します。WeakReferences
破棄できる範囲全体、および疎すぎる範囲 (たとえば、1 つだけの範囲)WeakReference
1000 スロットの範囲内)
ご了承ください FetchItems
アンロードされたアイテムの範囲が渡されるため、ヒューリスティックを使用して複数のアイテムを一度にロードできます。このような単純なヒューリスティックは、次の 100 個のアイテムをロードするか、現在のギャップの終わりまで、どちらか先に来る方をロードすることになります。
とともに VirtualizingCollection
, WPF の組み込み仮想化により、適切なタイミングでデータが読み込まれます。 ListBox
, ComboBox
, 、などを使用している限り。 VirtualizingStackPanel
の代わりに StackPanel
.
のために TreeView
, 、もう 1 つの手順が必要です。の中に HierarchicalDataTemplate
を設定します MultiBinding
のために ItemsSource
それはあなたの本当の姿に結びつく ItemsSource
そしてまた IsExpanded
テンプレート化された親上で。のコンバーター MultiBinding
最初の値を返します ( ItemsSource
) 2 番目の値 ( IsExpanded
value) が true の場合、それ以外の場合は null を返します。これにより、ノードを折りたたんだときに、 TreeView
コレクションのコンテンツへのすべての参照はすぐに削除されるため、 VirtualizingCollection
それらをきれいにすることができます。
仮想化はインデックスに基づいて実行する必要はないことに注意してください。ツリー シナリオではオール オアナッシングにすることができ、リスト シナリオでは推定カウントを使用し、「開始キー」/「終了キー」メカニズムを使用して必要に応じて範囲を埋めることができます。これは、基になるデータが変更される可能性があり、仮想化ビューが画面の上部にあるキーに基づいて現在の位置を追跡する必要がある場合に便利です。
他のヒント
<TreeView
VirtualizingStackPanel.IsVirtualizing = "True"
VirtualizingStackPanel.VirtualizationMode = "Recycling"
VirtualizingStackPanel.CleanUpVirtualizedItem="TreeView_CleanUpVirtualizedItem">
<TreeView.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel />
</ItemsPanelTemplate>
</TreeView.ItemsPanel>
</TreeView>