WPFのListViewで要素の追加・クリア時間の計測

コンピュータ

データバインディング

ファイル名:MainWindow.xaml.cs

using System.Collections.ObjectModel;
using System.Windows;

namespace WpfListView;

public partial class MainWindow : Window
{
    public ObservableCollection<string> Items {get; set;}
    public MainWindow()
    {
        InitializeComponent();

        Items = [ "A", "B", "C", ];

        this.DataContext = this;
    }
}

ファイル名:MainWindow.xaml

<Window x:Class="WpfListView.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="ListView - DataBinding" Height="200" Width="400">
    <Grid>
        <ListView ItemsSource="{Binding Items}" />
    </Grid>
</Window>

コードビハインド

ファイル名:MainWindow.xaml.cs

using System.Collections.ObjectModel;
using System.Windows;

namespace WpfListView;

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();

        string[] items = ["A", "B", "C"];
        foreach(var s in items)
        {
            ListView1.Items.Add(s);
        }
    }
}

ファイル名:MainWindow.xaml

<Window x:Class="WpfListView.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="ListView - Code Behind" Height="200" Width="400">
    <Grid>
        <ListView x:Name="ListView1" />
    </Grid>
</Window>

要素の追加・クリア時間の計測 – データバインディング

using System.Collections.ObjectModel;
using System.Windows;
using System.Diagnostics;

namespace WpfListView;

public partial class MainWindow : Window
{
    const int MAX = 999_999;
    public ObservableCollection<string> Items {get; set;} = [];
    public MainWindow()
    {
        InitializeComponent();

        this.DataContext = this;

        this.Loaded += (_, __) =>
        {
            var sw = Stopwatch.StartNew();
            for(int i=0; i < MAX; i++)
            {
                this.Items.Add(i.ToString());
            }
            sw.Stop();
            this.Title = $"DataBiding {MAX} Add: {sw.ElapsedMilliseconds}ms";
            // DataBiding 999999 Add: 1626ms
        };

        this.MouseDoubleClick += (_, __) =>
        {
            var sw = Stopwatch.StartNew();
            this.Items.Clear();
            sw.Stop();
            this.Title = $"DataBiding {MAX} Clear: {sw.ElapsedMilliseconds}ms";
            // DataBiding 999999 Clear: 3ms

        };
    }
}

DataBiding 999999 Add: 1626ms
DataBiding 999999 Clear: 3ms

要素の追加・クリア時間の計測(高速版) – データバインディング

using System.Collections.ObjectModel;
using System.Windows;
using System.Diagnostics;
using System.ComponentModel;

namespace WpfListView;

public partial class MainWindow : Window, INotifyPropertyChanged
{
    public event PropertyChangedEventHandler? PropertyChanged;

    void OnPropertyChanged(string name)
        => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
    
    const int MAX = 999_999;
    ObservableCollection<string> _items = [];
    public ObservableCollection<string> Items
    {
        get => _items;
        set
        {
            if (_items == value) return;
            _items = value;
            OnPropertyChanged(nameof(Items));
        }
    }
    public MainWindow()
    {
        InitializeComponent();

        this.DataContext = this;

        this.Loaded += (_, __) =>
        {
            var sw = Stopwatch.StartNew();
            var list = new List<string>(MAX);
            for (int i = 0; i < MAX; i++)
            {
                list.Add(i.ToString());
            }
            this.Items = new ObservableCollection<string>(list);
            sw.Stop();
            this.Title = $"DataBiding {MAX} Add: {sw.ElapsedMilliseconds}ms";
            // DataBiding 999999 Add: 84ms
        };

        this.MouseDoubleClick += (_, __) =>
        {
            var sw = Stopwatch.StartNew();
            this.Items = [];
            sw.Stop();
            this.Title = $"DataBiding {MAX} Clear: {sw.ElapsedMilliseconds}ms";
            // DataBiding 999999 Clear: 3ms

        };
    }
}

DataBiding 999999 Add: 84ms
DataBiding 999999 Clear: 3ms

要素の追加・クリア時間の計測 – コードビハインド

using System.Windows;
using System.Diagnostics;

namespace WpfListView;

public partial class MainWindow : Window
{
    const int MAX = 999_999;
    public MainWindow()
    {
        InitializeComponent();

        this.Loaded += (_, __) =>
        {
            var sw = Stopwatch.StartNew();
            for(int i=0; i < MAX; i++)
            {
                ListView1.Items.Add(i.ToString());
            }
            sw.Stop();
            this.Title = $"DataBiding {MAX} Add: {sw.ElapsedMilliseconds}ms";
            // DataBiding 999999 Add: 1630ms
        };

        this.MouseDoubleClick += (_, __) =>
        {
            var sw = Stopwatch.StartNew();
            this.ListView1.Items.Clear();
            sw.Stop();
            this.Title = $"DataBiding {MAX} Clear: {sw.ElapsedMilliseconds}ms";
            // DataBiding 999999 Clear: 22ms

        };
    }
}

