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

时间:2019-03-19 16:38:52

标签: c# wpf datagrid data-virtualization

我将一个大型集合(超过250,000条记录)绑定到一个DataGrid。为了使其正常运行,它必须同时使用UI虚拟化和数据虚拟化。经过一些研究,我弄清楚了如何使两种虚拟化都能工作。但是,只要我进行排序,就可以通过单击DataGrid中的列标题来放弃数据虚拟化,并尝试将整个数据集读取到内存中。

相反,我希望它将sort命令传递给基础集合,以便数据库在从磁盘检索数据之前执行排序。有办法吗?

1 个答案:

答案 0 :(得分: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绑定,该绑定列表执行数据虚拟化。

要将排序添加到Data Virtualization中,您必须将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术语的Model)是基础数据库。为了强调这一点,我尝试将两者合并到同一个类中。可以做到,但要棘手,因为两个类都实现了IList。拥有两个对象,每个对象都相互引用是容易的。