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を有効にしたまま、全非表示→全表示 を高速に行う手段があれば良いのですがまだ分かっていません。