初心者が迷ったReactiveCollectionとDataGridの重複値のチェック~リターンズ
2016/12/05 注記
この記事のModelのコレクションをReactiveCollectionで実装する方法には、問題があります。
理由は、Schedulerのデフォルトの動作で、ReactivePropertyはViewへの更新のみがRenderingスレッドで実行されるのに対して、ReactiveCollectionのAddOnSchedulerなどの操作を行うとコレクション操作がRenderingスレッドで実行されるので、ReactivePropertyとの処理タイミングのズレが発生し、トラブルになることです。
AddOnSchedulerなどの操作を行わないなら、ObservableCollectionで十分です。
時間があれば書き直しますが、ModelではObservableCollectionを使用してください。
本文
(2016/02/23 図追加)
(2016/02/23 コード修正)
やはり、ReactivePropertyのネタの掴みは抜群ですね・・・
アクセス数が跳ね上がります。
前回は、ToReadOnlyReactiveCollection()のコンバータでSetValidateNotifyError()を擦りつけられるのが面白くて、つい中途半端な投稿をしましたが、見栄えが悪いのでコレクションへの追加時に擦りつけます。
Points
.ObserveAddChangedItems()
.Subscribe(vms =>
{
vms.ToList()
.ForEach(vm =>
{
vm.X
.SetValidateNotifyError(s =>
{
// 追加のチェックをごにょごにょ・・・
});
});
})
「大体、コードの大まかな解説すらないし・・・」
前回の敗因は2つ。
- IndexOf()の使用
- ForceNotify()をブロックせずに使用
まず、SetValidateNotifyError()では、エラーではじかれているセルがあっても、出来るだけ見たままの表の値で重複チェック用のリストを作成します。
sは、SetValidateNotifyError()のパラメータです。
NumberStyles style = NumberStyles.Number & ~NumberStyles.AllowTrailingSign; IFormatProvider provider = CultureInfo.CurrentUICulture; //var input = s.ConvertBack(2); NG decimal input = decimal.Zero; if (!decimal.TryParse(s, style, provider, out input)) { return null; } var list = model.Points .Select((p, i) => { decimal value = decimal.Zero; if (Points[i] == vm) { return input; } if (Points[i].X.HasErrors && decimal.TryParse(Points[i].X.Value, style, provider, out value)) { return value; } else { return p.X.Value; } }) .ToList();
- 編集途中の値は変換してリストに入れる
このチェックルーチンが最後に実行されるので、そのままdecimalに変換しても問題ない- 他のチェックでエラーがあっても実行されるようです。ブランクにすると例外が出て以降チェックが出来なるのでTryParseでチェックする必要があります・・・(2016/02/23)
- 重複などでエラー状態の値は、もう一度変換をトライして、パスしたらリストに入れる
- エラーなしや、上記のチェックでパスしなかったものは、modelの値をリストに入れる
あとは、
- 編集中の値と重複するものがあれば、編集したセルにエラーをセット
- エラーがなければ、エラーが解除されたセルがないかを強制評価
var count = list .Where(x => x == input) .Count(); if (count > 1) { return "Cannot set the duplicate X."; } else if (!blocker.Blocked) { using (blocker.Enter()) { Points .Where(p => p.X.HasErrors && (p != vm)) .ToList() .ForEach(p => p.X.ForceNotify()); } } return null;
強制評価で、再入チェックをしています。
コレクションの初期化と、チェック周りの追加コードの全体は以下になります。
Points = // Observes the collection changed. model.Points // Initializes the collection. .ToReadOnlyReactiveCollection(m => new PointViewModel(m)) // Disposes this collection if unused. .AddTo(disposables); Points .ObserveAddChangedItems() .Subscribe(vms => { vms.ToList() .ForEach(vm => { vm.X .SetValidateNotifyError(s => { NumberStyles style = NumberStyles.Number & ~NumberStyles.AllowTrailingSign; IFormatProvider provider = CultureInfo.CurrentUICulture; // var input = s.ConvertBack(2); NG decimal input = decimal.Zero; if (!decimal.TryParse(s, style, provider, out input)) { return null; } var list = model.Points .Select((p, i) => { decimal value = decimal.Zero; if (Points[i] == vm) { return input; } if (Points[i].X.HasErrors && decimal.TryParse(Points[i].X.Value, style, provider, out value)) { return value; } else { return p.X.Value; } }) .ToList(); var count = list .Where(x => x == input) .Count(); if (count > 1) { return "Cannot set the duplicate X."; } else if (!blocker.Blocked) { using (blocker.Enter()) { Points .Where(p => p.X.HasErrors && (p != vm)) .ToList() .ForEach(p => p.X.ForceNotify()); } } return null; }); }); }) .AddTo(disposables);