【问题标题】:MVVM vs Data VirtualizationMVVM 与数据虚拟化
【发布时间】:2010-01-29 16:38:35
【问题描述】:

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

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

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

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

【问题讨论】:

    标签: wpf mvvm data-virtualization


    【解决方案1】:

    解决此问题的简单方法是使用“虚拟化集合”实现,该实现维护对其项目的弱引用以及用于获取/创建项目的算法。这个集合的代码相当复杂,需要所有接口和数据结构来有效跟踪加载数据的范围,但这里是基于索引虚拟化的类的部分 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 来更新索引跟踪。这会更新开始索引。对于插入,这也可能拆分一个范围,而对于删除,这可能需要重新创建一个或多个范围更小。当通过IListIList&lt;T&gt; 接口添加/删除项目时,内部使用相同的算法。
    • 在后台调用Cleanup 方法以增量方式搜索范围树中的WeakReferences 和可以处理的整个范围,以及过于稀疏的范围(例如,一个范围中只有一个WeakReference有 1000 个插槽)

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

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

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

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

    【讨论】:

      【解决方案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>
      

      【讨论】:

        猜你喜欢
        • 2013-11-27
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2011-10-20
        • 1970-01-01
        • 1970-01-01
        • 2013-03-31
        相关资源
        最近更新 更多