リスト/グリッドのようなもので、他の場所 (データベースなど) からロードされたかなり大量のアイテムを表示するアプリケーションを開発しています。
すべての項目を常にメモリに保持しておくのはもったいないので、リストの一部を仮想化する方法を検討しています。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
を使用して項目を強制的に仮想化します。データ コンテキストからアイテムを取得します。このカスタム実装には、オンザフライで 10000 個のデータ アイテムを作成する (インデクサー経由で取得した場合) が提供されます。IsVirtualizing
IList<T>
デバッグの目的で、アイテムが作成されるたびにテキストADD #
(#
アイテム インデックスに等しい) が出力され、アイテムがビューから外れてその UI が仮想化スタック パネルによって解放されると、CleanUpVirtualizedItem
イベントが出力に使用されます。DEL #
今、私の願いは、カスタム リストの実装が要求に応じてアイテムを提供することです。この最小限のサンプルでは、オンザフライでアイテムを作成し、実際のプロジェクトではデータベースからアイテムをロードします。残念ながら、VirtualizingStackPanel
このようには動作しないようです。代わりに、プログラムの開始時にリストの列挙子を呼び出し、最初に 10000 個のアイテムすべてを取得します!
したがって、私の質問は次のとおりです。GUI要素の数を減らすだけでなく、データの実際の仮想化(すべてのデータをロードしないなど)にVirtualizingStackPanelを使用するにはどうすればよいですか?
- 列挙子を使用するのではなく、仮想化スタック パネルにアイテムの総数を伝え、必要に応じてインデックスでアクセスするように指示する方法はありますか? (たとえば、私の記憶が正しければ、Delphi Virtual TreeView コンポーネントは機能します。)
- アイテムが実際に表示されたときにイベントをキャプチャする独創的な方法はありますか?少なくとも、通常は各アイテムの一意のキーを保存し、要求されたときに残りのアイテム データのみをロードできますか? (ただし、WPF API を満たす以外に、実際の理由もなく完全な長さのリストを提供する必要があるため、これはハッキーな解決策のように思えます。)
- この種の仮想化により適した別の WPF クラスはありますか?
編集:開発ヘッジホッグのアドバイスに従って、カスタムICollectionView
実装を作成しました。一部のメソッドは s をスローするように実装されて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);
}
}
}