MVVM 与数据虚拟化

发布于 2024-08-19 14:37:48 字数 437 浏览 5 评论 0原文

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

如何在遵守 MVVM 原则的情况下实现这一点? ViewModel 如何收到需要加载或卸载子节点的通知?也就是说,在不知道树视图存在的情况下展开或折叠节点?

有些事情让我觉得数据虚拟化与 MVVM 不太相配。由于在数据虚拟化中,ViewModel 通常需要了解有关 UI 的当前状态的大量信息,并且还需要控制 UI 中的许多方面。再举一个例子:

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

I have a TreeView that is bound to a tree of ViewModel instances. The problem is that the model data is coming from a slow repository so I need data virtualization. The list of sub ViewModel below a node should only be loaded when the parent tree view node is expanded and it should be unloaded when it is collapsed.

How can this be implemented while adhering to MVVM principles? How can the ViewModel get notified that it needs to load or unload subnodes? That is when a node was expanded or collapsed without knowning anything about the treeview's existence?

Something makes me feel that data virtualization doesn't go well with MVVM. Since in data virtualization the ViewModel generally needs to know quite a lot about the current state of the UI and aslo needs to control quite a lot of aspects in the UI. Take another example:

A listview with data virtualization. The ViewModel would need to control the length of the ListView's scrollthumb since it depends on the number of items in the Model. Also when the user scrolls, the ViewModel would need to known to what position he scrolled to and how big the listview is (how many items currently fit in) to be able to load the right portion of Model data form the repository.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

森林散布 2024-08-26 14:37:48

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

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

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

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

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

The easy way to solve this is with a "virtualizing collection" implementation that maintains weak references to its items along with an algorithm for fetching / creating items. The code for this collection is rather complex, what with all the interfaces required and the data structures to efficiently track ranges of loaded data but here is a partial API for a class that virtualized based on indexes:

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();
}

The internal data structure here is a balanced tree of data ranges, with each data range containing a start index and an array of weak references.

This class is designed to be subclassed to provide the logic for actually loading the data. Here is how it works:

  • In the subclass constructor, RecordInsertOrDelete is called to set the initial collection size
  • When an item is accessed using IList/ICollection/IEnumerable, the tree is used to find the data item. If found in the tree and there is a weak reference and the weak reference still points to a life object, that object is returned, otherwise it is loaded and returned.
  • When an item needs to be loaded, an index range is computed by searching forward and back though the data structure for the next/previous already-loaded item, then the abstract FetchItems is called so the subclass can load the items.
  • In the subclass FetchItems implementation, items are fetched and then RecordFetchedItems is called to update the tree of ranges with the new items. Some complexity here is required to merge adjacent nodes to prevent too much tree growth.
  • When the subclass gets a notification of external data changes it can call RecordInsertOrDelete to update the index tracking. This updates start indexes. For an insert, this may also split a range, and for a delete this may require one or more ranges to be recreated smaller. This same algorithm is used internally when items are added / deleted through the IList and IList<T> interfaces.
  • The Cleanup method is called in the background to incrementally search the tree of ranges for WeakReferences and whole ranges that can be disposed, and also for ranges that are too sparse (eg only one WeakReference in a range with 1000 slots)

Note that FetchItems is passed a range of unloaded items so it can use a heuristic to load multiple items at once. A simple such heuristic would be loading the next 100 items or up to the end of the current gap, whichever comes first.

With a VirtualizingCollection, WPF's built-in virtualization will cause data loading at the appropriate times for ListBox, ComboBox, etc, as long as you are using eg. VirtualizingStackPanel instead of StackPanel.

For a TreeView, one more step is required: In the HierarchicalDataTemplate set a MultiBinding for ItemsSource that binds to your real ItemsSource and also to IsExpanded on the templated parent. The converter for the MultiBinding returns its first value (the ItemsSource) if the second value (the IsExpanded value) is true, otherwise it returns null. What this does is makes it so that when you collapse a node in the TreeView all references to the collection contents are immediately dropped so that VirtualizingCollection can clean them up.

Note that virtualization need not be done based on indexes. In a tree scenario, it can be all-or-nothing, and in a list scenario an estimated count can be used and ranges filled in as necessary using a "start key" / "end key" mechanism. This is useful when the underlying data may change and the virtualized view should track its current location based on which key is at the top of the screen.

梦境 2024-08-26 14:37:48

请尝试这个。

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

    <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>

Please, try this.

TreeView that sets the VirtualizingStackPanel.IsVirtualizing attached property to true and the VirtualizingStackPanel.VirtualizationMode attached property to VirtualizationMode.Recycling to optimize its performance.

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

or this one also

 <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>
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文