UWP & C#6.0のデータバインディング
WPFやWin8/8.1アプリでは、XAMLにバインドしたときに更新通知を行うためのINotifyPropertyChanged実装を楽にするために共通の基底クラスとしてBindableBaseというようなクラスを作ったり、ICommandの共通基底クラスとしてRelayCommand/DelegateCommandなどというクラスを作るのが、一つの定石となっていました。
(またはPrismやMVVM Light Toolkit, Livetなどといった、MVVMフレームワークを導入するか)
UWPのお勉強の手始めに、この手の基本クラスを作ってみたいと思います。
INotifyPropertyChanged/ICommandなどの共通基底クラス
今まで自分でWPFアプリを作るときには、↓で書いたようなクラスを使っていました。
これらのコードはそのままコピペでUWPのプロジェクトでも使用できます。
ですが、せっかくなのでC#6.0の機能を使って少し書き換えたいと思います。
といっても、書き換えたのは、「this.PropertyChanged?.Invoke」みたいな、null条件演算子を使ったイベント呼び出しの部分くらい。
BindableBase.cs
/// <summary> /// モデルを簡略化するための <see cref="INotifyPropertyChanged"/> の実装。 /// </summary> public abstract class BindableBase : INotifyPropertyChanged { /// <summary> /// プロパティの変更を通知するためのマルチキャスト イベント。 /// </summary> public event PropertyChangedEventHandler PropertyChanged; /// <summary> /// プロパティが既に目的の値と一致しているかどうかを確認します。必要な場合のみ、 /// プロパティを設定し、リスナーに通知します。 /// </summary> /// <typeparam name="T">プロパティの型。</typeparam> /// <param name="storage">get アクセス操作子と set アクセス操作子両方を使用したプロパティへの参照。</param> /// <param name="value">プロパティに必要な値。</param> /// <param name="propertyName">リスナーに通知するために使用するプロパティの名前。 /// この値は省略可能で、 /// CallerMemberName をサポートするコンパイラから呼び出す場合に自動的に指定できます。</param> /// <returns>値が変更された場合は true、既存の値が目的の値に一致した場合は /// false です。</returns> protected 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; } /// <summary> /// プロパティ値が変更されたことをリスナーに通知します。 /// </summary> /// <param name="propertyName">リスナーに通知するために使用するプロパティの名前。 /// この値は省略可能で、 /// <see cref="CallerMemberNameAttribute"/> をサポートするコンパイラから呼び出す場合に自動的に指定できます。</param> protected void OnPropertyChanged([CallerMemberName] string propertyName = null) { this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } }
RelayCommand.cs
/// <summary> /// その機能を中継することのみを目的とするコマンド /// デリゲートを呼び出すことにより、他のオブジェクトに対して呼び出します。 ///CanExecute メソッドの既定の戻り値は 'true' です。 /// <see cref="RaiseCanExecuteChanged"/> は、次の場合は必ず呼び出す必要があります。 /// <see cref="CanExecute"/> は、別の値を返すことが予期されます。 /// </summary> public class RelayCommand : ICommand { private readonly Action _execute; private readonly Func<bool> _canExecute; /// <summary> /// RaiseCanExecuteChanged が呼び出されたときに生成されます。 /// </summary> public event EventHandler CanExecuteChanged; /// <summary> /// 常に実行可能な新しいコマンドを作成します。 /// </summary> /// <param name="execute">実行ロジック。</param> public RelayCommand(Action execute) : this(execute, null) { } /// <summary> /// 新しいコマンドを作成します。 /// </summary> /// <param name="execute">実行ロジック。</param> /// <param name="canExecute">実行ステータス ロジック。</param> public RelayCommand(Action execute, Func<bool> canExecute) { if (execute == null) throw new ArgumentNullException("execute"); _execute = execute; _canExecute = canExecute; } /// <summary> /// 現在の状態でこの <see cref="RelayCommand"/> が実行できるかどうかを判定します。 /// </summary> /// <param name="parameter"> /// コマンドによって使用されるデータ。コマンドが、データの引き渡しを必要としない場合、このオブジェクトを null に設定できます。 /// </param> /// <returns>このコマンドが実行可能な場合は true、それ以外の場合は false。</returns> public bool CanExecute(object parameter) { return _canExecute == null ? true : _canExecute(); } /// <summary> /// 現在のコマンド ターゲットに対して <see cref="RelayCommand"/> を実行します。 /// </summary> /// <param name="parameter"> /// コマンドによって使用されるデータ。コマンドが、データの引き渡しを必要としない場合、このオブジェクトを null に設定できます。 /// </param> public void Execute(object parameter) { _execute(); } /// <summary> /// <see cref="CanExecuteChanged"/> イベントを発生させるために使用されるメソッド /// <see cref="CanExecute"/> の戻り値を表すために /// メソッドが変更されました。 /// </summary> public void RaiseCanExecuteChanged() { CanExecuteChanged?.Invoke(this, EventArgs.Empty); } } /// <summary> /// 任意の型の引数を1つ受け付けるRelayCommand /// </summary> /// <typeparam name="T"></typeparam> public class RelayCommand<T> : ICommand { private readonly Action<T> _execute; private readonly Func<T, bool> _canExecute; /// <summary> /// RaiseCanExecuteChanged が呼び出されたときに生成されます。 /// </summary> public event EventHandler CanExecuteChanged; /// <summary> /// 常に実行可能な新しいコマンドを作成します。 /// </summary> /// <param name="execute">実行ロジック。</param> public RelayCommand(Action<T> execute) : this(execute, null) { } /// <summary> /// 新しいコマンドを作成します。 /// </summary> /// <param name="execute">実行ロジック。</param> /// <param name="canExecute">実行ステータス ロジック。</param> public RelayCommand(Action<T> execute, Func<T, bool> canExecute) { if (execute == null) throw new ArgumentNullException("execute"); _execute = execute; _canExecute = canExecute; } /// <summary> /// 現在の状態でこの <see cref="RelayCommand"/> が実行できるかどうかを判定します。 /// </summary> /// <param name="parameter"> /// コマンドによって使用されるデータ。コマンドが、データの引き渡しを必要としない場合、このオブジェクトを null に設定できます。 /// </param> /// <returns>このコマンドが実行可能な場合は true、それ以外の場合は false。</returns> public bool CanExecute(object parameter) { return _canExecute == null ? true : _canExecute((T)parameter); } /// <summary> /// 現在のコマンド ターゲットに対して <see cref="RelayCommand"/> を実行します。 /// </summary> /// <param name="parameter"> /// コマンドによって使用されるデータ。コマンドが、データの引き渡しを必要としない場合、このオブジェクトを null に設定できます。 /// </param> public void Execute(object parameter) { _execute((T)parameter); } /// <summary> /// <see cref="CanExecuteChanged"/> イベントを発生させるために使用されるメソッド /// <see cref="CanExecute"/> の戻り値を表すために /// メソッドが変更されました。 /// </summary> public void RaiseCanExecuteChanged() { CanExecuteChanged?.Invoke(this, EventArgs.Empty); } }
参考
C#6.0の新たな言語仕様のnameof演算子やnull条件演算子を使えば、BindableBaseのような共通の基底クラスを使わずに、INotifyPropertyChangedをそのまま実装する、というのも選択肢の一つになってきたかもしれません。
この辺のお話は↓の記事にとても分かりやすくまとまっています。
http://okazuki.hatenablog.com/entry/2015/05/09/124333
PropertyChangedEventArgsのインスタンスが都度生成されないようにしてパフォーマンス改善、というのは目から鱗でした!!
ただ、共通の基底クラスを用いない実装方法は、プロパティ定義ごとに繰り返す定型コードがまだ多い気がします。
個人的には、もうちょいスッキリ書けるようになるまでは、BindableBaseみたいな共通基底クラスを使う方法を続けようかと思ってます。
使ってみる
ということで、このBindableBaseとRelayCommandを使って、UWPでのプロパティやコマンドのバインディング動作を確認してみます。
テキストボックスに入力した文字を、MainPageViewModelのNameプロパティにバインド。
そして、ボタン押下時にShowMessageCommandを実行するようにコマンドをバインドし、このコマンドで、Messageプロパティを変更し、その値をテキストブロックに表示します。
こんな感じ。
MainPage.xaml
<Page x:Class="App1.MainPage" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:local="using:App1" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d"> <Page.DataContext> <local:MainPageViewModel /> </Page.DataContext> <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"> <TextBox Width="300" Margin="10" HorizontalAlignment="Left" VerticalAlignment="Top" Text="{Binding Name, Mode=TwoWay}" TextWrapping="Wrap" /> <Button Margin="315,10,0,0" HorizontalAlignment="Left" VerticalAlignment="Top" Command="{Binding ShowMessageCommand}" Content="メッセージを表示" /> <TextBlock Margin="10,47,0,0" HorizontalAlignment="Left" VerticalAlignment="Top" Text="{Binding Message}" TextWrapping="Wrap" /> </Grid> </Page>
MainPage.xaml.cs
特に変更箇所なし
MainPageViewModel.cs
class MainPageViewModel : BindableBase { private string name; public string Name { get { return name; } set { this.SetProperty(ref this.name, value); } } private string message; public string Message { get { return message; } set { this.SetProperty(ref this.message, value); } } private RelayCommand showMessageCommand; public RelayCommand ShowMessageCommand { get { return showMessageCommand = showMessageCommand ?? new RelayCommand(ShowMessage); } } private void ShowMessage() { // テキストボックスに表示するメッセージを設定します。 this.Message = string.Format("こんにちは。{0}さん", this.Name); } }
長くなったので、今日はここまで。
次はUWPの新機能のx:Bindを使ってみます。