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

11
我正在将一个非常大的集合(超过250,000条记录)绑定到DataGrid中。为了使其性能良好,必须同时使用UI虚拟化和数据虚拟化。经过一些研究,我找到了如何让两种虚拟化都工作的方法。但是,一旦我进行排序,通过在DataGrid中单击列标题,它就会放弃数据虚拟化,尝试读取整个数据集到内存中。

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

1个回答

17
我在回答自己的问题,希望能帮助其他遇到同样问题的人。相关信息分散在多篇文章中,Stack Overflow社区帮助我解决了很多问题。
首先,基础知识。UI虚拟化意味着控件(在这个例子中是DataGrid)只为可以在屏幕上看到的内容创建UI对象(加上一些额外的内容以实现快速滚动)。它内置于DataGrid中,并默认启用。所以,你不需要做太多就可以启用它。详情请参阅本文
数据虚拟化意味着仅读取对应屏幕上可见的数据。其余数据留在数据库中。有很多关于数据虚拟化的参考资料,但我发现很难找到正确的文章。这是微软的文章
在我的情况下,我正在进行随机访问虚拟化。简而言之,我的集合应该实现IList和INotifyCollectionChanged。如果需要,我还可以实现IItemsRangeInfo和ISelectionInfo。
到目前为止,一切都很好。我创建了一个测试集合来模拟从数据库中随机访问数据。在这种情况下,它通过索引算法创建行数据,以便我可以使用任意大的虚拟集合进行测试,并消除数据库性能在这些测试中的影响。实现IList和INotifyCollectionChanged是有效的。我可以创建一个有十亿条记录的集合,并且DataGrid的性能几乎是即时的。你可以抓住滚动条,瞬间从头到尾移动。
制作用于数据虚拟化的集合的两个提示。IList继承自IEnumerable。对于一个大型的、随机访问的集合,你不希望任何调用者枚举该集合。然而,DataGrid在初始化期间会调用一次Enumerate。你可以通过返回一个空集合来满足这个需求。我为此创建了一个单例空集合类。
另一个不希望被调用的IList方法是CopyTo。我只需让该方法抛出一个InvalidOperationException异常即可。
这一切都有效。然而,一旦你点击列标题执行排序操作,控件就会尝试复制整个集合。如果有10亿条记录,我会得到一个内存不足错误。实现IBindingList似乎可以解决这个问题,因为它提供了DataGrid所需的排序方法。然而,实现IBindingList会完全禁用数据虚拟化,导致控件在初始化期间尝试读取所有数据。
答案在CollectionView的文档中。当像DataGrid或ListView这样的控件绑定到一个集合时,它使用CollectionView作为中介。想法是有一个共享的集合(MVVM术语中的模型),排序和过滤是在CollectionView中实现,而不是在集合本身中实现。这样,如果相同的集合出现在多个控件中,对一个进行排序不会影响其他控件。各种CollectionView实现通过制作绑定集合的阴影副本并对其进行排序来实现这一点。在小型集合中效果很好,但对于数据虚拟化来说却是灾难性的。
数据绑定代码根据集合绑定的接口清单选择视图。实现了IList接口的集合由ListCollectionView绑定。如果该集合还实现了INotifyCollectionChanged,则ListCollectionView将执行数据虚拟化(直到调用排序或过滤)。实现了IBindingListView接口的集合由BindingListCollectionView绑定,它不执行数据虚拟化。
要为数据虚拟化添加排序,必须对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或codebehind中),并绑定到视图而不是集合(通过将其分配给DataGrid.ItemsSource)。另一种方法是在集合上实现ICollectionViewFactory,并让它创建自己的视图。
在此框架中,CollectionView将排序和过滤委托给底层集合类(IList实现)。因此,集合类成为视图(或使用MVVM术语的ModelView)的一部分,它们之间应该是1:1的关系。共享集合(或使用MVVM术语的Model)是底层数据库。为了强调这一点,我尝试将两者合并到同一个类中。它可以完成,但由于两个类都实现了IList,所以变得棘手。拥有两个对象,每个对象都引用另一个对象更容易。

非常好的撰写和研究答案! - SolarBear
非常感谢您!我和您有同样的问题。您是否有演示这些概念的代码示例? - Jeson Martajaya

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接