MVVMのめんどくさいと感じてるところ
ModelとViewModelのクラスのマッピング
MVVMでアプリ組んでるとModelとViewModelで似た構造のクラスを作って、値の移し替えを行うことがあります。AutoMapperとか使ってもいいのですが、ReactivePropertyを使うことでも楽をすることができます。
以下のようなModelクラスがあるとします。(BindableBaseクラスはPrismのINotifyPropertyChangedを実装したクラスです)
public class Person : BindableBase { private string name; public string Name { get { return this.name; } set { this.SetProperty(ref this.name, value); } } private int age; public int Age { get { return this.age; } set { this.SetProperty(ref this.age, value); } } }
上記のクラスは2プロパティしかないですが、こんな感じでINotifyPropertyChangedの変更通知に対応したプロパティをたくさんもっているクラスがModelの中にはあります。
ModelからViewModelへの値の移し替えは、Codeplex.Reactive.Extensions名前空間に定義されているObserveProperty拡張メソッドを使うことで簡単に定義できます。
public class PersonViewModel { public ReactiveProperty<string> Name { get; private set; } public ReactiveProperty<string> Age { get; private set; } public PersonViewModel(Person model) { this.Name = model .ObserveProperty(x => x.Name) // IObservable<string>に変換 .ToReactiveProperty(); // ReactiveProperty<string>に変換 this.Age = model .ObserveProperty(x => x.Age) // IObservable<int>に変換 .Select(x => x.ToString()) // IObservable<string>に変換 .ToReactiveProperty(); // ReactiveProperty<string>に変換 } }
これで、Personクラスのプロパティが書き換わると、ViewModelのプロパティが書き換わる処理が定義できます。
var p = new Person { Name = "tanaka" }; var vm = new PersonViewModel(p); Console.WriteLine(vm.Name.Value); // tanaka p.Name = "kimura"; Console.WriteLine(vm.Name.Value); // kimura p.Age = 10; Console.WriteLine(vm.Age.Value); // 10
Model → ViewModel間の一方向データバインディングと見ることもできます。
ModelとViewModelの双方向バインディング
単純なケースでは、MとVM間のプロパティを双方向でバインドするToReactivePropertyAsSynchronizedメソッドを提供しています。
public class PersonViewModel { public ReactiveProperty<string> Name { get; private set; } public ReactiveProperty<string> Age { get; private set; } public PersonViewModel(Person model) { this.Name = model .ToReactivePropertyAsSynchronized(x => x.Name); // ReactiveProperty<string>に変換 this.Age = model .ToReactivePropertyAsSynchronized( x => x.Age, convert: x => x.ToString(), // M -> VMの変換処理 convertBack: x => // VM -> Mの変換処理 { try { return int.Parse(x); } catch { return -1; // error } }); } }
var p = new Person { Name = "tanaka", Age = 10 }; var vm = new PersonViewModel(p); Console.WriteLine("{0} {1}", vm.Name.Value, vm.Age.Value); // tanaka 10 vm.Name.Value = "kimura"; vm.Age.Value = "30"; Console.WriteLine("{0} {1}", p.Name, p.Age); // kimura 30 vm.Age.Value = "xxx"; // error!! Console.WriteLine("{0} {1}", p.Name, p.Age); // kimura -1
バリデーションとの連携
ToReactivePropertyAsSynchronized拡張メソッドは、シンプルな双方向のバインディングをサポートしていますが、エラーがあったらModelのデータを書き換えたくないという要望には対応していません。そこは、少しめんどくさいですが、自前で対応するしか無いです。
まず、順を追ってReactivePropertyの値の検証から説明します。ReactivePropertyのSetValidateNotifyErrorメソッドでReactivePropertyの値の検証が出来ます。(そのほかにも色々提供していますが一番単純な奴で)エラーがある場合はエラーメッセージを返して、エラーがない場合はnullを返すようにします。
public class PersonViewModel { public ReactiveProperty<string> Name { get; private set; } public ReactiveProperty<string> Age { get; private set; } public PersonViewModel(Person model) { this.Name = model .ObserveProperty(x => x.Name) // IObservable<string>に変換して .ToReactiveProperty() // ReactiveProperty<string>に変換して .SetValidateNotifyError(x => // 空文字の時はエラーにする string.IsNullOrWhiteSpace(x) ? "Name is required" : null); this.Age = model .ObserveProperty(x => x.Age) // IObservable<int>に変換して .Select(x => x.ToString()) // IObservable<string>に変換して .ToReactiveProperty() // ReactiveProperty<string>に変換して .SetValidateNotifyError(x => // int型に変換できない場合はエラーにする { int result; // no use return int.TryParse(x, out result) ? null : "Error"; }); } }
プロパティのエラーの有無は、HasErrorsプロパティで確認できます。
var p = new Person { Name = "tanaka", Age = 10 }; var vm = new PersonViewModel(p); Console.WriteLine(vm.Name.HasErrors); // False vm.Name.Value = ""; // Error! Console.WriteLine(vm.Name.HasErrors); // True Console.WriteLine(vm.Age.HasErrors); // False vm.Age.Value = "xxx"; // Error! Console.WriteLine(vm.Age.HasErrors); // True
エラーメッセージは、WPFの場合はValidation.Errors添付プロパティのErrorContentで取得できるのでXAMLで完結できます。WPF以外のプラットフォームでは、特に検証エラーのサポートが無いので多少めんどくさい手順を踏むことにになります。手順は別記事に譲ります。
追記!!めんどく無いようにしました。
ReactiveProperty v1.2.0をリリースしました - かずきのBlog@hatena
追記ここまで
VMのプロパティにエラーが無いとき、つまりReactivePropertyのHasErrorsプロパティがFalseの時に、VM → Mに値を移せばいいということになります。
public class PersonViewModel { public ReactiveProperty<string> Name { get; private set; } public ReactiveProperty<string> Age { get; private set; } public PersonViewModel(Person model) { this.Name = model // M -> VM .ObserveProperty(x => x.Name) // IObservable<string>に変換して .ToReactiveProperty() // ReactiveProperty<string>に変換して .SetValidateNotifyError(x => // 空文字の時はエラーにする string.IsNullOrWhiteSpace(x) ? "Name is required" : null); this.Name // VM -> M .Where(_ => !this.Name.HasErrors) // エラーが無いときは .Subscribe(x => model.Name = x); // 値を書き戻す this.Age = model // M -> VM .ObserveProperty(x => x.Age) // IObservable<int>に変換して .Select(x => x.ToString()) // IObservable<string>に変換して .ToReactiveProperty() // ReactiveProperty<string>に変換して .SetValidateNotifyError(x => // int型に変換できない場合はエラーにする { int result; // no use return int.TryParse(x, out result) ? null : ""; }); this.Age // VM -> M .Where(_ => !this.Age.HasErrors) // エラーが無いときは .Select(x => int.Parse(x)) // int型に変換して .Subscribe(x => model.Age = x); // 書き戻す } }
これで、バリデーションエラーのないときだけVM → Mへ値を書き戻す処理のあるViewModelが出来ました。
var p = new Person { Name = "tanaka", Age = 10 }; var vm = new PersonViewModel(p); Console.WriteLine(p.Name); // tanaka vm.Name.Value = "kimura"; Console.WriteLine(p.Name); // kimura vm.Name.Value = ""; // Error! Console.WriteLine(p.Name); // kimura
コレクションのマッピング
MとVMの値の連携がRxで簡単に出来るようになりました。ただ、通常は、単一のオブジェクトだけではなく、コレクションに入ったModelをVMのコレクションに変換するといった処理がよくあります。
ReactivePropertyでは、これを単純化するためにReadOnlyReactiveCollectionクラスを提供しています。これは、ObservableCollectionから簡単に作ることができます。
var m = new ObservableCollection<Person>(); var vm = m.ToReadOnlyReactiveCollection(x => new PersonViewModel(x)); // M -> VMの変換処理を渡すと後はよろしくしてくれる m.Add(new Person { Name = "tanaka", Age = 30 }); Console.WriteLine(vm[0].Name.Value); // tanaka m[0].Name = "kimura"; Console.WriteLine(vm[0].Name.Value); // kimura m.Add(new Person { Name = "nakata", Age = 40 }); Console.WriteLine(vm[1].Name.Value); // nakata
関連
ReactiveProperty オーバービュー - かずきのBlog@hatena
ReactiveProperty/README-ja.md at master · runceel/ReactiveProperty · GitHub