【问题标题】:Use Data Virtualization when binding to WPF DataGrid and support Sorting绑定到 WPF DataGrid 并支持排序时使用数据虚拟化
【发布时间】:2019-08-10 06:33:34
【问题描述】:

我正在将一个大型集合(250,000 多条记录)绑定到一个 DataGrid。为此,它必须同时使用 UI 虚拟化和数据虚拟化。经过一番研究,我想出了如何让这两种虚拟化工作。但是一旦我进行排序,通过单击 DataGrid 中的列标题,它就会放弃数据虚拟化并尝试将整个数据集读入内存。

相反,我希望它将排序命令传递给底层集合,以便数据库在从磁盘检索数据之前执行排序。有没有办法做到这一点?

【问题讨论】:

    标签: c# wpf datagrid data-virtualization


    【解决方案1】:

    我在这里回答我自己的问题,希望能帮助其他人处理同样的问题。这些信息分布在多篇文章中,Stack Overflow 社区在解决这些问题方面提供了极大的帮助。

    首先,基础知识。 UI 虚拟化意味着控件(在本例中为 DataGrid)只为屏幕上可以看到的内容创建 UI 对象(加上更多以实现快速滚动)。它内置在 DataGrid 中并默认启用。因此,您无需做太多事情即可启用它。 See this article for details.

    数据虚拟化意味着只读取屏幕上可见的相应数据。其余的留在数据库中。有很多关于数据虚拟化的参考资料,但我发现很难找到合适的文章。 This is the one from Microsoft.

    就我而言,我正在做随机访问虚拟化。总结是我的集合应该实现IList和INotifyCollectionChanged。或者,如果 IItemsRangeInfo 和 ISelectionInfo 有帮助,我也可以实现它们。

    到目前为止,一切都很好。我创建了一个测试集合来模拟对数据库数据的随机访问。在这种情况下,它从索引中通过算法创建行数据,以便我可以使用任意大的虚拟集合进行测试,并消除数据库性能作为这些测试的一个因素。实施 IList 和 INotifyCollectionChanged 有效。我可以创建一个包含十亿条记录的集合,并且 DataGrid 性能几乎是瞬时的。您可以抓住滚动条并立即从头到尾移动。

    有助于制作用于数据虚拟化的集合的两个提示。 IList 继承自 IEnumerable。对于大型随机访问集合,您不希望任何调用者枚举该集合。但是,DataGrid 会在初始化期间调用一次 Enumerate。您可以通过返回一个空集合来满足这一点。为此,我创建了一个单例空集合类。

    另一个您不想被调用的 IList 方法是 CopyTo。我只是让那个方法抛出一个 InvalidOperationException。

    这一切都有效。但是,只要单击列标题执行排序,控件就会尝试复制整个集合。有十亿条记录,我得到一个内存不足的错误。似乎实现 IBindingList 应该可以解决这个问题,因为它提供了 DataGrid 需要的排序方法。但是,实现 IBindingList 会完全禁用数据虚拟化,从而导致控件在初始化期间尝试读取所有数据。

    答案在documentation for CollectionView。当控件(如 DataGrid 或 ListView)绑定到集合时,它使用 CollectionView 作为中介。这个想法是有一个共享集合(MVVM 术语中的模型),并且排序和过滤是在 CollectionView 而不是集合本身中实现的。这样,如果同一个集合出现在多个控件中,对一个集合进行排序不会影响其他集合。各种 CollectionView 实现通过制作绑定集合的影子副本并对影子进行排序来完成此操作。它在小型集合中运行良好,但对于数据虚拟化来说是一场灾难。

    数据绑定代码根据被绑定集合的接口清单选择视图。实现 IList 的集合由 ListCollectionView 绑定。如果该集合还实现了 INotifyCollectionChanged,则 ListCollectionView 将执行数据虚拟化(直到调用排序或过滤)。实现 IBindingListView 的集合由 BindingListCollectionView 绑定,后者执行数据虚拟化。

    要将排序添加到数据虚拟化,您必须继承 ListCollectionView,捕获排序请求,将它们传递给您的集合类,并阻止 ListCollectionView 制作影子副本。尽管我不得不咨询source code to ListCollectionView 来解决这个问题,但这非常容易。代码如下:

    class VirtualListCollectionView : ListCollectionView
    {
        VirtualCollection m_collection;
    
        public VirtualListCollectionView(VirtualCollection collection)
            : base(collection)
        {
            m_collection = collection;
        }
    
        protected override void RefreshOverride()
        {
            m_collection.SetSortInternal(SortDescriptions);
    
            // Notify listeners that everything has changed
            OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
    
            // The implementation of ListCollectionView saves the current item before updating the search
            // and restores it after updating the search. However, DataGrid, which is the primary client
            // of this view, does not use the current values. So, we simply set it to "beforeFirst"
            SetCurrent(null, -1);
        }
    }
    

    关键是覆盖“RefreshOverride()”。这就是制作不需要的卷影副本的地方。相反,覆盖将排序要求传递给关联的集合。自定义类上的特殊“SetSortInternal()”方法不会生成 INotifyCollectionChanged 事件。这很重要,因为该事件会导致对 RefreshOverride() 的递归调用。

    接下来,您必须使数据绑定使用您的自定义 CollectionView 类而不是默认类。有两种方法可以做到这一点。一种是自己创建 VirtualListCollectionView(在 XAML 或代码隐藏中)并绑定到视图而不是集合(通过将其分配给 DataGrid.ItemsSource)。另一种方法是在你的集合上实现 ICollectionViewFactory 并让它创建自己的视图。

    在此框架中,CollectionView 将排序和过滤委托给底层集合类(IList 实现)。因此,集合类成为视图的一部分(或使用 MVVM 术语的 ModelView),它们之间应该存在 1:1 的关系。共享集合(或使用 MVVM 术语的模型)是底层数据库。为了强调这一点,我尝试将两者合并到同一个类中。它可以完成,但它变得很棘手,因为这两个类都实现了 IList。拥有两个对象更容易,每个对象都引用另一个对象。

    【讨论】:

    • 写得很好的答案!
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2010-12-15
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多