かずきのBlog@hatena

すきな言語は C# + XAML の組み合わせ。Azure Functions も好き。最近は Go 言語勉強中。日本マイクロソフトで働いていますが、ここに書いていることは個人的なメモなので会社の公式見解ではありません。

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