WPF listbox render 230 million items via task and Application.Current.Dispatcher.InvokeAsync without UI blocking issues

Install-Package System.Management;

 

image

 

 

 

private object collectionLock = new object();
private async Task LoadDataAsync()
{
    List<string> isbnsList = new List<string>();
    ISBNSCollection=new BulkObservableCollection<string>();
    await Task.Run(async () =>
    {
        for (int i = 1; i<=230000000; i++)
        {
            isbnsList.Add($"{i}_{Guid.NewGuid().ToString("N")}");
            if (i<20000 && i%10000==0)
            {
                await PopulateISBNCollection(isbnsList);
                await Task.Delay(1);
            }
            else if (i%1000000==0)
            {
                await PopulateISBNCollection(isbnsList);
                await Task.Delay(1);
            }
        }               
    });

    watch.Stop();            
    await ScrollIntoLastItem();
}

private async Task PopulateISBNCollection(List<string> isbnsList)
{
    List<string> tempList;
    lock (collectionLock)
    {
        tempList = isbnsList.ToList();
        isbnsList.Clear();
    }

    // Await the UI work to finish
    await Application.Current.Dispatcher.InvokeAsync(() =>
    {
        ISBNSCollection.AddRange(tempList);
        CollectionCount=ISBNSCollection==null ? 0 : ISBNSCollection.Count;
        LoadingStr=$"Loaded {ISBNSCollection?.Count} items,time cost {watch.Elapsed.TotalSeconds} seconds," +
        $"Private memory,{MemoryMonitor.GetPrivateMemoryMB()} MB," +
        $"Working set {MemoryMonitor.GetWorkingSetMemoryMB()} MB," +
        $"Private {MemoryMonitor.GetPrivateMemoryMB()} MB," +
        $"WorkingSet {MemoryMonitor.GetWorkingSetMemoryMB()} MB," +
        $"Total {MemoryMonitor.GetTotalPhysicalMemory()} MB," +
        $"Available {MemoryMonitor.GetAvailablePhysicalMemory()}MB";
    }, System.Windows.Threading.DispatcherPriority.Background);
}

private async Task ScrollIntoLastItem()
{
    var lbx = Application.Current?.MainWindow.FindName("lbx") as ListBox;
    if (lbx != null)
    {
        await Application.Current?.Dispatcher.InvokeAsync(() =>
        {
            lbx?.ScrollIntoView(lbx.Items[lbx.Items.Count - 1]);
        },System.Windows.Threading.DispatcherPriority.Send);
        MessageBox.Show(LoadingStr);
    }
}

 

 

 

//xaml
<Window x:Class="WpfApp73.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:WpfApp73"
        mc:Ignorable="d"
        Title="{Binding CollectionCount,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"
        Height="450" Width="800">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition/>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>
        <ListBox x:Name="lbx"
                 Grid.Row="0"
                 ItemsSource="{Binding ISBNSCollection,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"
                 VirtualizingPanel.IsVirtualizing="True"
                 VirtualizingPanel.VirtualizationMode="Recycling"
                 ScrollViewer.IsDeferredScrollingEnabled="True">
            <ListBox.ItemContainerStyle>
                <Style TargetType="{x:Type ListBoxItem}">
                    <Setter Property="FontSize" Value="30"/>
                    <Setter Property="HorizontalAlignment" Value="Left"/>
                    <Setter Property="VerticalAlignment" Value="Center"/>
                    <Style.Triggers>
                        <Trigger Property="IsMouseOver" Value="True">
                            <Setter Property="FontSize" Value="50"/>
                            <Setter Property="Foreground" Value="Red"/>
                        </Trigger>
                    </Style.Triggers>
                </Style>
            </ListBox.ItemContainerStyle>
        </ListBox>
        <TextBlock Grid.Row="1"
                   TextWrapping="Wrap"
                   Text="{Binding LoadingStr,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"
                   FontSize="30"/>
    </Grid>
</Window>


