Viewへリストの変更を通知するための機能を持つObservableCollection
ここら辺はReactiveProperty等を使用しても事情が同じなのでいかんともしがたいですね…(ReactiveProperty<List
変更が検出できない例
前述の検出ができない場合のコード例です。
ModelにObservavleCollectionの代わりにListが実装されていたりするケースで変更が検出できません。
// メインGUIのクラス public partial class MainWindow : Window { public MainWindow() { this.InitializeComponent(); this.DataContext = new ViewModel(); // ViewModelをここで設定 } } // Listを持っているモデル public class Model { public IList<string> List { get; private set; } = new List<string>(); } // モデルのListを監視対象にしたいViewModel public class ViewModel : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; private readonly Model model = new Model(); // ★★モデルのリストをデータソースにしてObservableCollectionを作成する public ObservableCollection<string> List => new ObservableCollection<string>(this.model.List); }
かなり特定の状況ですが、モデルが他所で作成されていて、画面に表示したいケースでデータがListとかになってるとちょっと扱いに困ります。普通はModel側に更新通知を出すイベントを出してもらうのがよさそうですが、変更できない場合少し考える必要がありそうです。
リストの長さの変更を監視して通知を出す
そこで、リストの長さを監視して要素数に変更があった場合通知を出すクラスを以下のように作成しました。
ちょっと長いですが以下の通り。
// リストの長さを定周期で監視して変更通知を出すクラス public class CollectionLengthWatcher<T> : IDisposable { private readonly IList<T> collection; // 監視対象のリスト private Action raiseAction; // 更新を検出したときの処理 private Timer timer = new Timer(); // 定周期監視用のタイマー private int latestcount; // 最後に確認したリストの長さ // 更新周期の設定と取得 public TimeSpan WatchInterval { get => TimeSpan.FromMilliseconds(this.timer.Interval); set => this.timer.Interval = value.TotalMilliseconds; } // 監視対象のリストと検出時の処理を指定してオブジェクトを初期化する public CollectionLengthWatcher(IList<T> collection, Action raiseAction) { this.timer.Interval = 100/*ms*/; this.collection = collection; this.raiseAction = raiseAction; this.latestcount = collection.Count; this.timer.Elapsed += this.Timer_Elapsed; } // 監視を開始する public void Start() => this.timer.Start(); // 監視を停止する public void Stop() => this.timer.Stop(); // IDisposableの実装 public void Dispose() { this.raiseAction = null; using (this.timer) { this.Stop(); } this.timer = null; } // リストの長さを監視する定周期処理 protected virtual void Timer_Elapsed(object sender, ElapsedEventArgs e) { try { this.timer.Stop(); if (this.latestcount != this.collection.Count) // 最後の長さと { this.raiseAction?.Invoke(); this.latestcount = this.collection.Count; } } finally { this.timer.Start(); } } }
使い方ですが、先ほどのViewModelクラスを以下のように修正します。CollectionLengthWatcherをメンバーに持たせてコンストラクタで監視する処理を追記します。
// モデルのListを監視対象にしたいViewModel public class ViewModel : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; private readonly Model model = new Model(); // ★追加 private readonly CollectionLengthWatcher<string> listWatcher; // モデルのリストをデータソースにしてObservableCollectionを作成する public ObservableCollection<string> List => new ObservableCollection<string>(this.model.List); // ★追加 public ViewModel() { // ViewModelのList<string> を監視対象に指定して // 変更を検出した場合、RaisePropertyChangedを呼び出す this.listWatcher = new CollectionLengthWatcher<string>(this.model.List, () => this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(this.model.List)))); // 監視を開始 this.listWatcher.Start(); } }
ちなみに、要素数しか検出できないので、同じ要素数で内容が変更されたときは対応できていません。リストの要素数がそんなに多くない場合は、数秒に一回ごとに強制的に更新通知だしてもそんなに問題ないような気がします。(もちろん数万件もリストがあったら(そもそもの設計としても)ダメだと思いますが…
動作イメージ
動かしてみたところですがそんなに違和感ない動きをしています。
以上です。