4

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

在遵守 MVVM 原则的同时如何实现这一点?ViewModel 如何获得需要加载或卸载子节点的通知?那是在不知道树视图存在的情况下展开或折叠节点的时候?

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

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

4

2 回答 2

6

解决这个问题的简单方法是使用“虚拟化集合”实现来维护对其项目的弱引用以及用于获取/创建项目的算法。这个集合的代码相当复杂,需要所有接口和数据结构来有效跟踪加载数据的范围,但这里是基于索引虚拟化的类的部分 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.

对于 a TreeView,还需要一个步骤:在HierarchicalDataTemplate集合中 a MultiBindingforItemsSource绑定到您的真实对象ItemsSource以及IsExpanded模板化父对象。如果第二个值(值)为真,则的转换器MultiBinding返回其第一个值(the ) ,否则返回 null。这样做的目的是,当您折叠一个节点时,所有对集合内容的引用都会立即被删除,以便清理它们。ItemsSourceIsExpandedTreeViewVirtualizingCollection

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

于 2010-01-29T19:26:35.540 回答
-2

请试试这个。

将 VirtualizingStackPanel.IsVirtualizing 附加属性设置为 true 并将 VirtualizingStackPanel.VirtualizationMode 附加属性设置为 VirtualizationMode.Recycling 以优化其性能的 TreeView。

    <TreeView VirtualizingStackPanel.IsVirtualizing = "True" VirtualizingStackPanel.VirtualizationMode = "Recycling" VirtualizingStackPanel.CleanUpVirtualizedItem="TreeView_CleanUpVirtualizedItem">
            <TreeView.ItemsPanel>
                <ItemsPanelTemplate>
                    <VirtualizingStackPanel />
                </ItemsPanelTemplate>
            </TreeView.ItemsPanel>
        </TreeView>

或者这个也是

 <TreeView Height="200" 
        ItemsSource="{Binding Source={StaticResource dataItems}}"
        VirtualizingStackPanel.IsVirtualizing="True"
        VirtualizingStackPanel.VirtualizationMode="Recycling">
<TreeView.ItemContainerStyle>
  
  <!--Expand each TreeViewItem in the first level and 
      set its foreground to Green.-->
  <Style TargetType="TreeViewItem">
    <Setter Property="IsExpanded" Value="True"/>
    <Setter Property="Foreground" Value="Green"/>
  </Style>
</TreeView.ItemContainerStyle>
于 2015-07-19T01:21:26.397 回答