プログラム系統備忘録ブログ

記事中のコードは自己責任の下でご自由にどうぞ。

Live Shapingを使ってコレクション要素を自動フィルタリングする例

19:00頃: 「一度に大量の要素を変更した場合の性能」を追記

.NET Framework 4.5で ICollectionViewLiveShaping Interface が追加されました。このインターフェースを通じて、コレクション要素のグループ化/ソート/フィルタリングをリアルタイムに行えます。
この記事では、フィルタリング機能を使うサンプルコードを紹介します。
(余談ですが、MSDNのWPF関連のページでは、.NET 4.5〜のページではサンプルコードが表示されないことや、"This topic is no longer available"であることが多々あります。その場合は.NET 4.0のページに移動しましょう。)

バインディングソースに使うクラスについて

Live Shapingで検索すると CollectionViewSource Class を使用するサンプルが多いです。
しかし、この記事で紹介するサンプルでは ListCollectionView Class を使用することにします。このクラスはICollectionViewLiveShapingを実装しています。
理由は以下のとおり:

  • MSDNのCollectionViewSource Classの概要に「The Extensible Application Markup Language (XAML) proxy of a CollectionView class.」とあり、xamlで記述する場合にだけ使用する、と読める。
  • CollectionView Class のRemarksに「You should not create objects of this class in your code.」とありますが、派生クラスのListCollectionViewにはそのような記述はない。
  • 【WPF】CollectionViewSource.GetDefaultView()メソッド その2 | 創造的プログラミングと粘土細工 に「A.ソースがICollectionViewを継承するオブジェクトの場合は、そのオブジェクトを返す。」とあるので、余分なビューの生成を防げそう。

自動フィルタリングの準備

自動フィルタリングを利用するには、ListCollectionViewの次のプロパティを設定します。(CollectionViewSourceの場合では、Filterをイベントで処理する点が異なるだけで他は同様のようです。)

  • Filter Property に、表示する要素ではtrue/隠す要素ではfalseを返す述語を設定します。
  • IsLiveFiltering Property にtrueを設定します。
  • LiveFilteringProperties Property に、変更通知を監視するプロパティ名を追加します。ここで追加したプロパティが変更されると、その要素についてFilter述語が再度評価されます。

サンプルコード

コレクションに含める要素です。
表示用のNameプロパティ、変更通知付きのVisibleプロパティを定義します。ついでに表示を楽にするためにToStringをオーバーライドします。

using System.ComponentModel;

public class Item : INotifyPropertyChanged
{
    public Item(string name) { this.Name = name; }

    private bool _visible = true;
    public bool Visible
    {
        get { return this._visible; }
        set
        {
            if (this._visible == value) { return; }
            this._visible = value;
            this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Visible)));
        }
    }
    public event PropertyChangedEventHandler PropertyChanged;

    public string Name { get; }
    public override string ToString() => this.Name;
}

ViewModelです。
扱う全てのデータを保持するList型のItemsプロパティに、適当にデータを格納します。
次に、Itemsプロパティをsourceとする、Visibleプロパティでリアルタイムフィルター処理をするListCollectionView型プロパティを準備します。

using System.Collections.Generic;
using System.Linq;
using System.Windows.Data;

public class ViewModel
{
    public ViewModel()
    {
        this.Items = Enumerable.Range(0, 20)
            .Select(x => new Item("Item " + x))
            .ToList();

        this.FilteredItems = new ListCollectionView(this.Items)
        {
            Filter = obj => ((Item)obj).Visible,
            IsLiveFiltering = true,
        };
        this.FilteredItems.LiveFilteringProperties.Add(nameof(Item.Visible));
    }

    public List<Item> Items { get; }
    public ListCollectionView FilteredItems { get; }
}

Viewです。
左側のListBoxには全要素を表示、右側のListBoxにはリアルタイムフィルター処理結果の要素を表示します。

<Window x:Class="WpfTest.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:WpfTest"
        Title="MainWindow">
    <Window.DataContext>
        <local:ViewModel />
    </Window.DataContext>

    <UniformGrid Columns="2">
        <ListBox ItemsSource="{Binding Items}">
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <CheckBox Content="{Binding Name}"
                              IsChecked="{Binding Visible}" />
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>
        <ListBox ItemsSource="{Binding FilteredItems}" />
    </UniformGrid>
</Window>

実行して左側ListBoxのチェック状態を変更すると、連動して右側ListBox要素の表示/非表示が切り替わることを確認できます。

一度に大量の要素を変更した場合の性能(=遅いので注意)

上の例では、コレクション要素のVisibleプロパティはそれぞれのCheckBoxをポチポチした場合だけ変更されます。
それでは一度に大量に表示/非表示を切り替えた場合のパフォーマンスはどの程度なのか、というわけで試しました。

コレクションの要素数を10000に増やします。また、全要素のVisibleプロパティを変更するSetAllVisibleメソッドを追加します。Viewの変更は省略しますが、SetAllVisibleメソッドを呼び出すようにしました。

public class ViewModel
{
    public ViewModel()
    {
        this.Items = Enumerable.Range(0, 10000) // 20から増量
            .Select(x => new Item("Item " + x))
            .ToList();

        // FilteredItemsの初期化は上と同じなので省略
    }
    // プロパティ定義も上と同じなので省略

    // ViewのButton.Clickなどから呼び出します
    public void SetAllVisible(bool visible)
    {
        using (this.FilteredItems.DeferRefresh())
        {
            foreach (var item in this.Items)
            {
                item.Visible = visible;
            }
        }
    }
}

私の環境では、 全非表示→全表示 には16秒程度かかりました。なお全表示→全非表示 は1秒未満であり、大きな差がありました。
DeferRefresh Method を呼び出さない版も試してみましたが、所要時間は変わりませんでした。
(余談ですが、このメソッドは .NET3.0 から提供されているようです。Live Shaping以前でも何か用途があったのでしょうか。)

もしやFilter処理や表示用UI要素の構築が遅いのでは、と思いLive Shapingを使わない版を試しました。

public class ViewModel
{
    public ViewModel()
    {
        // Itemsの要素数は上と同じく10000

        // LiveFiltering関連のプロパティは設定しない
        // CheckBoxをポチポチしても表示/非表示は変化しなくなります
        this.FilteredItems = new ListCollectionView(this.Items)
        {
            Filter = obj => ((Item)obj).Visible,
        };
    }
    // プロパティ定義は同じなので省略

    public void SetAllVisible(bool visible)
    {
        foreach (var item in this.Items)
        {
            item.Visible = visible;
        }
        this.FilteredItems.Refresh();
    }
}

Filterにかかわるプロパティを変更した後に Refresh Method を明示的に呼び出さないと、表示/非表示は切り替わりません。
さて問題の所要時間は、 全非表示→全表示/全表示→全非表示 ともに、一瞬でした。LiveShapingは遅いのか……。

Live Shapingを有効にしたまま、全非表示→全表示 を高速に行う手段があれば良いのですがまだ分かっていません。