VirtualizingStackPanel с виртуализированным списком

Я разрабатываю приложение, которое должно отображать довольно большое количество элементов, которые загружаются из других источников (скажем, из базы данных) в виде списка / сетки.

Поскольку хранение всех элементов в памяти все время кажется пустой тратой, я ищу способы виртуализации части моего списка. VirtualizingStackPanel похоже на то, что мне нужно, однако, несмотря на то, что он, кажется, хорошо справляется с виртуализацией пользовательского интерфейса элементов, я не уверен, как виртуализировать части самого базового списка элементов.

В качестве небольшого примера рассмотрим приложение WPF со следующим главным окном:

<Window x:Class="VSPTest.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="VSPTest" Height="300" Width="300">
    <Window.Resources>
        <DataTemplate x:Key="itemTpl">
            <Border BorderBrush="Blue" BorderThickness="2" CornerRadius="5" Margin="2" Padding="4" Background="Chocolate">
                <Border BorderBrush="Red" BorderThickness="1" CornerRadius="4" Padding="3" Background="Yellow">
                    <TextBlock Text="{Binding Index}"/>
                </Border>
            </Border>
        </DataTemplate>
    </Window.Resources>
    <Border Padding="5">
        <ListBox VirtualizingStackPanel.IsVirtualizing="True" ItemsSource="{Binding .}" ItemTemplate="{StaticResource itemTpl}" VirtualizingStackPanel.CleanUpVirtualizedItem="ListBox_CleanUpVirtualizedItem">
            <ListBox.ItemContainerStyle>
                <Style TargetType="{x:Type ListBoxItem}">
                    <Setter Property="HorizontalContentAlignment" Value="Stretch"/>
                </Style>
            </ListBox.ItemContainerStyle>
        </ListBox>
    </Border>
</Window>

Код, который предоставляет список, должен выглядеть так:

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Windows;
using System.Windows.Controls;

namespace VSPTest
{
    public partial class Window1 : Window
    {
        private class DataItem
        {
            public DataItem(int index)
            {
                this.index = index;
            }

            private readonly int index;

            public int Index {
                get {
                    return index;
                }
            }

            public override string ToString()
            {
                return index.ToString();
            }
        }

        private class MyTestCollection : IList<DataItem>
        {
            public MyTestCollection(int count)
            {
                this.count = count;
            }

            private readonly int count;

            public DataItem this[int index] {
                get {
                    var result = new DataItem(index);
                    System.Diagnostics.Debug.WriteLine("ADD " + result.ToString());
                    return result;
                }
                set {
                    throw new NotImplementedException();
                }
            }

            public int Count {
                get {
                    return count;
                }
            }

            public bool IsReadOnly {
                get {
                    throw new NotImplementedException();
                }
            }

            public int IndexOf(Window1.DataItem item)
            {
                throw new NotImplementedException();
            }

            public void Insert(int index, Window1.DataItem item)
            {
                throw new NotImplementedException();
            }

            public void RemoveAt(int index)
            {
                throw new NotImplementedException();
            }

            public void Add(Window1.DataItem item)
            {
                throw new NotImplementedException();
            }

            public void Clear()
            {
                throw new NotImplementedException();
            }

            public bool Contains(Window1.DataItem item)
            {
                throw new NotImplementedException();
            }

            public void CopyTo(Window1.DataItem[] array, int arrayIndex)
            {
                throw new NotImplementedException();
            }

            public bool Remove(Window1.DataItem item)
            {
                throw new NotImplementedException();
            }

            public IEnumerator<Window1.DataItem> GetEnumerator()
            {
                for (int i = 0; i < count; i++) {
                    yield return this[i];
                }
            }

            System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
            {
                return this.GetEnumerator();
            }
        }

        public Window1()
        {
            InitializeComponent();

            DataContext = new MyTestCollection(10000);
        }

        void ListBox_CleanUpVirtualizedItem(object sender, CleanUpVirtualizedItemEventArgs e)
        {
            System.Diagnostics.Debug.WriteLine("DEL " + e.Value.ToString());
        }
    }
}

