WPF ListBox loaded 26M items in mvvm
Install-Package CommunityToolkit.mvvm;
//xaml <Window x:Class="WpfApp4.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" WindowState="Maximized" xmlns:local="clr-namespace:WpfApp4" mc:Ignorable="d" Title="{Binding StatusMsg,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}" Height="450" Width="800"> <Grid> <ListBox ItemsSource="{Binding BooksCollection,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}" VirtualizingPanel.IsVirtualizing="True" VirtualizingPanel.VirtualizationMode="Recycling" ScrollViewer.IsDeferredScrollingEnabled="True"> <ListBox.ItemTemplate> <DataTemplate> <Grid Width="{Binding DataContext.GridWidth,RelativeSource={RelativeSource AncestorType=Window}}" Height="{Binding DataContext.GridHeight,RelativeSource={RelativeSource AncestorType=Window}}"> <Grid.Background> <ImageBrush ImageSource="{Binding ImgSource}"/> </Grid.Background> <Grid.Resources> <Style TargetType="ColumnDefinition"> <Setter Property="Width" Value="Auto"/> </Style> <Style TargetType="{x:Type TextBlock}"> <Setter Property="FontSize" Value="50"/> <Setter Property="HorizontalAlignment" Value="Center"/> <Setter Property="VerticalAlignment" Value="Center"/> <Style.Triggers> <Trigger Property="IsMouseOver" Value="True"> <Setter Property="FontSize" Value="80"/> <Setter Property="Foreground" Value="Red"/> </Trigger> </Style.Triggers> </Style> </Grid.Resources> <Grid.RowDefinitions> <RowDefinition/> <RowDefinition/> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition/> <ColumnDefinition/> <ColumnDefinition/> <ColumnDefinition/> <ColumnDefinition/> <ColumnDefinition/> <ColumnDefinition/> <ColumnDefinition/> </Grid.ColumnDefinitions> <TextBlock Grid.Column="0" Text="{Binding Id}"/> <TextBlock Grid.Column="1" Text="{Binding Name}"/> <TextBlock Grid.Column="2" Text="{Binding Author}"/> <TextBlock Grid.Column="3" Text="{Binding Comment}"/> <TextBlock Grid.Column="4" Text="{Binding Content}"/> <TextBlock Grid.Column="5" Text="{Binding Summary}"/> <TextBlock Grid.Column="6" Text="{Binding Title}"/> <TextBlock Grid.Column="7" Text="{Binding Topic}"/> <TextBlock Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="8" Text="{Binding ISBN}"/> </Grid> </DataTemplate> </ListBox.ItemTemplate> <ListBox.ContextMenu> <ContextMenu> <MenuItem Header="Export As Pictures" Command="{Binding SaveAsPicturesCommand}" CommandParameter="{Binding RelativeSource={RelativeSource AncestorType=ContextMenu},Path=PlacementTarget}"/> </ContextMenu> </ListBox.ContextMenu> </ListBox> </Grid> </Window> //cs using CommunityToolkit.Mvvm.ComponentModel; using System.Collections.ObjectModel; using System.ComponentModel; using System.Diagnostics; using System.Diagnostics.Eventing.Reader; using System.IO; using System.Text; using System.Threading.Tasks; 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; namespace WpfApp4 { /// <summary> /// Interaction logic for MainWindow.xaml /// </summary> public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); var vm = new MainVM(); this.DataContext=vm; this.SizeChanged+=MainWindow_SizeChanged; } private void MainWindow_SizeChanged(object sender, SizeChangedEventArgs e) { if (DataContext is MainVM vm) { var fe = this.Content as FrameworkElement; if (fe!=null) { vm.GridWidth = fe.ActualWidth; vm.GridHeight = fe.ActualHeight/2; } } } } public partial class MainVM : ObservableObject { public MainVM() { InitData(); InitCommands(); System.Timers.Timer tmr = new System.Timers.Timer(); tmr.Elapsed += Tmr_Elapsed; tmr.Interval = 1000; tmr.Start(); } private void Tmr_Elapsed(object? sender, System.Timers.ElapsedEventArgs e) { System.Diagnostics.Debug.WriteLine($"{DateTime.Now.ToString("yyyyMMddHHmmssffff")},Count:{BooksCollection.Count}," + $"{GetMemory()}"); StatusMsg = $"Loaded {BooksCollection.Count} items,{GetMemory()}"; //Debug.WriteLine($"Loaded {BooksCollection.Count} items,{GetMemory()}"); } private void InitCommands() { SaveAsPicturesCommand=new DelCommand(async(obj)=>await SaveAsPicturesCommandExecuted(obj)); } private async Task SaveAsPicturesCommandExecuted(object? obj) { await Task.Run(async () => { var lbx = obj as ListBox; if (lbx==null) { return; } var items = lbx.Items; string dir=$"Lbx_{DateTime.Now.ToString("yyyyMMddHHmmssffff")}"; int jpgIdx = 0; Directory.CreateDirectory(dir); foreach (var item in items) { var lbxItem = lbx.ItemContainerGenerator.ContainerFromItem(item) as ListBoxItem; if (lbxItem==null) { continue; } await Application.Current?.Dispatcher.InvokeAsync(() => { lbx.ScrollIntoView(lbxItem); lbx.UpdateLayout(); string jpgFileName = $"JPG_{++jpgIdx}.jpg"; SaveLbxItemAsJpgFile(lbxItem, jpgFileName); }); } }); } private void SaveLbxItemAsJpgFile(ListBoxItem lbxItem, string jpgFileName) { var dpi = VisualTreeHelper.GetDpi(lbxItem); var rtb = new RenderTargetBitmap( (int)(lbxItem.ActualWidth*dpi.DpiScaleX), (int)(lbxItem.ActualHeight*dpi.DpiScaleY), dpi.PixelsPerInchX, dpi.PixelsPerInchY, PixelFormats.Pbgra32); rtb.Render(lbxItem); using(FileStream fileStream=new FileStream(jpgFileName,FileMode.Create)) { JpegBitmapEncoder encoder = new JpegBitmapEncoder(); encoder.Frames.Add(BitmapFrame.Create(rtb)); encoder.Save(fileStream); Debug.WriteLine(jpgFileName); } } private async Task InitData() { string dir = @"../../../Images"; var imgs = Directory.GetFiles(dir); if (imgs==null || !imgs.Any()) { return; } int imgsCount = imgs.Count(); await Task.Run(async () => { var tempBooksList = new List<Book>(); BooksCollection=new ObservableCollection<Book>(); int idx = 1; while (idx<100000001) { tempBooksList.Add(new Book() { Id=idx, Name=$"Name_{idx}", Author=$"Author_{idx}", Comment=$"Comment_{idx}", Content=$"Content_{idx}", Summary=$"Summary_{idx}", Title=$"Title_{idx}", Topic=$"Topic_{idx}", ISBN=$"ISBN_{idx}_{Guid.NewGuid():N}", ImgSource=GetImgSourceViaUrl(imgs[idx%imgsCount]) }); if (idx<1000 && idx%100==0) { await PopulateBooksCollection(tempBooksList); } else if (idx>=1000 && idx%1000000==0) { await PopulateBooksCollection(tempBooksList); } ++idx; } MessageBox.Show($"Loaded {BooksCollection.Count} items,{GetMemory()}"); }); } private string GetMemory() { var proc=Process.GetCurrentProcess(); return $"Memory:{(proc.PrivateMemorySize64 / 1024 / 1024).ToString("#,##0")}M"; } private async Task PopulateBooksCollection(List<Book> tempBooksList) { var tempList = tempBooksList.ToList(); tempBooksList.Clear(); await Application.Current?.Dispatcher.InvokeAsync(() => { foreach (var bk in tempList) { BooksCollection.Add(bk); } StatusMsg=$"Loaded {BooksCollection.Count} items,{GetMemory()}"; Debug.WriteLine($"Loaded {BooksCollection.Count} items,{GetMemory()}"); }); } private ImageSource GetImgSourceViaUrl(string imgUrl) { BitmapImage bmi = new BitmapImage(); if (!File.Exists(imgUrl)) { return bmi; } bmi.BeginInit(); bmi.UriSource=new Uri(imgUrl, UriKind.RelativeOrAbsolute); bmi.CacheOption=BitmapCacheOption.OnDemand; bmi.EndInit(); if (bmi.CanFreeze) { bmi.Freeze(); } return bmi; } [ObservableProperty] private ObservableCollection<Book> booksCollection; [ObservableProperty] private double gridWidth; [ObservableProperty] private double gridHeight; [ObservableProperty] private string statusMsg; public ICommand SaveAsPicturesCommand { get; set; } } public class DelCommand : ICommand { private Action<object?> execute; private Predicate<object?> canExecute; public DelCommand(Action<object?> executeValue, Predicate<object?> canExecuteValue= null) { execute = executeValue; canExecute = canExecuteValue; } public event EventHandler? CanExecuteChanged { add { CommandManager.RequerySuggested += value; } remove { CommandManager.RequerySuggested -= value; } } public bool CanExecute(object? parameter) { return canExecute==null ? true : canExecute(parameter); } public void Execute(object? parameter) { execute(parameter); } } public class Book { public int Id { get; set; } public string Author { get; set; } public string Name { get; set; } public string Comment { get; set; } public string Content { get; set; } public string Summary { get; set; } public string Title { get; set; } public string Topic { get; set; } public string ISBN { get; set; } public ImageSource ImgSource { get; set; } } }