DataBiding 999999 Add: 1630ms
DataBiding 999999 Clear: 22ms

要素の追加・クリア時間の計測(高速版) – コードビハインド

using System.Windows;
using System.Diagnostics;

namespace WpfListView;

public partial class MainWindow : Window
{
    const int MAX = 999_999;
    public MainWindow()
    {
        InitializeComponent();

        this.Loaded += (_, __) =>
        {
            var sw = Stopwatch.StartNew();
            var list = new List<string>(MAX);
            for(int i=0; i < MAX; i++)
            {
                list.Add(i.ToString());
            }
            ListView1.ItemsSource = list;
            sw.Stop();
            this.Title = $"DataBiding {MAX} Add: {sw.ElapsedMilliseconds}ms";
            // DataBiding 999999 Add: 95ms
        };

        this.MouseDoubleClick += (_, __) =>
        {
            var sw = Stopwatch.StartNew();
            this.ListView1.ItemsSource = null;
            sw.Stop();
            this.Title = $"DataBiding {MAX} Clear: {sw.ElapsedMilliseconds}ms";
            // DataBiding 999999 Clear: 3ms

        };
    }
}

DataBiding 999999 Add: 95ms
DataBiding 999999 Clear: 3ms

ソート機能 – データバインディング

ファイル名:Item.cs

namespace WpfListView;

public class Item
{
    public string Index { get; set; } = "";
    public string Value { get; set; } = "";
}

ファイル名:MainViewModel.cs

using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Windows.Data;

namespace WpfListView;
public class MainViewModel : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler? PropertyChanged;

    ObservableCollection<Item> _items = new();
    public ObservableCollection<Item> Items
    {
        get => _items;
        set
        {
            _items = value;
            PropertyChanged?.Invoke(this,
                new PropertyChangedEventArgs(nameof(Items)));
        }
    }

    public void LoadData(int count)
    {
        var list = new List<Item>(count);
        for (int i = 0; i < count; i++)
        {
            list.Add(new Item { Index = i.ToString(), Value = $"Value {i}" });
        }
        // Items を丸ごと入れ替え
        Items = new ObservableCollection<Item>(list);
    }
    public void ClearData()
    {
        Items = new ObservableCollection<Item>();
    }

    public void Sort(string property, ListSortDirection direction)
    {
        var view = CollectionViewSource.GetDefaultView(Items);
        view.SortDescriptions.Clear();
        view.SortDescriptions.Add(new SortDescription(property, direction));
        view.Refresh();
    }
}

ファイル名:MainWindow.xaml.cs

using System.ComponentModel;
using System.Diagnostics;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;

namespace WpfListView;

public partial class MainWindow : Window
{
    const int MAX = 999_999;
    readonly MainViewModel _vm = new();
    public MainWindow()
    {
        InitializeComponent();

        this.DataContext = _vm;


        this.Loaded += (_, __) =>
        {
            var sw = Stopwatch.StartNew();
            
            _vm.LoadData(MAX);

            sw.Stop();
            this.Title = $"DataBiding {MAX} Add: {sw.ElapsedMilliseconds}ms";
            // DataBiding 999999 Add: 255ms
        };
        this.MouseDoubleClick += (_, __)=>
        {
            var sw = Stopwatch.StartNew();

            _vm.ClearData();

            sw.Stop();
            this.Title = $"DataBiding {MAX} Clear: {sw.ElapsedMilliseconds}ms";
            // DataBiding 999999 Clear: 5ms
        };
    }
    private GridViewColumnHeader? _lastHeader;
    private ListSortDirection _lastDirection;

    private void GridViewColumnHeader_Click(object sender, RoutedEventArgs e)
    {
        var sw = Stopwatch.StartNew();

        var header = (GridViewColumnHeader)sender;

        // Header の文字列をそのままプロパティ名として使う
        string property = header.Column.Header.ToString()!;

        ListSortDirection direction =
            (_lastHeader == header && _lastDirection == ListSortDirection.Ascending)
            ? ListSortDirection.Descending
            : ListSortDirection.Ascending;

        _lastHeader = header;
        _lastDirection = direction;

        var view = CollectionViewSource.GetDefaultView(ListView1.ItemsSource);
        view.SortDescriptions.Clear();
        view.SortDescriptions.Add(new SortDescription(property, direction));

        sw.Stop();
        this.Title = $"DataBiding {MAX} Sort: {sw.ElapsedMilliseconds}ms";
        // DataBiding 999999 Sort: 3185ms
    }
}

ファイル名:MainWindow.xaml

<Window x:Class="WpfListView.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"
        mc:Ignorable="d"
        Title="ListView + Sort" Height="450" Width="800">
    <Window.Resources>
        <Style x:Key="SortableHeader" TargetType="GridViewColumnHeader">
            <EventSetter Event="Click" Handler="GridViewColumnHeader_Click"/>
        </Style>
    </Window.Resources>
    <Grid>
        <ListView ItemsSource="{Binding Items}" x:Name="ListView1">
            <ListView.View>
                <GridView>
                    <GridViewColumn Header="Index"
                                    DisplayMemberBinding="{Binding Index}"
                                    HeaderContainerStyle="{StaticResource SortableHeader}" />
                    <GridViewColumn Header="Value"
                                    DisplayMemberBinding="{Binding Value}" />
                </GridView>
            </ListView.View>
        </ListView>
    </Grid>
