wpf – MVVM与数据虚拟化

我有一个绑定到ViewModel实例树的TreeView.问题是模型数据来自缓慢的存储库,因此我需要数据虚拟化.只应在展开父树视图节点时加载节点下的子ViewModel列表,并在折叠时卸载它.

如何在遵守MVVM原则的同时实施?如何通知ViewModel需要加载或卸载子节点?那是一个节点在没有了解树视图存在的情况下被展开或折叠的时候?

有些东西让我觉得数据虚拟化与MVVM不相符.由于在数据虚拟化中,ViewModel通常需要了解UI的当前状态,并且需要在UI中控制很多方面.举另一个例子:

具有数据虚拟化的列表视图. ViewModel需要控制ListView的scrollthumb的长度,因为它取决于Model中的项目数.此外,当用户滚动时,ViewModel需要知道他滚动到什么位置以及listview有多大(当前适合多少项)才能从存储库加载模型数据的正确部分.

最佳答案 解决这个问题的简单方法是使用“虚拟化集合”实现,该实现维护对其项目的弱引用以及用于获取/创建项目的算法.此集合的代码相当复杂,需要所有接口和数据结构以有效跟踪加载数据的范围,但这里是基于索引虚拟化的类的部分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和可以处理的整个范围的范围树,以及太稀疏的范围(例如,在1000个插槽的范围内只有一个WeakReference)

请注意,FetchItems会传递一系列卸载的项目,因此它可以使用启发式方法一次加载多个项目.一个简单的启发式方法是加载接下来的100个项目或者直到当前差距的末尾,以先到者为准.

使用VirtualizingCollection,只要您使用例如,WPF的内置虚拟化将在ListBox,ComboBox等适当的时间导致数据加载. VirtualizingStackPanel而不是StackPanel.

对于TreeView,还需要一个步骤:在HierarchicalDataTemplate中为ItemsSource设置MultiBinding,它绑定到您的真实ItemsSource以及模板化父级上的IsExpanded.如果第二个值(IsExpanded值)为true,则MultiBinding的转换器返回其第一个值(ItemsSource),否则返回null.这样做是为了使您在TreeView中折叠节点时立即删除对集合内容的所有引用,以便VirtualizingCollection可以清理它们.

请注意,无需基于索引完成虚拟化.在树情形中,它可以是全有或全无,并且在列表情形中,可以使用估计计数,并且根据需要使用“开始键”/“结束键”机制填充范围.当基础数据可能发生变化并且虚拟化视图应根据哪个键位于屏幕顶部时跟踪其当前位置时,这非常有用.

点赞