Таким образом, это отображает приложение с ListBox, который вынужден виртуализировать свои элементы с IsVirtualizing собственность Он берет свои элементы из контекста данных, для которых пользовательский IList<T> поставляется реализация, которая создает 10000 элементов данных на лету (когда они извлекаются через индексатор).

В целях отладки текст ADD # (где # равен элементу), выводится при каждом создании элемента, а CleanUpVirtualizedItem событие используется для вывода DEL # когда элемент выходит из поля зрения и его интерфейс освобождается панелью стека виртуализации.

Теперь я хочу, чтобы моя пользовательская реализация списка предоставляла элементы по запросу - в этом минимальном примере, создавая их на лету, и в реальном проекте, загружая их из базы данных. К несчастью, VirtualizingStackPanel похоже, ведет себя не так - вместо этого он запускает перечислитель списка при запуске программы и сначала получает все 10000 элементов!

Таким образом, мой вопрос: как я могу использовать VirtualizingStackPanel для фактической виртуализации данных (например, не загружая все данные), а не просто уменьшить количество элементов GUI?

  • Есть ли способ сообщить панели стека виртуализации, сколько всего элементов, и попросить ее обращаться к ним по индексу по мере необходимости, а не с помощью перечислителя? (Как, например, компонент Delphi Virtual TreeView работает, если я правильно помню.)
  • Существуют ли какие-либо изобретательные способы захвата события, когда элемент фактически появляется в поле зрения, так что, по крайней мере, я обычно могу просто хранить уникальный ключ каждого элемента и загружать оставшиеся данные элемента только тогда, когда он запрашивается? (Хотя это может показаться хакерским решением, поскольку мне все равно придется предоставлять полный список без какой-либо реальной причины, кроме как для удовлетворения API WPF.)
  • Другой класс WPF больше подходит для такого рода виртуализации?

РЕДАКТИРОВАТЬ: Следуя совету dev hedgehog, я создал обычай ICollectionView реализация. Некоторые из его методов все еще реализованы, чтобы бросить NotImplementedException s, но те, которые вызываются при открытии окна, этого не делают.

Однако, похоже, что первое, что вызывается для этого представления коллекции, это GetEnumerator метод, снова перечисляющий все 10000 элементов (о чем свидетельствуют выходные данные отладки, где я печатаю сообщение для каждого 1000-го элемента), чего я и пытался избежать.

Вот пример, чтобы воспроизвести проблему:

Window1.xaml

<Window x:Class="CollectionViewTest.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="CollectionViewTest" Height="300" Width="300"
    >
    <Border Padding="5">
        <ListBox VirtualizingStackPanel.IsVirtualizing="True" ItemsSource="{Binding .}">
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <Border BorderBrush="Blue" BorderThickness="2" CornerRadius="5" Margin="2" Padding="4" Background="Chocolate">
                        <Border BorderBrush="Red" BorderThickness="1" CornerRadius="4" Padding="3" Background="Yellow">
                            <TextBlock Text="{Binding Index}"/>
                        </Border>
                    </Border>
                </DataTemplate>
            </ListBox.ItemTemplate>
            <ListBox.ItemContainerStyle>
                <Style TargetType="{x:Type ListBoxItem}">
                    <Setter Property="HorizontalContentAlignment" Value="Stretch"/>
                </Style>
            </ListBox.ItemContainerStyle>
        </ListBox>
    </Border>
</Window>

Window1.xaml.cs

using System;
using System.ComponentModel;
using System.Collections;
using System.Collections.Specialized;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Windows;

namespace CollectionViewTest
{
    public partial class Window1 : Window
    {
        private class DataItem
        {
            public DataItem(int index)
            {
                this.index = index;
            }

            private readonly int index;

            public int Index {
                get {
                    return index;
                }
            }

            public override string ToString()
            {
                return index.ToString();
            }
        }

        private class MyTestCollection : IList<DataItem>
        {
            public MyTestCollection(int count)
            {
                this.count = count;
            }

            private readonly int count;

            public DataItem this[int index] {
                get {
                    var result = new DataItem(index);
                    if (index % 1000 == 0) {
                        System.Diagnostics.Debug.WriteLine("ADD " + result.ToString());
                    }
                    return result;
                }
                set {
                    throw new NotImplementedException();
                }
            }