</Window>

DataBiding 999999 Add: 255ms
DataBiding 999999 Clear: 5ms
DataBiding 999999 Sort: 3185ms

ソート機能 – コードビハインド

ファイル名:Item.cs

namespace WpfListView;

public class Item
{
    public string Index { get; set; } = "";
    public string Value { get; set; } = "";
}

ファイル名:MainWindow.xaml.cs

using System.ComponentModel;
using System.Diagnostics;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;

namespace WpfListView;

public partial class MainWindow : Window, INotifyPropertyChanged
{
    public event PropertyChangedEventHandler? PropertyChanged;

    // 表示用(Binding 対象)
    IReadOnlyList<Item> _items = Array.Empty<Item>();
    public IReadOnlyList<Item> Items
    {
        get => _items;
        set
        {
            _items = value;
            PropertyChanged?.Invoke(
                this, new PropertyChangedEventArgs(nameof(Items)));
        }
    }

    // 元データ(常に未ソートの基準)
    List<Item> _source = new();

    string? _lastKey;
    bool _ascending = true;

    const int MAX = 999_999;

    public MainWindow()
    {
        InitializeComponent();
        DataContext = this;

        Loaded += (_, __) => LoadData();
        MouseDoubleClick +=(_, __) =>
        {
            var sw = Stopwatch.StartNew();

            _source = [];
            Items = _source;

            sw.Stop();
            Title = $"Clear {MAX}: {sw.ElapsedMilliseconds} ms";
            // Clear 999999: 5ms
        };
    }

    void LoadData()
    {
        var sw = Stopwatch.StartNew();

        _source = Enumerable.Range(0, MAX)
            .Select(i => new Item
            {
                Index = i.ToString(),
                Value = $"Value {i}"
            })
            .ToList();

        Items = _source;   // ← DataBinding で一括反映

        sw.Stop();
        Title = $"Load {MAX}: {sw.ElapsedMilliseconds} ms";
        // Load 999999: 267ms
    }


    void GridViewColumnHeader_Click(object sender, RoutedEventArgs e)
    {
        var header = (GridViewColumnHeader)sender;
        string key = (string)header.Tag;

        bool asc = _lastKey == key ? !_ascending : true;
        _lastKey = key;
        _ascending = asc;

        var sw = Stopwatch.StartNew();

        IEnumerable<Item> sorted = key switch
        {
            "Index" => asc
                ? _source.OrderBy(x => x.Index)
                : _source.OrderByDescending(x => x.Index),

            "Value" => asc
                ? _source.OrderBy(x => x.Value)
                : _source.OrderByDescending(x => x.Value),

            _ => _source
        };

        // ★ 高速ポイント:丸ごと差し替え(通知1回)
        Items = sorted.ToList();

        sw.Stop();
        Title = $"Sort {key} {(asc ? "ASC" : "DESC")}: {sw.ElapsedMilliseconds} ms";
        // Sort Index ASC: 1215ms
        // Sort Index DESC: 1087ms
    }
}

ファイル名:MainWindow.xaml

<Window x:Class="WpfListView.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="WpfListView" Height="450" Width="800">

    <Window.Resources>
        <Style TargetType="GridViewColumnHeader">
            <EventSetter Event="Click"
                         Handler="GridViewColumnHeader_Click"/>
        </Style>
    </Window.Resources>

    <Grid>
        <ListView ItemsSource="{Binding Items}">
            <ListView.View>
                <GridView>

                    <GridViewColumn>
                        <GridViewColumn.Header>
                            <GridViewColumnHeader Tag="Index">
                                Index
                            </GridViewColumnHeader>
                        </GridViewColumn.Header>
                        <GridViewColumn.DisplayMemberBinding>
                            <Binding Path="Index"/>
                        </GridViewColumn.DisplayMemberBinding>
                    </GridViewColumn>

                    <GridViewColumn>
                        <GridViewColumn.Header>
                            <GridViewColumnHeader Tag="Value">
                                Value
                            </GridViewColumnHeader>
                        </GridViewColumn.Header>
                        <GridViewColumn.DisplayMemberBinding>
                            <Binding Path="Value"/>
                        </GridViewColumn.DisplayMemberBinding>
                    </GridViewColumn>

                </GridView>
            </ListView.View>
        </ListView>
    </Grid>
</Window>

Load 999999: 267ms
Clear 999999: 5ms
Sort Index ASC: 1215ms
Sort Index DESC: 1087ms

感想

速度面だけみると、データバインディングもコードビハインドも同じぐらいでした。
ただ、ソートがコードビハインドが速いのは以外な結果でした。

コメント