Используйте виртуализацию данных при привязке к WPF DataGrid и поддержку сортировки.

Я привязываю большую коллекцию (более 250 000 записей) к DataGrid. Чтобы это работало хорошо, оно должно использовать как виртуализацию пользовательского интерфейса, так и виртуализацию данных. После некоторых исследований я понял, как заставить работать обе виртуализации. Но как только я делаю сортировку, щелкнув заголовок столбца в DataGrid, он отказывается от виртуализации данных и пытается прочитать весь набор данных в память.

Вместо этого я хочу, чтобы он передал команду сортировки базовой коллекции, чтобы база данных выполняла сортировку перед извлечением данных с диска. Есть ли способ сделать это?


person bredd    schedule 19.03.2019    source источник


Ответы (1)


Я отвечаю на свой вопрос здесь в надежде помочь другим, имеющим дело с той же проблемой. Информация разбросана по нескольким статьям, и сообщество Stack Overflow оказало огромную помощь в ее выяснении.

Во-первых, основы. Виртуализация пользовательского интерфейса означает, что элемент управления (в данном случае DataGrid) создает объекты пользовательского интерфейса только для того, что можно увидеть на экране (плюс еще несколько для обеспечения быстрой прокрутки). Он встроен в DataGrid и включен по умолчанию. Таким образом, вам не нужно много делать, чтобы включить его. Подробности см. в этой статье .

Виртуализация данных означает только чтение соответствующих данных, которые видны на экране. Остальное остается в базе. Есть много ссылок на виртуализацию данных, но мне было трудно найти нужную статью. Это один из Майкрософт.

В моем случае я делаю виртуализацию с произвольным доступом. Резюмируя, моя коллекция должна реализовывать IList и INotifyCollectionChanged. При желании я также могу реализовать IItemsRangeInfo и ISelectionInfo, если они помогут.

Все идет нормально. Я создал тестовую коллекцию для имитации произвольного доступа к данным из базы данных. В этом случае он создал данные строки алгоритмически из индекса, чтобы я мог тестировать с произвольно большими виртуальными коллекциями и исключить производительность базы данных как фактор в этих тестах. Реализация IList и INotifyCollectionChanged работает. Я могу создать коллекцию с миллиардом записей и производительностью DataGrid практически мгновенно. Вы можете захватить полосу прокрутки и мгновенно перемещаться от начала к концу.

Два совета, которые помогут в создании коллекций, предназначенных для виртуализации данных. IList наследуется от IEnumerable. При работе с большой коллекцией с произвольным доступом вы не хотите, чтобы какие-либо вызывающие объекты перечисляли эту коллекцию. Однако DataGrid вызывает Enumerate один раз во время инициализации. Вы можете удовлетворить это, вернув пустую коллекцию. Для этой цели я создал одноэлементный пустой класс коллекции.

Другой метод IList, который вы не хотите вызывать, — это CopyTo. У меня просто этот метод вызывает исключение InvalidOperationException.

Это все работает. Однако, как только вы щелкаете заголовок столбца для выполнения сортировки, элемент управления пытается сделать копию всей коллекции. С миллиардом записей я получаю ошибку нехватки памяти. Похоже, реализация 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, либо в отделенном коде) и привязать его к представлению вместо коллекции (назначив его DataGrid.ItemsSource). Другой способ — реализовать ICollectionViewFactory в вашей коллекции и позволить ей создать собственное представление.

В этой структуре CollectionView делегирует сортировку и фильтрацию базовому классу коллекции (реализация IList). Таким образом, класс коллекции становится частью представления (или ModelView, используя терминологию MVVM), и между ними должна быть связь 1:1. Общая коллекция (или модель с использованием терминологии MVVM) является базовой базой данных. Чтобы подчеркнуть это, я экспериментировал с объединением обоих в один и тот же класс. Это можно сделать, но это будет сложно, потому что оба класса реализуют IList. Проще иметь два объекта, каждый со ссылкой на другой.

person bredd    schedule 19.03.2019