WPF ListBox VirtualizingPanel.CacheLengthUnit="Item" VirtualizingPanel.CacheLength="5,5"
Install-Package Microsoft.Extensions.DependencyInjection;
Totally speaking, VirtualizationPanel.CacheLengthUnit's value is item and its performance is better than pixel, the latter may lead to
-
Unpredictable memory usage - Caches as many items as fit in the pixel range
-
Performance issues with variable-height items
-
Can cache too many items if items are small, wasting memory
-
Harder to optimize since item count varies based on content
VirtualizingPanel.CacheLengthUnit="Item" VirtualizingPanel.CacheLength="5,5"
CacheLength="Before,After" First number (5) = Items cached BEFORE the visible viewport Second number (5) = Items cached AFTER the visible viewport Total cached items = 5 + 5 = 10 items (plus whatever is visible)
<ListBox ItemsSource="{Binding BooksCollection}" VirtualizingPanel.IsVirtualizing="True" VirtualizingPanel.VirtualizationMode="Recycling" VirtualizingPanel.CacheLengthUnit="Item" VirtualizingPanel.CacheLength="5,5" ScrollViewer.IsDeferredScrollingEnabled="True" ScrollViewer.CanContentScroll="True"> <ListBox.ItemTemplate> <DataTemplate> <Grid> <Grid.Resources> <Style TargetType="TextBlock"> <Setter Property="FontSize" Value="30"/> <Setter Property="Width" Value="Auto"/> <Style.Triggers> <Trigger Property="IsMouseOver" Value="True"> <Setter Property="FontSize" Value="50"/> <Setter Property="Foreground" Value="Red"/> </Trigger> </Style.Triggers> </Style> </Grid.Resources> <Grid.ColumnDefinitions> <ColumnDefinition/> <ColumnDefinition/> <ColumnDefinition/> <ColumnDefinition/> <ColumnDefinition/> <ColumnDefinition/> </Grid.ColumnDefinitions> <TextBlock Text="{Binding Id}" Grid.Column="0"/> <TextBlock Text="{Binding Name}" Grid.Column="1"/> <TextBlock Text="{Binding Author}" Grid.Column="2"/> <TextBlock Text="{Binding ISBN}" Grid.Column="3"/> <TextBlock Text="{Binding Title}" Grid.Column="4"/> <TextBlock Text="{Binding Topic}" Grid.Column="5"/> </Grid> </DataTemplate> </ListBox.ItemTemplate> </ListBox> public async Task InitBooksCollectionAsync() { StatusMsg = $"Loading...,{GetMemory()},{GetTimeCost()}"; BooksCollection = new ObservableCollection<Book>(); List<Book> booksList = new List<Book>(); for (int i = 1; i < 100000001; i++) { booksList.Add(new Book() { Id = idService.GetID(), Author = $"Author_{i}", Name = nameService.GetName(), ISBN = isbnService.GetISBN(), Title = $"Title_{i}", Topic = $"Topic_{i}" }); if (i < 1001 && i % 100 == 0) { await PopulateBooksCollectionAsync(booksList); } else if (i > 1000 && i % 1000000 == 0) { await PopulateBooksCollectionAsync(booksList); } } if (booksList.Any()) { await PopulateBooksCollectionAsync(booksList); } StatusMsg = $"Loaded {BooksCollection.Count} items,{GetMemory()},{GetTimeCost()}"; MessageBox.Show(StatusMsg); } private async Task PopulateBooksCollectionAsync(List<Book> booksList) { List<Book> tempList = new List<Book>(); lock (objLock) { tempList = booksList.ToList(); booksList.Clear(); } await Application.Current.Dispatcher.InvokeAsync(() => { foreach (var book in tempList) { BooksCollection.Add(book); } StatusMsg = $"Loaded {BooksCollection.Count} items,{GetMemory()},{GetTimeCost()}"; }, DispatcherPriority.Background); } private string GetMemory() { var procMemory = Process.GetCurrentProcess().PrivateMemorySize64 / 1024.0d / 1024.0d; return $"Memory:{procMemory.ToString("#,##0.00")} M"; } private string GetTimeCost() { return $"Time cost:{watch.Elapsed.TotalSeconds.ToString("#,##0.00")} seconds!"; }
//App.xaml <Application x:Class="WpfApp6.App" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:WpfApp6"> <Application.Resources> </Application.Resources> </Application> //App.xaml.cs using Microsoft.Extensions.DependencyInjection; using System.Configuration; using System.Data; using System.Windows; namespace WpfApp6 { /// <summary> /// Interaction logic for App.xaml /// </summary> public partial class App : Application { ServiceProvider serviceProvider; public App() { var services=new ServiceCollection(); ConfigureServices(services); serviceProvider = services.BuildServiceProvider(); var mainWin=serviceProvider.GetRequiredService<MainWindow>(); mainWin?.Show(); } private void ConfigureServices(ServiceCollection services) { services.AddSingleton<IIDService, IDService>(); services.AddSingleton<INameService, NameService>(); services.AddSingleton<IISBNService,ISBNService>(); services.AddSingleton<MainWindow>(); services.AddSingleton<MainVM>(); } protected override void OnExit(ExitEventArgs e) { serviceProvider?.Dispose(); base.OnExit(e); } } } //MainWindow.xaml <Window x:Class="WpfApp6.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:WpfApp6" WindowState="Maximized" mc:Ignorable="d" Title="{Binding StatusMsg}" Height="450" Width="800"> <Grid> <ListBox ItemsSource="{Binding BooksCollection}" VirtualizingPanel.IsVirtualizing="True" VirtualizingPanel.VirtualizationMode="Recycling" VirtualizingPanel.CacheLengthUnit="Item" VirtualizingPanel.CacheLength="5,5" ScrollViewer.IsDeferredScrollingEnabled="True" ScrollViewer.CanContentScroll="True"> <ListBox.ItemTemplate> <DataTemplate> <Grid> <Grid.Resources> <Style TargetType="TextBlock"> <Setter Property="FontSize" Value="30"/> <Setter Property="Width" Value="Auto"/> <Style.Triggers> <Trigger Property="IsMouseOver" Value="True"> <Setter Property="FontSize" Value="50"/> <Setter Property="Foreground" Value="Red"/> </Trigger> </Style.Triggers> </Style> </Grid.Resources> <Grid.ColumnDefinitions> <ColumnDefinition/> <ColumnDefinition/> <ColumnDefinition/> <ColumnDefinition/> <ColumnDefinition/> <ColumnDefinition/> </Grid.ColumnDefinitions> <TextBlock Text="{Binding Id}" Grid.Column="0"/> <TextBlock Text="{Binding Name}" Grid.Column="1"/> <TextBlock Text="{Binding Author}" Grid.Column="2"/> <TextBlock Text="{Binding ISBN}" Grid.Column="3"/> <TextBlock Text="{Binding Title}" Grid.Column="4"/> <TextBlock Text="{Binding Topic}" Grid.Column="5"/> </Grid> </DataTemplate> </ListBox.ItemTemplate> </ListBox> </Grid> </Window> //MainWindow.xaml.cs using System.Collections.ObjectModel; using System.ComponentModel; using System.Diagnostics; using System.Runtime.CompilerServices; using System.Text; using System.Windows; using System.Windows.Controls; using System.Windows.Data; using System.Windows.Documents; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Imaging; using System.Windows.Navigation; using System.Windows.Shapes; using System.Windows.Shell; using System.Windows.Threading; namespace WpfApp6 { /// <summary> /// Interaction logic for MainWindow.xaml /// </summary> public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); } public MainWindow(MainVM vm) { InitializeComponent(); RenderOptions.ProcessRenderMode = System.Windows.Interop.RenderMode.Default; this.DataContext = vm; this.Loaded += async (s, e) => { await vm.InitBooksCollectionAsync(); }; } } public class MainVM : INotifyPropertyChanged { private IIDService idService; private INameService nameService; private IISBNService isbnService; private object objLock = new object(); private Stopwatch watch; public MainVM(IIDService idServiceValue, INameService nameServiceValue, IISBNService isbnServiceValue) { idService = idServiceValue; nameService = nameServiceValue; isbnService = isbnServiceValue; watch = new Stopwatch(); watch.Start(); } public async Task InitBooksCollectionAsync() { StatusMsg = $"Loading...,{GetMemory()},{GetTimeCost()}"; BooksCollection = new ObservableCollection<Book>(); List<Book> booksList = new List<Book>(); for (int i = 1; i < 100000001; i++) { booksList.Add(new Book() { Id = idService.GetID(), Author = $"Author_{i}", Name = nameService.GetName(), ISBN = isbnService.GetISBN(), Title = $"Title_{i}", Topic = $"Topic_{i}" }); if (i < 1001 && i % 100 == 0) { await PopulateBooksCollectionAsync(booksList); } else if (i > 1000 && i % 1000000 == 0) { await PopulateBooksCollectionAsync(booksList); } } if (booksList.Any()) { await PopulateBooksCollectionAsync(booksList); } StatusMsg = $"Loaded {BooksCollection.Count} items,{GetMemory()},{GetTimeCost()}"; MessageBox.Show(StatusMsg); } private async Task PopulateBooksCollectionAsync(List<Book> booksList) { List<Book> tempList = new List<Book>(); lock (objLock) { tempList = booksList.ToList(); booksList.Clear(); } await Application.Current.Dispatcher.InvokeAsync(() => { foreach (var book in tempList) { BooksCollection.Add(book); } StatusMsg = $"Loaded {BooksCollection.Count} items,{GetMemory()},{GetTimeCost()}"; }, DispatcherPriority.Background); } private string GetMemory() { var procMemory = Process.GetCurrentProcess().PrivateMemorySize64 / 1024.0d / 1024.0d; return $"Memory:{procMemory.ToString("#,##0.00")} M"; } private string GetTimeCost() { return $"Time cost:{watch.Elapsed.TotalSeconds.ToString("#,##0.00")} seconds!"; } public event PropertyChangedEventHandler? PropertyChanged; private void OnPropertyChanged([CallerMemberName] string propName = "") { var handler = PropertyChanged; if (handler != null) { handler?.Invoke(this, new PropertyChangedEventArgs(propName)); } } private ObservableCollection<Book> booksCollection; public ObservableCollection<Book> BooksCollection { get { return booksCollection; } set { if (value != booksCollection) { booksCollection = value; OnPropertyChanged(); } } } private string statusMsg; public string StatusMsg { get { return statusMsg; } set { if (value != statusMsg) { statusMsg = value; OnPropertyChanged(); } } } } public interface IIDService { int GetID(); } public class IDService : IIDService { int id = 0; public int GetID() { return Interlocked.Increment(ref id); } } public interface INameService { string GetName(); } public class NameService : INameService { int idx = 0; public string GetName() { return $"Name_{Interlocked.Increment(ref idx)}"; } } public interface IISBNService { string GetISBN(); } public class ISBNService : IISBNService { int idx = 0; public string GetISBN() { return $"ISBN_{Interlocked.Increment(ref idx)}_{Guid.NewGuid():N}"; } } public class Book { public int Id { get; set; } public string Name { get; set; } public string Author { get; set; } public string ISBN { get; set; } public string Title { get; set; } public string Topic { get; set; } } }