4

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

由于一直将所有项目都放在内存中似乎是一种浪费,因此我正在研究虚拟化列表的一部分的方法。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附加的属性虚拟化其项目。它从数据上下文中获取其项目,为此提供了一个自定义IList<T>实现,可以动态创建 10000 个数据项(当它们通过索引器检索时)。

出于调试目的,每当创建项目时都会输出文本ADD #(其中等于项目索引),并且该事件用于在项目离开视图并且其 UI 被虚拟化堆栈面板释放时输出。#CleanUpVirtualizedItemDEL #

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

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

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

编辑:按照开发刺猬的建议,我创建了一个自定义ICollectionView实现。它的一些方法仍然实现为 throw NotImplementedExceptions,但在打开窗口时调用的方法却没有。

但是,似乎该集合视图调用的第一件事是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

4 回答 4

4

你想要Data Virtualization,你UI Virtualization现在就拥有。

您可以在此处查看有关数据虚拟化的更多信息

于 2014-02-11T20:06:16.173 回答
3

为了解决VirtualizingStackPanel尝试枚举其整个数据源的问题,我逐步浏览了http://referencesource.microsoft.com上的源代码(https://referencesource.microsoft.com/#PresentationFramework/src/Framework/系统/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 。

于 2018-06-25T16:08:52.527 回答
1

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

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

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

于 2014-02-11T20:04:51.350 回答
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的成员都可以不实现。

于 2017-08-08T13:17:27.047 回答