由于始终将所有项目保存在内存中似乎是一种浪费,因此我正在寻找虚拟化列表部分的方法。
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由虚拟堆栈面板释放时,使用CleanUpVirtualizedItem
事件输出DEL#
。
现在,我的愿望是,我的自定义列表实现按请求提供项目-在这个最小的示例中,通过即时创建它们,在实际项目中通过从数据库加载它们。不幸的是,VirtualizingStackPanel
似乎不会以这种方式行事-相反,它会在程序启动时调用列表的枚举器并首先检索所有10000个项目!
因此,我的问题是:如何使用VirtualizingStackPanel实际虚拟化数据(即不加载所有数据),而不仅仅是减少GUI元素的数量?
- 是否有任何方法告诉虚拟堆栈面板有多少总项目,并告诉它按需要按索引访问它们,而不是使用枚举器? (例如,如果我记得正确,则Delphi Virtual TreeView组件的工作方式)
- 是否有任何巧妙的方法可以捕获项目实际进入视图的事件,以便至少可以正常存储每个项目的唯一键,并且仅在请求时加载其余项目数据? (尽管这似乎是一个hacky解决方案,因为我仍然必须出于没有真正原因而提供完整长度的列表,除了满足WPF API之外。)
- 是否有另一个WPF类更适合这种虚拟化?
编辑:根据dev hedgehog的建议,我创建了一个自定义的ICollectionView
实现。其中一些方法仍然被实现为抛出NotImplementedException
,但在打开窗口时调用的那些方法不会抛出异常。
然而,似乎对于该集合视图调用的第一件事就是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);
}
}
}
VirtualizingStackPanel
与数据虚拟化无关。您需要在数据层面上自己实现它。 - Federico Berasategui