我有一个绑定到ViewModel实例树的TreeView。问题是模型数据来自缓慢的存储库,因此我需要数据虚拟化。只应在展开父树视图节点时加载节点下的子ViewModel列表,并在折叠时卸载它。
如何在遵守MVVM原则的同时实施?如何通知ViewModel需要加载或卸载子节点?那就是当一个节点被扩展或折叠而没有了解树视图的存在时呢?
有些东西让我觉得数据虚拟化与MVVM不太合适。由于在数据虚拟化中,ViewModel通常需要了解UI的当前状态,并且需要在UI中控制很多方面。再举一个例子:
包含数据虚拟化的列表视图。 ViewModel需要控制ListView的scrollthumb的长度,因为它取决于Model中的项目数。此外,当用户滚动时,ViewModel需要知道他滚动到什么位置以及listview有多大(当前适合的项目数),以便能够从存储库加载模型数据的正确部分。
答案 0 :(得分:4)
解决这个问题的简单方法是使用“虚拟化集合”实现,该实现维护对其项目的弱引用以及用于获取/创建项目的算法。这个集合的代码相当复杂,需要所有接口和有效跟踪加载数据范围的数据结构,但这里是基于索引虚拟化的类的部分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
的范围树和可以处置的整个范围,以及过于稀疏的范围(例如,只有一个{{} 1}}在1000个插槽的范围内)请注意,WeakReference
会传递一系列已卸载的项目,因此可以使用启发式方法一次加载多个项目。一个简单的启发式方法是加载接下来的100个项目,或者直到当前差距的末尾,以先到者为准。
使用FetchItems
,WPF的内置虚拟化将在VirtualizingCollection
,ListBox
等适当的时间导致数据加载,只要您使用例如。 ComboBox
代替VirtualizingStackPanel
。
对于StackPanel
,还需要再做一步:在TreeView
设置一个HierarchicalDataTemplate
MultiBinding
,ItemsSource
绑定到您的真实ItemsSource
,并且模板化父级的IsExpanded
。如果第二个值(MultiBinding
值)为真,则ItemsSource
的转换器返回其第一个值(IsExpanded
),否则返回null。这样做是为了让你在TreeView
中折叠节点时立即删除所有对集合内容的引用,以便VirtualizingCollection
可以清理它们。
请注意,无需基于索引进行虚拟化。在树情形中,它可以是全有或全无,并且在列表情形中,可以使用估计计数,并且根据需要使用“开始键”/“结束键”机制填充范围。当基础数据可能发生变化且虚拟化视图应根据屏幕顶部的哪个键跟踪其当前位置时,此功能非常有用。
答案 1 :(得分:-2)
<TreeView
VirtualizingStackPanel.IsVirtualizing = "True"
VirtualizingStackPanel.VirtualizationMode = "Recycling"
VirtualizingStackPanel.CleanUpVirtualizedItem="TreeView_CleanUpVirtualizedItem">
<TreeView.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel />
</ItemsPanelTemplate>
</TreeView.ItemsPanel>
</TreeView>