//cs
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Diagnostics;
using System.Management;
using System.Runtime.CompilerServices;
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 WpfApp73
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            RenderOptions.ProcessRenderMode=System.Windows.Interop.RenderMode.Default;
            var vm = new MainVM();
            this.DataContext=vm;
        }
    }

    public class MainVM : INotifyPropertyChanged
    {
        Stopwatch watch;
        public MainVM()
        {
            watch=new Stopwatch();
            watch.Start();
            LoadDataAsync();
        }

        private object collectionLock = new object();
        private async Task LoadDataAsync()
        {
            List<string> isbnsList = new List<string>();
            ISBNSCollection=new BulkObservableCollection<string>();
            await Task.Run(async () =>
            {
                for (int i = 1; i<=230000000; i++)
                {
                    isbnsList.Add($"{i}_{Guid.NewGuid().ToString("N")}");
                    if (i<20000 && i%10000==0)
                    {
                        await PopulateISBNCollection(isbnsList);
                        await Task.Delay(1);
                    }
                    else if (i%1000000==0)
                    {
                        await PopulateISBNCollection(isbnsList);
                        await Task.Delay(1);
                    }
                }               
            });

            watch.Stop();            
            await ScrollIntoLastItem();
        }

        private async Task PopulateISBNCollection(List<string> isbnsList)
        {
            List<string> tempList;
            lock (collectionLock)
            {
                tempList = isbnsList.ToList();
                isbnsList.Clear();
            }

            // Await the UI work to finish
            await Application.Current.Dispatcher.InvokeAsync(() =>
            {
                ISBNSCollection.AddRange(tempList);
                CollectionCount=ISBNSCollection==null ? 0 : ISBNSCollection.Count;
                LoadingStr=$"Loaded {ISBNSCollection?.Count} items,time cost {watch.Elapsed.TotalSeconds} seconds," +
                $"Private memory,{MemoryMonitor.GetPrivateMemoryMB()} MB," +
                $"Working set {MemoryMonitor.GetWorkingSetMemoryMB()} MB," +
                $"Private {MemoryMonitor.GetPrivateMemoryMB()} MB," +
                $"WorkingSet {MemoryMonitor.GetWorkingSetMemoryMB()} MB," +
                $"Total {MemoryMonitor.GetTotalPhysicalMemory()} MB," +
                $"Available {MemoryMonitor.GetAvailablePhysicalMemory()}MB";
            }, System.Windows.Threading.DispatcherPriority.Background);
        }

        private async Task ScrollIntoLastItem()
        {
            var lbx = Application.Current?.MainWindow.FindName("lbx") as ListBox;
            if (lbx != null)
            {
                await Application.Current?.Dispatcher.InvokeAsync(() =>
                {
                    lbx?.ScrollIntoView(lbx.Items[lbx.Items.Count - 1]);
                },System.Windows.Threading.DispatcherPriority.Send);
                MessageBox.Show(LoadingStr);
            }
        }

        public event PropertyChangedEventHandler? PropertyChanged;
        private void OnPropertyChanged([CallerMemberName] string propertyName = "")
        {
            var handler = PropertyChanged;
            if (handler!=null)
            {
                handler?.Invoke(this, new PropertyChangedEventArgs(propertyName));
            }
        }

        private BulkObservableCollection<string> iSBNSCollection;
        public BulkObservableCollection<string> ISBNSCollection
        {
            get
            {
                return iSBNSCollection;
            }
            set
            {
                if (value!=iSBNSCollection)
                {
                    iSBNSCollection = value;
                    OnPropertyChanged();
                }
            }
        }

        private int collectionCount;
        public int CollectionCount
        {
            get
            {
                return collectionCount;
            }
            set
            {
                collectionCount = value;
                OnPropertyChanged();
            }
        }

        private string loadingStr;
        public string LoadingStr
        {
            get
            {
                return loadingStr;
            }
            set
            {
                if (value!=loadingStr)
                {
                    loadingStr = value;
                    OnPropertyChanged();
                }
            }
        }
    }

    public class MemoryMonitor
    {
        private static PerformanceCounter memoryCounter = null;

        //Get the current private memory usage in MB.
        public static int GetPrivateMemoryMB()
        {
            if (memoryCounter == null)
            {
                memoryCounter=new PerformanceCounter("Process", "Working Set - Private",
                    Process.GetCurrentProcess().ProcessName);
            }
            return (int)(memoryCounter.NextValue()/1024f/1024f);
        }

        //Gets the total working set memory in MB
        public static int GetWorkingSetMemoryMB()
        {
            using (var proc = Process.GetCurrentProcess())
            {
                return (int)(proc.WorkingSet64/1024f/1024f);
            }
        }

        //Total physical memory
        public static double GetTotalPhysicalMemory()
        {
            using (var searcher = new ManagementObjectSearcher("select TotalPhysicalMemory from win32_computersystem"))
            {
                foreach (ManagementObject item in searcher.Get())
                {
                    return Convert.ToDouble(item["TotalPhysicalMemory"])/1024f/1024f;
                }
            }
            return 0;
        }

        //Available memory
        public static double GetAvailablePhysicalMemory()
        {
            using (var searcher = new ManagementObjectSearcher("select availablebytes from win32_perfformatteddata_perfos_memory"))
            {
                foreach (ManagementObject item in searcher.Get())
                {
                    return Convert.ToDouble(item["availableBytes"])/1024f/1024f;
                }
            }
            return 0;
        }
    }


    public class BulkObservableCollection<T> : ObservableCollection<T>
    {
        private bool suppressNotification = false;

        public void AddRange(IEnumerable<T> items)
        {
            if (Items==null || items==null)
            {
                return;
            }

            suppressNotification= true;
            //directory adds without firing collectionchanged
            foreach (var item in items)
            {
                Items.Add(item);
            }

            suppressNotification= false;
            OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
        }

        protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
        {
            if (!suppressNotification)
            {
                base.OnCollectionChanged(e);
            }
        }
    }
}

 

 

 

image

 

posted @ 2025-09-07 12:30  FredGrit  阅读(8)  评论(0)    收藏  举报