I am developing an application that is supposed to display a fairly large amount of items that are loaded from elsewhere (say, a database) in a list/grid-like thing.
As having all the items in memory all the time seems like a waste, I am looking into ways to virtualize a part of my list. VirtualizingStackPanel
seems just like what I need - however, while it seems to do a good job virtualizing the UI of items, I am not sure how to virtualize parts of the underlying item list itself.
As a small sample, consider a WPF application with this as its main window:
<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>
The code-behind that supplies a list should look like this:
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());
}
}
}
So, this displays an application with a ListBox
, which is forced to virtualize its items with the IsVirtualizing
attached property. It takes its items from the data context, for which a custom IList<T>
implementation is supplied that creates 10000 data items on the fly (when they are retrieved via the indexer).
For debugging purposes, the text ADD #
(where #
equals the item index) is output whenever an item is created, and the CleanUpVirtualizedItem
event is used to output DEL #
when an item goes out of view and its UI is released by the virtualizing stack panel.
Now, my wish is that my custom list implementation supplies items upon request - in this minimal sample, by creating them on the fly, and in the real project by loading them from the database. Unfortunately, VirtualizingStackPanel
does not seem to behave this way - instead, it invokes the enumerator of the list upon program start and first retrieves all 10000 items!
Thus, my question is: How can I use VirtualizingStackPanel for actual virtualization of data (as in, not loading all the data) rather than just reducing the number of GUI elements?
- Is there any way to tell the virtualizing stack panel how many items there are in total and telling it to access them by index as needed, rather than using the enumerator? (Like, for example, the Delphi Virtual TreeView component works, if I recall correctly.)
- Are there any ingenious ways of capturing the event when an item actually comes into view, so at least I could normally just store a unique key of each item and only load the remaining item data when it is requested? (That would seem like a hacky solution, though, as I would still have to provide the full-length list for no real reason, other than satisfying the WPF API.)
- Is another WPF class more suitable for this kind of virtualization?
EDIT: Following dev hedgehog's advice, I have created a custom ICollectionView
implementation. Some of its methods are still implemented to throw NotImplementedException
s, but the ones that get called when the window is opened do not.
However, it seems that about the first thing that is called for that collection view is the GetEnumerator
method, enumerating all 10000 elements again (as evidenced by the debug output, where I print a message for every 1000th item), which is what I was trying to avoid.
Here is an example to reproduce the issue:
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);
}
}
}