Prism(Xamarin Forms) における INotifyPropertyChanged

Xamarin Formsおいて ソース→ターゲット のデータバインディングでは、ソースオブジェクトに INotifyPropertyChangedインターフェイス を実装する必要があります(OneTimeモードは除く)。

この点について「素のXamarin Forms」と「Xamarin Forms With Prism」での実装方法を比較してみます。

また、Prism内部の実装を覗いて「ライブラリとして何をサポートしてくれているのか?」を見てみたいと思います。

こんなサンプルで説明します

以下のようなサンプルで説明を進めたいと思います。
実行画面は以下の通り。

f:id:daigo-knowlbo:20170109015009p:plain

サンプルの要点は以下の通りです。

  • R G Bの各値をソースとして画面上の各ラベルにデータバインドする
  • 「set White」/「set Black」/「set Yellow」のボタンをクリックするとソースの RGB値 がロジック上で変更される
  • ロジック上で変更された RGB値 は 即座に画面表示(ラベル)に反映される

素のXamarin Formsでは

ソースオブジェクトとして「MyColorクラス」を実装する事とします。
ソースオブジェクトは INotifyPropertyChangedインターフェイス を実装します。
ソース値変更時には PropertyChanged を呼び出します。

// MyColor.cs(ソースとなるオブジェクト)
using System;
using System.ComponentModel;

namespace Example1.Models
{
  public class MyColor : INotifyPropertyChanged
  {
    // INotifyPropertyChangedインターフェイスの実装
    public event PropertyChangedEventHandler PropertyChanged;

    // Fields
    private int red;
    private int green;
    private int blue;

    // Properties
    public int Red {
      get {
        return this.red;
      }
      set {
        if (this.red != value) {
          this.red = value;
          OnPropertyChanged("Red");
        }
      }
    }

    public int Green {
      get {
        return this.green;
      }
      set {
        if (this.green != value) {
          this.green = value;
          OnPropertyChanged("Green");
        }
      }
    }

    public int Blue {
      get {
        return this.blue;
      }
      set {
        if (this.blue != value) {
          this.blue = value;
          OnPropertyChanged("Blue");
        }
      }
    }

    // プロパティ値の変更を通知します
    protected virtual void OnPropertyChanged(string propertyName)
    {
      PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
  }
}
  • PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    「?」はC#言語仕様で「nullでなければ呼び出す」、つまりPropertyChangedがnullでなければInvoke()を呼び出します。

ページの実装は以下の通りです。

// CustomColorPage.xaml(ページ定義)
<?xml version="1.0" encoding="utf-8"?>
<ContentPage
  xmlns="http://xamarin.com/schemas/2014/forms" 
  xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" 
  x:Class="Example1.CustomColorPage">

  <StackLayout HorizontalOptions="Center" VerticalOptions="Center">
    <StackLayout Orientation="Horizontal">
      <Label Text="Red:"/>
      <Label Text="{Binding Red}" />
    </StackLayout>
    <StackLayout Orientation="Horizontal">
      <Label Text="Green:"/>
      <Label Text="{Binding Green}" />
    </StackLayout>
    <StackLayout Orientation="Horizontal">
      <Label Text="Blue:"/>
      <Label Text="{Binding Blue}" />
    </StackLayout>
    <StackLayout Orientation="Vertical">
      <Button Text="set White" Clicked="whiteButtonClicked"/>
      <Button Text="set Black" Clicked="blackButtonClicked"/>
      <Button Text="set Yellow" Clicked="yellowButtonClicked"/>
    </StackLayout>
  </StackLayout>
</ContentPage>

コードビハインドクラスで、ソースオブジェクト MyColor をインスタンス化して、BindingContextに設定する事でデータバインディングを行います。

// CustomColorPage.xaml.cs(コードビハインドクラス)
using System;
using Xamarin.Forms;

using Example1.Models;

namespace Example1
{
  public partial class CustomColorPage : ContentPage
  {
    // ソースオブジェクト
    public MyColor MyColor { get; } = new MyColor();

    // コンストラクタ
    public CustomColorPage()
    {
      InitializeComponent();

      this.BindingContext = this.MyColor;
    }

    // Whiteボタンクリックイベントハンドラ
    public void whiteButtonClicked(object sender, EventArgs e)
    {
      this.MyColor.Red = 255;
      this.MyColor.Green = 255;
      this.MyColor.Blue = 255;
    }

    // Blackボタンクリックイベントハンドラ
    public void blackButtonClicked(object sender, EventArgs e)
    {
      this.MyColor.Red = 0;
      this.MyColor.Green = 0;
      this.MyColor.Blue = 0;
    }

    // Yellowボタンクリックイベントハンドラ
    public void yellowButtonClicked(object sender, EventArgs e)
    {
      this.MyColor.Red = 255;
      this.MyColor.Green = 255;
      this.MyColor.Blue = 0;
    }
  }
}

Prismでは

ソースオブジェクトは BindableBaseクラス を継承します。
ソースオブジェクトはViewModelクラスとします。
ソース変更時には SetProperty() メソッドを呼び出します。

// MainPageViewModel.cs(ビューモデルクラス)
using Prism.Commands;
using Prism.Mvvm;
using System.Windows.Input;

namespace PrismExample1.ViewModels
{
  public class CustomColorPageViewModel : BindableBase
  {
    // Fields
    private int red;
    private int green;
    private int blue;

    // Properties(for DataBind)
    public int Red {
      get {
        return this.red;
      }
      set {
        this.SetProperty(ref this.red, value);
      }
    }

    public int Green {
      get {
        return this.green;
      }
      set {
        this.SetProperty(ref this.green, value);
      }
    }

