ぴーさんログ

だいたいXamarin.Formsのブログ

INotifyPropertyChangedなViewModelをReactivePropertyに移行するとどう変わるか

前回の記事でサンプル用に割とオーソドックスViewModelを作ったところ、ReactivePropertyに置き換えた場合と比較したら面白そうだと思ったので書いてみます。

目次

  1. INotifyPropertyChangedインターフェースを実装しなくなる
  2. 変更通知プロパティのコードが短くなる
  3. ICommand.CanExecuteChangedの管理がラクになる
  4. Binding記述が少し長くなる

1. INotifyPropertyChangedインターフェースを実装しなくなる

PropertyChangedイベントの発火はReactivePropertyがやってくれるので、ViewModelはINotifyPropertyChangedを実装する必要がありません。「全てのViewModelがINotifyPropertyChangedを実装した基底クラスから派生する」といった定番スタイルと比較しても、継承関係が自由になるという利点があります。

public class ViewModel /* : INotifyPropertyChanged */
{
    /* 略 */

/* まるっと不要になる
   public event PropertyChangedEventHandler PropertyChanged;

   protected void RaisePropertyChanged(string propertyName)
   {
       PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
   }
*/
}

2. 変更通知プロパティのコードが短くなる

使用前 (INotifyPropertyChanged)

private string entryText;
public string EntryText
{
    get { return entryText; }
    set
    {
        if(entryText != value)
        {
            entryText = value;
            RaisePropertyChanged(nameof(EntryText));
        }
    }
}

使用後 (ReactiveProperty)

public ReactiveProperty<string> EntryText
{
    get;
} = new ReactiveProperty<string>();

行数が約半分になります。タイプ数はコードスニペットで減らす事も可能ですが、行数が少ないと全体の見通しも良くなるのでReactivePropertyの方がいい感じです。

3. ICommand.CanExecuteChangedの管理がラクになる

ReactivePropertyの中には ReactiveCommand というICommand実装が含まれています。これはReactivePropertyや変更通知プロパティ、ReactiveExtensionsのイベントストリームなどをソースにインスタンスを生成する事が可能で、元ソースの変化に連動して ICommand.CanExecute() の状態を切り替えてくれます。つまるところ、自分で ICommand.CanExecuteChanged イベントを発火する必要がなくなります。

使用前 (INotifyPropertyChanged)

public Command SaveCommand
{
    get;
}

public ViewModel()
{
    // Command実行時の処理はインスタンス生成時に渡す
    SaveCommand = new Command(() => {
        Texts.Add(EntryText);
        EntryText = "";
    }, () => IsValid);

    // IsValid変更通知プロパティのsetterで発火してもいいが、SaveCommand生成の記述が分散してしまうので敢えてこのように書いている
    PropertyChanged += (sender, e) => {
        if(e.PropertyName == nameof(IsValid))
        {
            SaveCommand.ChangeCanExecute();
        }
    };
}

使用後 (ReactiveProperty)

public ReactiveCommand SaveCommand
{
    get;
}

public ReactiveViewModel()
{
    // ReactiveProperty<bool>から生成することでソースに連動してCanExecuteChangedイベントが発火される
    SaveCommand = IsValid.ToReactiveCommand(false);
    // Command実行時の処理はSubscribe時に渡す("Commandが実行された"というイベントを購読する)
    SaveCommand.Subscribe((_) => {
        Texts.Add(EntryText.Value);
        EntryText.Value = "";
    });
}

4. Binding記述が少し長くなる

ReactivePropertyで中の値にアクセスするには ReactiveProperty.Value プロパティを参照するというだけの話なのですが、いかんせん数が多くなりがちなので注意が必要です。忘れると「なぜ更新されないのか...」と軽くハマることもあるので気に留めておきましょう。

使用前 (INotifyPropertyChanged)

Text="{Binding EntryText, Mode=TwoWay}"

使用後 (ReactiveProperty)

Text="{Binding EntryText.Value, Mode=TwoWay}"

まとめ

たとえReactiveExtentionsを利用しなかったとしても、単純に楽ができるのでじゃんじゃん使ったらいいと思います。

以下全体の比較

使用前 (INotifyPropertyChanged)

using System.Collections.ObjectModel;
using System.ComponentModel;
using Xamarin.Forms;

namespace XFApp8
{
    public class ViewModel : INotifyPropertyChanged
    {
        private string entryText;
        public string EntryText
        {
            get { return entryText; }
            set
            {
                if(entryText != value)
                {
                    entryText = value;
                    RaisePropertyChanged(nameof(EntryText));
                }
            }
        }

        private bool isValid;
        public bool IsValid
        {
            get { return isValid; }
            private set
            {
                if(isValid != value)
                {
                    isValid = value;
                    RaisePropertyChanged(nameof(IsValid));
                }
            }
        }

        public ObservableCollection<string> Texts
        {
            get;
        } = new ObservableCollection<string>{ "first", "second", "third" };

        public Command ValidateCommand
        {
            get;
        }

        public Command SaveCommand
        {
            get;
        }

        public ViewModel()
        {
            ValidateCommand = new Command(() => IsValid = !string.IsNullOrEmpty(EntryText));
            SaveCommand = new Command(() => {
                Texts.Add(EntryText);
                EntryText = "";
            }, () => IsValid);

            PropertyChanged += (sender, e) => {
                if(e.PropertyName == nameof(IsValid))
                {
                    SaveCommand.ChangeCanExecute();
                }
            };
        }

       #region INotifyPropertyChanged implementation
        public event PropertyChangedEventHandler PropertyChanged;

        protected void RaisePropertyChanged(string propertyName)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
       #endregion
    }
}

使用後 (ReactiveProperty)

using System;
using System.Collections.ObjectModel;
using Reactive.Bindings;

namespace XFApp8
{
    public class ReactiveViewModel
    {
        public ReactiveProperty<string> EntryText
        {
            get;
        } = new ReactiveProperty<string>();

        public ReactiveProperty<bool> IsValid
        {
            get;
        } = new ReactiveProperty<bool>(false);

        public ObservableCollection<string> Texts
        {
            get;
        } = new ObservableCollection<string>{ "first", "second", "third" };

        public ReactiveCommand ValidateCommand
        {
            get;
        } = new ReactiveCommand();

        public ReactiveCommand SaveCommand
        {
            get;
        }

        public ReactiveViewModel()
        {
            ValidateCommand.Subscribe((_) =>
                IsValid.Value = !string.IsNullOrEmpty(EntryText.Value));

            SaveCommand = IsValid.ToReactiveCommand(false);
            SaveCommand.Subscribe((_) => {
                Texts.Add(EntryText.Value);
                EntryText.Value = "";
            });
        }
    }
}