在绑定到WPF DataGrid时使用数据虚拟化并支持排序

2admgd59  于 2023-08-07  发布在  其他
关注(0)|答案(1)|浏览(131)

我正在将一个大型集合(250,000多条记录)绑定到DataGrid。要使其表现良好,它必须同时使用UI虚拟化和数据虚拟化。经过一些研究,我找到了如何让这两个虚拟化工作。但是,只要我通过单击DataGrid中的列标题进行排序,它就会放弃数据虚拟化,并尝试将整个数据集读入内存。
相反,我希望它将sort命令传递给底层集合,以便数据库在从磁盘检索数据之前执行排序。有办法做到这一点吗?

thigvfpy

thigvfpy1#

我在这里回答我自己的问题,希望能帮助其他人处理同样的问题。这些信息分布在多篇文章中,Stack Overflow社区在解决这些问题方面提供了极大的帮助。
首先是基础。UI虚拟化意味着控件(在本例中为DataGrid)只为屏幕上可见的内容创建UI对象(再加上一些用于快速滚动的对象)。它内置在DataGrid中,默认情况下启用。因此,您无需执行太多操作即可启用它。详情请参阅本文。
数据虚拟化意味着只阅读屏幕上可见的相应数据。其余的都留在数据库中。有很多关于数据虚拟化的参考文献,但我发现很难找到合适的文章。This is the one from Microsoft的数据。
在我的例子中,我正在做随机访问虚拟化。我的集合应该实现IList和INotifyCollectionChanged。可选地,我还可以实现IItemsRangeInfo和ISelectionInfo,如果它们有帮助的话。
到目前为止,一切顺利。我创建了一个测试集合来模拟随机访问数据库中的数据。在本例中,它通过算法从索引中创建行数据,这样我就可以使用任意大的虚拟集合进行测试,并消除数据库性能作为这些测试中的一个因素。实现IList和INotifyCollectionChanged工作。我可以创建一个拥有十亿条记录的集合,并且数据网格的性能几乎是瞬时的。你可以抓住滚动条,从开始移动到结束瞬间。
两个提示有助于创建旨在用于数据虚拟化的集合。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绑定,BindingListCollectionView执行数据虚拟化。
要将排序添加到Data Virtualization中,您必须子类化ListCollectionView,捕获排序请求,将其传递到您的集合类,并停止ListCollectionView制作卷影副本。这是令人惊讶的容易,虽然我不得不查阅源代码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术语的Model)是底层数据库。为了强调这一点,我尝试将两者合并到同一个类中。这是可以做到的,但它变得棘手,因为两个类都实现了IList。有两个对象更容易,每个对象都有对另一个对象的引用。

相关问题