具有虚拟化列表的VirtualizingStackPanel

时间:2014-02-11 19:54:07

标签: wpf collectionview virtualizingstackpanel data-virtualization

我正在开发一个应用程序,该应用程序应该显示从列表/网格状物中的其他地方(例如,数据库)加载的相当大量的项目。

由于所有内存中的所有项目似乎都是浪费,我正在研究如何虚拟化列表的一部分。 VirtualizingStackPanel似乎就像我需要的那样 - 但是,虽然它似乎可以很好地虚拟化项目的UI ,但我不确定如何虚拟化基础项目列表本身的部分内容。

作为一个小样本,请考虑一个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 attached property虚拟化其项目。它从数据上下文中获取其项目,为其提供了一个自定义IList<T>实现,可以动态创建10000个数据项(当它们通过索引器检索时)。

出于调试目的,只要创建项目,就会输出文本ADD #(其中#等于项目索引),CleanUpVirtualizedItem event用于输出DEL #当一个项目离开视图并且其UI由虚拟化堆栈面板释放时。

现在,我的愿望是我的自定义列表实现根据请求提供项目 - 在此最小样本中,通过动态创建它们,以及通过从数据库加载它们在实际项目中。不幸的是,VirtualizingStackPanel似乎没有这样的行为 - 相反,它在程序启动时调用列表的枚举器,首先检索所有10000个项目!

因此,我的问题是:我如何使用VirtualizingStackPanel进行数据的实际虚拟化(如,不加载所有数据)而不仅仅是减少GUI元素的数量?

  • 有没有办法告诉虚拟化堆栈面板总共有多少项目,并告诉它根据需要通过索引访问它们,而不是使用枚举器? (例如,Delphi Virtual TreeView component可以正常工作,如果我没记错的话。)
  • 当项目实际进入视图时,是否有任何巧妙的方法来捕获事件,所以至少我通常只能存储每个项目的唯一键,并且只在请求时加载剩余的项目数据? (但这似乎是一个hacky解决方案,因为除了满足WPF API之外,我仍然必须提供完整列表,而不是满足WPF API。)
  • 另一个WPF类更适合这种虚拟化吗?

编辑:根据dev hedgehog的建议,我创建了一个自定义ICollectionView实现。它的一些方法仍然实现为抛出NotImplementedException,但是在打开窗口时调用的方法不会。{/ p>

然而,似乎关于该集合视图调用的第一件事是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 个答案:

答案 0 :(得分:2)

您想要Data Virtualization,您现在有UI Virtualization

您可以详细了解数据虚拟化here

答案 1 :(得分:1)

你几乎就在那里,不是VirtualizingStackPanel调用列表的枚举器。

当您绑定到ListBox.ItemsSource时,将在您的实际数据源和ListBox目标之间自动创建ICollectionView接口。该接口是调用枚举器的meany。

如何解决这个问题?那么只需编写自己的CollectionView类,它继承自ICollectionView接口。将它传递给ItemsSource,ListBox将知道您希望拥有自己的数据视图。这是你需要的。然后,一旦ListBox意识到您正在使用自己的视图,只需在ListBox请求时返回所需的数据。就是这样。使用ICollectionView玩得很好:)

答案 2 :(得分:1)

问题发布后很长一段时间,但可能对那里的人有用。在解决完全相同的问题时,我发现您的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成员都可以保持未实现状态。

答案 3 :(得分:1)

要解决VirtualizingStackPanel试图枚举其整个数据源的问题,我在http://referencesource.microsoft.comhttps://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

相关问题