我正在开发一个应用程序,该应用程序应该显示从列表/网格状物中的其他地方(例如,数据库)加载的相当大量的项目。
由于所有内存中的所有项目似乎都是浪费,我正在研究如何虚拟化列表的一部分。 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元素的数量?
编辑:根据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);
}
}
}
答案 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.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。