    public int Blue {
      get {
        return this.blue;
      }
      set {
        this.SetProperty(ref this.blue, value);
      }
    }

    // コマンド
    public ICommand WhiteCommand { get; }

    public ICommand BlackCommand { get; }

    public ICommand YellowCommand { get; }

    // Constructor
    public CustomColorPageViewModel()
    {
      // ボタンクリックコマンド時のイベント処理
      this.WhiteCommand = new DelegateCommand(() =>
      {
        this.Red = 255;
        this.Green = 255;
        this.Blue = 255;
      });
    
      this.BlackCommand = new DelegateCommand(() =>
      {
        this.Red = 0;
        this.Green = 0;
        this.Blue = 0;
      });

      this.YellowCommand = new DelegateCommand(() =>
      {
        this.Red = 255;
        this.Green = 255;
        this.Blue = 0;
      });
    }
  }
}

ページ実装は以下の通りです。
ボタンクリック時の挙動は Command をCustomColorPageViewModeの各ICommandにデータバインディングします。

// CustomColorPage.xaml(ページ定義)
<?xml version="1.0" encoding="utf-8"?>
<ContentPage 
  xmlns="http://xamarin.com/schemas/2014/forms" 
  xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" 
  xmlns:prism="clr-namespace:Prism.Mvvm;assembly=Prism.Forms" 
  prism:ViewModelLocator.AutowireViewModel="True" 
  x:Class="PrismExample1.Views.CustomColorPage" 
  Title="MainPage">
  <StackLayout HorizontalOptions="Center" VerticalOptions="Center">
    <StackLayout Orientation="Horizontal">
      <Label Text="Red:"/>
      <Label Text="{Binding Red}" />
    </StackLayout>
    <StackLayout Orientation="Horizontal">
      <Label Text="Green:"/>
      <Label Text="{Binding Green}" />
    </StackLayout>
    <StackLayout Orientation="Horizontal">
      <Label Text="Blue:"/>
      <Label Text="{Binding Blue}" />
    </StackLayout>
    <StackLayout Orientation="Vertical">
      <Button Text="set White" Command="{Binding WhiteCommand}"/>
      <Button Text="set Black" Command="{Binding BlackCommand}"/>
      <Button Text="set Yellow" Command="{Binding YellowCommand}"/>
    </StackLayout>
  </StackLayout>
</ContentPage>

SetProperty()とは

Prism版ではソースオブジェクトのセッター内で、SetProperty()メソッドというものが使われています(素のXamarin Forms実装では「PropertyChangedEventHandler」を扱う部分)。
SetProperty()メソッドは、Prismライブラリ内の「Prism.Mvvm.BindableBaseクラス(Prismアセンブリ)」で実装されています。
Prismはオープンソースとして以下のGithubでソース一式が公開されています。

github.com

その中で BindableBaseクラス の実装は以下です。

Prism/BindableBase.cs at master · PrismLibrary/Prism · GitHub

2017/1/8時点の実装ソースを抜粋させていただくと以下となります(コメント文は除去しています)。

using System;
using System.ComponentModel;
using System.Linq.Expressions;
using System.Runtime.CompilerServices;

namespace Prism.Mvvm
{
  /// <summary>
  /// Implementation of <see cref="INotifyPropertyChanged"/> to simplify models.
  /// </summary>
  public abstract class BindableBase : INotifyPropertyChanged
  {
    public event PropertyChangedEventHandler PropertyChanged;

    protected virtual bool SetProperty<T>(
      ref T storage,
      T value,
      [CallerMemberName] string propertyName = null)
    {
      if (object.Equals(storage, value)) return false;

      storage = value;
      this.OnPropertyChanged(propertyName);

      return true;
    }

    protected virtual void OnPropertyChanged(
      [CallerMemberName]string propertyName = null)
    {
      PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }

    protected virtual void OnPropertyChanged<T>(
      Expression<Func<T>> propertyExpression)
    {
      var propertyName = PropertySupport.ExtractPropertyName(propertyExpression);
      this.OnPropertyChanged(propertyName);
    }
  }
}

SetProperty()メソッドの引数は3つ。
第1引数は ref でプロパティの値を保持する変数(フィールド変数)を受け取ります。
第2引数は 変更後の値 を受け取ります。
第3引数は 変更が発生したプロパティ名を文字列で受け取ります。[CallerMemberName]属性が付けられ、デフォルト値としてnullが指定されています。CallerMemberName属性はC# 5で導入された属性で「呼び出し元のプロパティ名・メソッド名」が割り当てられます。つまり、ソースオブジェクトのプロパティセッターからSetProperry()を呼び出す場合、第3引数は省略しても暗黙的にプロパティ名が指定されます。

では、続けて SetProperty() の内部実装に目を移します。

if (object.Equals(storage, value)) return false;

現在値と変更値の比較を行い、変更がなければそのままリターンします。

storage = value;

値の変更をフィールド変数に代入しています。

this.OnPropertyChanged(propertyName);

OnPropertyChanged()メソッドの呼び出しを行なっています。

OnPropertyChanged()メソッドの実装は以下の通りです。

PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));

PropertyChangedは「event PropertyChangedEventHandler」として定義されたイベント変数です。
これは、素のXamarin Formsにおける実装と同様ですね。

まとめ

つまり・・・データバインディング周りの実装に関して、PrismのSetProperty()によって以下のような点がラップされ便利になっています。
* 値の変更チェックをラップしている
* PropertyChanged呼び出しをラップしている
* [CallerMemberName]によってプロパティ名を暗黙的に取得している