かずきのBlog@hatena

日本マイクロソフトに勤めています。XAML + C#の組み合わせをメインに、たまにASP.NETやJavaなどの.NET系以外のことも書いています。掲載内容は個人の見解であり、所属する企業を代表するものではありません。

ReactiveProperty v1.2.0をリリースしました

先日書いた記事で、めんどくさいと思ってた部分を簡単にかけるようにしました。

MVVMでめんどくさいと思ってる部分を、個人的にどうやって緩和してるか - かずきのBlog@hatena

データのバリデーションンを伴うMとVMのプロパティの同期は以下のように書く必要がありました。

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);                                  // 書き戻す
    } 
}

これを、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,
                ignoreValidationErrorValue: true)
            .SetValidateNotifyError(x => string.IsNullOrEmpty(x) ? "error": null);
         
        this.Age = model                                                    
            .ToReactivePropertyAsSynchronized(
                x => x.Age,
                convert: x => x.ToString(),
                convertBack: x => int.Parse(x),
                ignoreValidationErrorValue: true)
            .SetValidateNotifyError(x =>
                {
                    int result; // no use
                    return int.TryParse(x, out result) ? null : "error";
                });
    } 
}

今まではバリデーションエラーが、あろうが無かろうが、Mへ値が渡されてたのですが(しかも、convertBackに変な値がわたって例外がでると死ぬ)バリデーションエラーのときは値を流さないようにしました。個人的に気に入ってる。

動作は以下のようになります。

var model = new Person { Name = "tanaka", Age = 30 };
var vm = new PersonViewModel(model);

Console.WriteLine("{0} {1}", model.Name, model.Age); // tanaka 30

vm.Age.Value = "50"; // valid value
Console.WriteLine("{0} {1}", model.Name, model.Age); // tanaka 50

vm.Age.Value = "XX"; // invalid value
Console.WriteLine("{0} {1}", model.Name, model.Age); // tanaka 50

vm.Age.Value = "30"; // valid value
Console.WriteLine("{0} {1}", model.Name, model.Age); // tanaka 30