            public int Count {
                get {
                    return count;
                }
            }

            public bool IsReadOnly {
                get {
                    throw new NotImplementedException();
                }
            }

            public int IndexOf(Window1.DataItem item)
            {
                throw new NotImplementedException();
            }

            public void Insert(int index, Window1.DataItem item)
            {
                throw new NotImplementedException();
            }

            public void RemoveAt(int index)
            {
                throw new NotImplementedException();
            }

            public void Add(Window1.DataItem item)
            {
                throw new NotImplementedException();
            }

            public void Clear()
            {
                throw new NotImplementedException();
            }

            public bool Contains(Window1.DataItem item)
            {
                throw new NotImplementedException();
            }

            public void CopyTo(Window1.DataItem[] array, int arrayIndex)
            {
                throw new NotImplementedException();
            }

            public bool Remove(Window1.DataItem item)
            {
                throw new NotImplementedException();
            }

            public IEnumerator<Window1.DataItem> GetEnumerator()
            {
                for (int i = 0; i < count; i++) {
                    yield return this[i];
                }
            }

            System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
            {
                return this.GetEnumerator();
            }
        }

        private class MyCollectionView : ICollectionView
        {
            public MyCollectionView(int count)
            {
                this.list = new MyTestCollection(count);
            }

            private readonly MyTestCollection list;

            public event CurrentChangingEventHandler CurrentChanging;

            public event EventHandler CurrentChanged;

            public event NotifyCollectionChangedEventHandler CollectionChanged;

            public System.Globalization.CultureInfo Culture {
                get {
                    return System.Globalization.CultureInfo.InvariantCulture;
                }
                set {
                    throw new NotImplementedException();
                }
            }

            public IEnumerable SourceCollection {
                get {
                    return list;
                }
            }

            public Predicate<object> Filter {
                get {
                    throw new NotImplementedException();
                }
                set {
                    throw new NotImplementedException();
                }
            }

            public bool CanFilter {
                get {
                    return false;
                }
            }

            public SortDescriptionCollection SortDescriptions {
                get {
                    return new SortDescriptionCollection();
                }
            }

            public bool CanSort {
                get {
                    throw new NotImplementedException();
                }
            }

            public bool CanGroup {
                get {
                    throw new NotImplementedException();
                }
            }

            public ObservableCollection<GroupDescription> GroupDescriptions {
                get {
                    return new ObservableCollection<GroupDescription>();
                }
            }

            public ReadOnlyObservableCollection<object> Groups {
                get {
                    throw new NotImplementedException();
                }
            }

            public bool IsEmpty {
                get {
                    throw new NotImplementedException();
                }
            }

            public object CurrentItem {
                get {
                    return null;
                }
            }

            public int CurrentPosition {
                get {
                    throw new NotImplementedException();
                }
            }

            public bool IsCurrentAfterLast {
                get {
                    throw new NotImplementedException();
                }
            }

            public bool IsCurrentBeforeFirst {
                get {
                    throw new NotImplementedException();
                }
            }

            public bool Contains(object item)
            {
                throw new NotImplementedException();
            }

            public void Refresh()
            {
                throw new NotImplementedException();
            }

            private class DeferRefreshObject : IDisposable
            {
                public void Dispose()
                {
                }
            }

            public IDisposable DeferRefresh()
            {
                return new DeferRefreshObject();
            }

            public bool MoveCurrentToFirst()
            {
                throw new NotImplementedException();
            }

            public bool MoveCurrentToLast()
            {
                throw new NotImplementedException();
            }

            public bool MoveCurrentToNext()
            {
                throw new NotImplementedException();
            }

            public bool MoveCurrentToPrevious()
            {
                throw new NotImplementedException();
            }

            public bool MoveCurrentTo(object item)
            {
                throw new NotImplementedException();
            }

            public bool MoveCurrentToPosition(int position)
            {
                throw new NotImplementedException();
            }

            public IEnumerator GetEnumerator()
            {
                return list.GetEnumerator();
            }
        }

        public Window1()
        {
            InitializeComponent();
            this.DataContext = new MyCollectionView(10000);
        }
    }
}

4 ответа

Ты хочешь Data Virtualization, у тебя есть UI Virtualization прямо сейчас.

