読者です 読者をやめる 読者になる 読者になる

tmori3y2のブログ

主にWindowsのプログラムなど

初心者が迷ってるReactiveCollectionとDataGridの重複値のチェック

C# MVVM ReactiveProperty WPF

2016/12/05 注記

この記事のModelのコレクションをReactiveCollectionで実装する方法には、問題があります。

理由は、Schedulerのデフォルトの動作で、ReactivePropertyはViewへの更新のみがRenderingスレッドで実行されるのに対して、ReactiveCollectionのAddOnSchedulerなどの操作を行うとコレクション操作がRenderingスレッドで実行されるので、ReactivePropertyとの処理タイミングのズレが発生し、トラブルになることです。

AddOnSchedulerなどの操作を行わないなら、ObservableCollectionで十分です。

時間があれば書き直しますが、ModelではObservableCollectionを使用してください。

本文

(2016/02/22 寝ぼけて思いっきり外したコードをUPしていたので訂正)

tmori3y2.hatenablog.com

では、Point (X, Y)をDataGridで直接編集するサンプルを示した。

例えば、Xが時間でYが何かの強度を表す時に、Xの重複を許したくない場合とかがある。

ここ2、3日、この検証を行うためのPointViewModel.Xの検証式を考えていた・・・

やりたいことは、

  • 行を追加した時とかに追加の検証を行う
  • Xを修正したときにエラー設定を行う
  • Xを修正したときにエラー解除を行う

よく複雑な検証はModelにやらせるとか色々書いてあるけど、ReactivePropertyの機能だけで実装することに拘った。

取りあえずCollectionの要素の検証でCollectionの中身をチェックさせるのは、ToReadOnlyReactiveCollection()のconverterでXにSetValidateNotifyError()をなすりつければ良いと思いついた・・・

色々試行錯誤したのがこちら。

            Points =
                // Observes the collection changed.
                model.Points
                // Initializes the collection.
                .ToReadOnlyReactiveCollection(m =>
                    {
                        var vm = new PointViewModel(m);
                        vm.X
                            .SetValidateNotifyError(s =>
                            {
                                NumberStyles style = NumberStyles.Number & ~NumberStyles.AllowTrailingSign;
                                IFormatProvider provider = CultureInfo.CurrentUICulture;

                                var input = s.ConvertBack(2);
                                var index = model.Points.IndexOf(m);
                                var list = model.Points
                                    .Select((p, i) =>
                                    {
                                        decimal value = decimal.Zero;
                                        if (i == index)
                                        {
                                            return input;
                                        }
                                        else 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 (count == 2) 何かの拍子にStackOverflowが出たので慌てて付けたがこのコードは動かなくなっていた・・・
                                {
                                    Points
                                        .Where(p =>
                                            p.X.HasErrors && (p != vm))
                                        .ToList()
                                        .ForEach(p =>
                                            p.X.ForceNotify());
                                }
                                return null;
                            });
                        return vm;
                    })
                // Disposes this collection if unused.
                .AddTo(disposables);

そこそこ動くが不具合がある。

重複行が3行あるときに、エラーマークが付いていない最初の行を変更した後、残りの2行を変更してもエラーが解除されない場合がある。

更に致命的なのは、挿入をした時にインデックスが正しく処理されずに例外が発生する。

まだまだ、検討の余地がある・・・