c# - 绑定(bind)到 WPF DataGrid 时使用数据虚拟化并支持排序

标签 c# wpf datagrid data-virtualization

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

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

最佳答案

我在这里回答我自己的问题,希望能帮助其他人处理同样的问题。这些信息分布在多篇文章中,Stack Overflow 社区对弄清楚这一点非常有帮助。

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

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

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

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

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

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

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

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

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

要将排序添加到数据虚拟化中,您必须子类化 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() 的递归调用。

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

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

关于c# - 绑定(bind)到 WPF DataGrid 时使用数据虚拟化并支持排序,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/55245962/

相关文章:

apache-flex - 如何在Flex中控制Spark Datagrid的滚动器位置?

c# - WPF 网格不显示内容

C# 打印包含多个值的 ArrayList

c# - RSA 加密 - 使用私钥加密

c# - 使用 SQL 在 C# 中使用 Combobox

c# - 如何清除单击按钮上的文本框

c# - 如果在 RowDetails 中单击鼠标,如何阻止 DataGridRow 选择

c# - 列表框中元素之间的分隔线

c# - 同时调用 3 个函数。应该使用 Parallel.For 吗?

wpf - 为什么两个模型不应该互相交谈