データの入れ物のModelに対するViewModelは大体こんな感じになるよねっていう感じのものを作ってみました。
とりあえず以下のようなイメージです。
- ModelはINotifyPropertyChangedを実装した値の入れ物のクラス
- ViewModelはそれをラップしてVからの入力値を保持するクラス
- ViewModelでは入力値の検証を行う
- ViewModelではModelのプロパティが変わったらそれを直ちに反映する
- ViewModelではVからの入力値にエラーがない状態でCommitメソッドを呼ばれたらModelに値を書き戻す
まず、値の入れ物のクラスを作ります。
class Person : BindableBase { privateint age; publicint Age { get { returnthis.age; } set { this.SetProperty(refthis.age, value); } } privatestring name; publicstring Name { get { returnthis.name; } set { this.SetProperty(refthis.name, value); } } }
ViewModelはこんな感じ。
class PersonViewModel : IDisposable { private CompositeDisposable Disposable { get; } = new CompositeDisposable(); public Person Model { get; } [Required] public ReactiveProperty<string> Name { get; } [Required] [RegularExpression("[0-9]+")] [Range(0, 100)] public ReactiveProperty<string> Age { get; } private ReactiveProperty<bool> hasErrors; public ReactiveProperty<bool> HasErrors => hasErrors ?? (hasErrors = Observable.CombineLatest( this.Name.ObserveHasErrors, this.Age.ObserveHasErrors) .Select(x => x.Any(y => y)) .ToReactiveProperty()); private Subject<Unit> CommitTrigger { get; } = new Subject<Unit>(); private IObservable<Unit> CommitAsObservable => this.CommitTrigger .Where(_ => !this.HasErrors.Value); public PersonViewModel(Person model) { this.Model = model; // Name propertythis.Name = this.Model .ObserveProperty(x => x.Name) .ToReactiveProperty() .SetValidateAttribute(() => this.Name) .AddTo(this.Disposable); this.CommitAsObservable .Select(_ => this.Name.Value) .Subscribe(x => this.Model.Name = x) .AddTo(this.Disposable); // Age propertythis.Age = this.Model .ObserveProperty(x => x.Age) .Select(x => x.ToString()) .ToReactiveProperty() .SetValidateAttribute(() => this.Age) .AddTo(this.Disposable); this.CommitAsObservable .Select(_ => this.Age.Value) .Select(x => int.Parse(x)) .Subscribe(x => this.Model.Age = x) .AddTo(this.Disposable); } publicvoid Commit() { this.CommitTrigger.OnNext(Unit.Default); } publicvoid Dispose() { this.Disposable.Dispose(); } }
M -> VMのプロパティの書き換えはObserveProperty -> ToReactivePropertyでやっています。基本ですね。SetValudateAttributeを使ってDataAnnnotationsのデータ検証を有効化しています。
次にVMのReactivePropertyにエラーがあるかどうかを返すHasErrorsというReactivePropertyを定義しています。これは、各プロパティのObserveHasErrorsをCombineLatestで結合して1つでもTrueがあったらTrueになるようにしています。
private ReactiveProperty<bool> hasErrors; public ReactiveProperty<bool> HasErrors => hasErrors ?? (hasErrors = Observable.CombineLatest( this.Name.ObserveHasErrors, this.Age.ObserveHasErrors) .Select(x => x.Any(y => y)) .ToReactiveProperty());
次に、値をVMからMに書き戻すためのきっかけとなるタイミングを制御するIObservableを作ります。これは以下のようにSubjectで発火したものをエラーが無いときだけ素通りさせるという感じでいきます。
private Subject<Unit> CommitTrigger { get; } = new Subject<Unit>(); private IObservable<Unit> CommitAsObservable => this.CommitTrigger .Where(_ => !this.HasErrors.Value);
このCommitAsObservableに値が流れてきたときにVM -> Mへの値の書き戻し処理を書きます。例としてAgeプロパティを。
// Age propertythis.Age = this.Model .ObserveProperty(x => x.Age) .Select(x => x.ToString()) .ToReactiveProperty() .SetValidateAttribute(() => this.Age) .AddTo(this.Disposable); this.CommitAsObservable .Select(_ => this.Age.Value) .Select(x => int.Parse(x)) .Subscribe(x => this.Model.Age = x) .AddTo(this.Disposable);
こんな感じでM -> VMとVM -> Mの値の移し替え処理を近いところに定義できるので個人的には書き忘れ起きにくいと思ってるので気に入ってます。
最後に、CommitでCommitTriggerに値を流します。
publicvoid Commit() { this.CommitTrigger.OnNext(Unit.Default); }
これで、Commitを呼ぶとエラーのないときだけ、VM -> Mへの値の書き戻し処理が走るようになります。
まだちょっとめんどいかな。