题
我有一个绑定到 ViewModel 实例树的 TreeView。问题是模型数据来自缓慢的存储库,所以我需要数据虚拟化。节点下方的子 ViewModel 列表仅应在父树视图节点展开时加载,在折叠时卸载。
如何在遵守 MVVM 原则的情况下实现这一点?ViewModel 如何收到需要加载或卸载子节点的通知?也就是说,在不知道树视图存在的情况下展开或折叠节点?
有些事情让我觉得数据虚拟化与 MVVM 不太相配。由于在数据虚拟化中,ViewModel 通常需要了解有关 UI 的当前状态的大量信息,并且还需要控制 UI 中的许多方面。再举个例子:
具有数据虚拟化的列表视图。ViewModel 需要控制 ListView 的滚动拇指的长度,因为它取决于模型中的项目数。此外,当用户滚动时,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
更新索引跟踪。这会更新开始索引。对于插入,这也可能会分割一个范围,而对于删除,这可能需要重新创建一个或多个更小的范围。当通过添加/删除项目时,内部使用相同的算法IList
和IList<T>
接口。 - 这
Cleanup
方法在后台调用以增量搜索范围树WeakReferences
以及可以处理的整个范围,以及过于稀疏的范围(例如只有一个WeakReference
在 1000 个插槽的范围内)
注意 FetchItems
传递了一系列已卸载的项目,因此它可以使用启发式方法一次加载多个项目。一个简单的启发式方法是加载接下来的 100 个项目或直到当前间隙的末尾,以先到者为准。
与一个 VirtualizingCollection
, ,WPF的内置虚拟化将导致数据在适当的时间加载 ListBox
, ComboBox
, 等等,只要您使用例如。 VirtualizingStackPanel
代替 StackPanel
.
为一个 TreeView
, ,还需要一步:在里面 HierarchicalDataTemplate
设置一个 MultiBinding
为了 ItemsSource
与你的真实绑定 ItemsSource
并且还 IsExpanded
在模板化父级上。转换器为 MultiBinding
返回其第一个值( ItemsSource
)如果第二个值( 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>