我正在开发一个应用程序,该应用程序应该以类似列表/网格的方式显示从其他地方(例如数据库)加载的大量项目。
由于一直将所有项目都放在内存中似乎是一种浪费,因此我正在研究虚拟化列表的一部分的方法。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 类是否更适合这种虚拟化?
编辑:按照开发刺猬的建议,我创建了一个自定义ICollectionView
实现。它的一些方法仍然实现为 throw 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);
}
}
}