Prism(Xamarin Forms) における INotifyPropertyChanged
Xamarin Formsおいて ソース→ターゲット のデータバインディングでは、ソースオブジェクトに INotifyPropertyChangedインターフェイス を実装する必要があります(OneTimeモードは除く)。
この点について「素のXamarin Forms」と「Xamarin Forms With Prism」での実装方法を比較してみます。
また、Prism内部の実装を覗いて「ライブラリとして何をサポートしてくれているのか?」を見てみたいと思います。
こんなサンプルで説明します
以下のようなサンプルで説明を進めたいと思います。
実行画面は以下の通り。
サンプルの要点は以下の通りです。
- 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でソース一式が公開されています。
その中で 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]によってプロパティ名を暗黙的に取得している