Вы можете посмотреть больше о виртуализации данных здесь

Чтобы обойти проблему, где VirtualizingStackPanel пытаясь перечислить весь его источник данных, я прошелся по исходному коду на http://referencesource.microsoft.com/ ( https://referencesource.microsoft.com/#PresentationFramework/src/Framework/System/Windows/Controls/VirtualizingStackPanel.cs)

Я предоставлю TLDR здесь:

  • Если вы указали VirtualizingStackPanel.ScrollUnit="Pixel" Вы должны убедиться, что все элементы отображаются / виртуализированы из его ItemTemplate одинакового размера (высоты). Даже если у вас другой пиксель, все ставки выключены, и вы, скорее всего, вызовете загрузку всего списка.

  • Если отображаемые элементы имеют разную высоту, необходимо указать VirtualizingStackPanel.ScrollUnit="Item",

Мои выводы:

Есть несколько "мин" в VirtualizingStackPanel источник, инициирующий попытку перебора всей коллекции с помощью оператора индекса [], Один из них - во время цикла измерения, в котором он пытается обновить размер виртуализированного контейнера, чтобы обеспечить точность просмотра прокрутки. Если какие-либо новые элементы, добавляемые в течение этого цикла, имеют разный размер Pixel режим, он перебирает весь список для настройки, и вы попали.

Другая "мина" как-то связана с выбором и активным обновлением. Это применимо больше для сеток - но под капотом, его использование DataGridRowPresenter который вытекает из VirtualizingStackPanel, Поскольку он хочет синхронизировать выборки между обновлениями, он пытается перечислить все. Это означает, что нам нужно отключить выделение (имейте в виду, что нажатие на строку вызывает выделение).

Я решил это путем получения моей собственной сетки и переопределения OnSelectionChanged:

protected override void OnSelectionChanged(SelectionChangedEventArgs e)
{
    if(SelectedItems.Count > 0)
    {
        UnselectAll();
    }
    e.Handled = true;
}

Кажется, есть и другие ошибки, но я пока не смог их надежно активировать. Настоящее "исправление" - это бросить наш собственный VirtualizingStackPanel с более свободными ограничениями для создания размера контейнера. В конце концов, для больших наборов данных (более миллиона) точность полосы прокрутки имеет гораздо меньшее значение. Если у меня будет время, я обновлю свой ответ репозиторием gist / github.

В своих тестах я использовал решение для виртуализации данных, доступное здесь: https://github.com/anagram4wander/VirtualizingObservableCollection.

Долгое время после того, как вопрос был опубликован, но может быть полезным для кого-то там. Решая точно такую ​​же проблему, я обнаружил, что ваш ItemsProvider (в твоем случае, MyTestCollection) должен реализовать IList интерфейс (не шаблонный). Только тогда VirtualizingStackPanel доступ к отдельным элементам через [] оператор, а не перечислять их через GetEnumerator, В вашем случае должно быть достаточно добавить:

    object IList.this[int index]
    {
        get { return this[index]; }
        set { throw new NotSupportedException(); }
    }

    public int IndexOf(DataItem item)
    {
        // TODO: Find a good way to find out the item's index
        return DataItem.Index;
    }

    public int IndexOf(object value)
    {
        var item = value as DataItem;
        if (item != null)
            return IndexOf(item);
        else
            throw new NullReferenceException();
    }

Все остальные IListНасколько я вижу, участники могут остаться неосуществленными.

Вы почти у цели, просто не VirtualizingStackPanel вызывает перечислитель списка.

Когда вы связываетесь с ListBox.ItemsSource, автоматически создается интерфейс ICollectionView между вашим фактическим источником данных и объектом ListBox. Этот интерфейс - простой способ вызвать перечислитель.

Как это исправить? Хорошо, просто напишите свой собственный класс CollectionView, который наследуется от интерфейса ICollectionView. Передайте его в ItemsSource, и ListBox узнает, что вы хотите иметь собственное представление о данных. Что вам нужно. Затем, когда ListBox обнаружит, что вы используете свое собственное представление, просто верните необходимые данные по запросу ListBox. Это было бы это. Играйте хорошо с ICollectionView:)

Другие вопросы по тегам