Model View ViewModelパターン(以下MVVMパターン)が登場して約10年になります。 ここらへんで一度MVVMを実装するうえで必要になる技術要素を振り返ってみたいと思います。
その前にMVVM
MVVMは以下のWikipediaあたりでも見てください。
Model View ViewModel - Wikipedia
見た目と、それ以外にクラスを分離して、さらに見た目をXAMLで作りやすいようにViewとViewModelに分離したようなイメージです。
見ていこう
ということでMVVMで必要になる技術要素を見ていこうと思います。
INotifyPropertyChangedインターフェース
まずは、これが無いと始まりません。MVVMではViewはViewModelを監視して、ViewModelはModelを監視していることが多いです。その時に、クラスのプロパティが変わったことを通知するためにC#に用意されている共通のインターフェースがSystem.ComponentModel.INotifyPropertyChanged
インターフェースになります。
このインターフェースはPropertyChanged
イベントを持つだけのシンプルなクラスになります。このインターフェースを実装したクラスでは、プロパティのsetterでPropertyChanged
イベントの引数にPropertyChangedEventArgs
でプロパティ名を渡したものを使ってイベントを発火することで、外部に対してプロパティの変更通知が出来ます。
例えば、以下のような実装になります。
public class Person : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; private string name; public string Name { get { return this.name; } set { this.name = value; this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Name))); } } }
以下のようなコードで動作が確認できます。これは、XamarinでもUWPでもない、プレーンなコンソールアプリケーションです。
public class Program { public static void Main(string[] args) { var p = new Person(); p.PropertyChanged += (_, e) => Console.WriteLine($"{e.PropertyName} changed."); p.Name = "tanaka"; p.Name = "okazuki"; } }
Person
クラスのインスタンスを作成して、PropertyChanged
イベントを購読しています。購読した先では変更のあったプロパティ名を表示しています。そして、2回値を代入しています。実行すると以下のような結果になります。
Name changed. Name changed.
とても単純ですね。単純さ故に無駄もあります。例えば、コードを以下のように書き換えてみましょう。
public class Program { public static void Main(string[] args) { var p = new Person(); p.PropertyChanged += (_, e) => Console.WriteLine($"{e.PropertyName} changed."); p.Name = "tanaka"; p.Name = "tanaka"; } }
実行すると先ほどと同じように2回Name changed.
が表示されます。値が変わってないのに値が変わったと表示されるのは無駄なので、Name
プロパティを以下のように書き換えます。
public class Person : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; private string name; public string Name { get { return this.name; } set { // 比較して同じなら何もしない if (EqualityComparer<string>.Default.Equals(this.name, value)) { return; } this.name = value; this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Name))); } } }
これで実行すると以下のようになります。
Name changed.
これで無駄なイベントの発行がなくなりました。
共通クラス化
MVVMでは、INotifyPropertyChangedを実装したクラスを大量に作成します。そのプロパティすべてに上記のような記述をするのは冗長です。そのため、以下のようなベースクラスを作成するのが一般的です。
public class BindableBase : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; protected virtual void OnPropertyChanged([CallerMemberName]string propertyName = null) => this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); protected virtual bool SetProperty<T>(ref T field, T value, [CallerMemberName]string propertyName = null) { if (EqualityComparer<T>.Default.Equals(field, value)) { return false; } field = value; this.OnPropertyChanged(propertyName); return true; } }
このBindableBase
クラスを使うことでPerson
クラスは以下のようになります。
public class Person : BindableBase { private string name; public string Name { get { return this.name; } set { this.SetProperty(ref this.name, value); } } }
public string Name { get; set; }
に比べると冗長ですが、現状これが精一杯です。
XAML
XAMLはMVVMパターンでViewを記述するのに使います。XAMLはXMLをベースとした言語で階層構造を持ったオブジェクトを組み立てることに特化した言語になります。基本的に以下のようなルールがあります。
- XML名前空間はC#の名前空間に対応
- タグ名はクラス名に対応
- 属性はプロパティに対応
例えば、XamarinApp.Models
名前空間にあるPerson
クラスをインスタンス化してNameプロパティにtanakaと設定するコードはC#で書くと以下のようになると思います。
new Person { Name = "tanaka" }
これをXAMLで書くと以下のようになります。
<!-- Xamarin.Formsの場合 --> <Models:Person xmlns:Models="clr-namespace:XamarinApp.Models" Name="tanaka" /> <!-- UWPの場合 --> <Models:Person xmlns:Models="using:XamarinApp.Models" Name="tanaka" />
XML名前空間内でのC#の名前空間の記述方法が違う以外は同じ書き方になります。
プロパティのもう1つの書き方として属性ではなくタグを使用して書く方法があります。
<!-- Xamarin.Formsの場合 --> <Models:Person xmlns:Models="clr-namespace:XamarinApp.Models"> <Models:Person.Name>tanaka</Models.Person.Name> </Models:Person> <!-- UWPの場合 --> <Models:Person xmlns:Models="using:XamarinApp.Models"> <Models:Person.Name>tanaka</Models.Person.Name> </Models:Person>
クラス名.プロパティ名
という命名規約でタグを書くことで、そのタグの子要素がプロパティに設定されます。この書き方のいいところは、プロパティの値として、複雑なクラスを定義できるという点です。上記の例のような文字列では、ありがたみがわかりませんが、以下のようにPersonクラスに、Person型のChildプロパティがあって、そこに値を設定するといったときに効果を発揮します。
<!-- Xamarin.Formsの場合 --> <Models:Person xmlns:Models="clr-namespace:XamarinApp.Models" Name="tanaka"> <Models:Person.Child> <Models:Person Name="kimura" /> </Models:Person.Child> </Models:Person> <!-- UWPの場合 --> <Models:Person xmlns:Models="using:XamarinApp.Models" Name="tanaka"> <Models:Person.Child> <Models:Person Name="kimura" /> </Models:Person.Child> </Models:Person>
XAMLには、コンテンツプロパティというものがあって、タグの直下に何も指定せずに書いた場合は、クラス単位に指定されたプロパティに値をセットするということが出来ます。例えば、上記の例でChildプロパティがコンテンツプロパティだった場合、以下のようにシンプルに書くことが出来ます。
<!-- Xamarin.Formsの場合 --> <Models:Person xmlns:Models="clr-namespace:XamarinApp.Models" Name="tanaka"> <Models:Person Name="kimura" /> </Models:Person> <!-- UWPの場合 --> <Models:Person xmlns:Models="using:XamarinApp.Models" Name="tanaka"> <Models:Person Name="kimura" /> </Models:Person>
さらに、XAMLにはコレクションのプロパティに対して値を設定するときに、以下のようにタグを並べて定義することが出来ます。
<!-- Xamarin.Formsの場合 --> <Models:Person xmlns:Models="clr-namespace:XamarinApp.Models" Name="tanaka"> <Models:Person.Children> <Models:Person Name="kimura1" /> <Models:Person Name="kimura2" /> <Models:Person Name="kimura3" /> <Models:Person Name="kimura4" /> </Models:Person.Children> </Models:Person> <!-- UWPの場合 --> <Models:Person xmlns:Models="using:XamarinApp.Models" Name="tanaka"> <Models:Person.Children> <Models:Person Name="kimura1" /> <Models:Person Name="kimura2" /> <Models:Person Name="kimura3" /> <Models:Person Name="kimura4" /> </Models:Person.Children> </Models:Person>
Childrenがコンテンツプロパティだった場合は、以下のように書くこともできます。
<!-- Xamarin.Formsの場合 --> <Models:Person xmlns:Models="clr-namespace:XamarinApp.Models" Name="tanaka"> <Models:Person Name="kimura1" /> <Models:Person Name="kimura2" /> <Models:Person Name="kimura3" /> <Models:Person Name="kimura4" /> </Models:Person> <!-- UWPの場合 --> <Models:Person xmlns:Models="using:XamarinApp.Models" Name="tanaka"> <Models:Person Name="kimura1" /> <Models:Person Name="kimura2" /> <Models:Person Name="kimura3" /> <Models:Person Name="kimura4" /> </Models:Person>
XAMLには、オブジェクトに対して、別のオブジェクトのプロパティを設定するという添付プロパティというものがあります。 この添付プロパティは、レイアウトの情報をコントロールに追加するときによく使われます。例えば、Gridという格子状に領域を区切って、そこにコントロールを置くことが出来るレイアウトパネルがあります。このとき、Gridの中の要素に対して何行何列目に置くという指定がしたくなります。そういうようなケースで添付プロパティが活躍します。
添付プロパティは、クラス名.添付プロパティ名
という形で指定します。
<Grid> <!-- 行と列の定義は省略 --> <Button Grid.Row="0" Grid.Column="1" ... /> <Button Grid.Row="1" Grid.Column="1" ... /> </Grid>
上記の例では、最初のボタンではGrid.Rowという添付プロパティとGrid.Columnという添付プロパティを使って、0行1列目にボタンを置くという指定をしています。このように、Gridのプロパティを、あたかもButtonのプロパティであるかのように指定できるところが添付プロパティの特徴です。
最後にマークアップ拡張について説明します。 マークアップ拡張は、オブジェクトを組み立てるためのショートカットの記法です。普通ならXMLのタグを使って組み立てないといけないような複雑なオブジェクトを、属性に対して書いたりすることが出来るようになります。
マークアップ拡張は{}
で括られたものになります。例えばリソースを参照するStaticResource
マークアップ拡張や、後述するデータバインディングで使用するBinding
マークアップ拡張があります。マークアップ拡張を使ったケースと使わないケースの比較のために同じ意味合いのXAMLを書いてみます。
<Label Text="{Binding Name, Mode=OneWay}" /> <Label> <Label.Text> <Binding Path="Name" Mode="OneWay" /> </Label.Text> </Label>
データバインディング
MVVMにおいて、ViewとViewModelを紐づける方法としてデータバインディングが使われます。データバインディングをざっくりと説明すると、画面のコントロールのプロパティ(厳密にはXamarin.Formsではバインダブルプロパティ、UWPでは依存関係プロパティ)と、普通のクラスのオブジェクトのプロパティの同期をとる仕組みです。
Xamarin.Formsでは、BindableObject
で定義されたBindingContext
プロパティに設定されたオブジェクトがBinding
のPath
の起点になります。UWPでは、FrameworkElement
で定義されたDataContext
プロパティに設定されたオブジェクトがBinding
のPath
の起点になります。
Path
の起点になるとは以下のような動作のことです。
例としてPerson
クラスをバインドしてみます。
// さっき定義したBindableBaseクラスを使用 public class Person : BindableBase { private string name; public string Name { get { return this.name; } set { this.SetProperty(ref this.name, value); } } }
Xamarin.Formsでは、以下のようになります。
<?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:Models="clr-namespace:XamarinApp.Models" x:Class="XamarinApp.Views.MainPage"> <ContentPage.BindingContext> <Models:Person Name="tanaka" /> </ContentPage.BindingContext> <StackLayout> <Entry Text="{Binding Name, Mode=TwoWay}" /> <Label Text="{Binding Name, Mode=OneWay}" /> </StackLayout> </ContentPage>
UWPでは、以下のようになります。
<Page x:Class="UWPApp.MainPage" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="using:UWPApp" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:Models="using:UWPApp.Models" mc:Ignorable="d"> <Page.DataContext> <Models:Person Name="kimura" /> </Page.DataContext> <StackPanel Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"> <TextBox Text="{Binding Name, Mode=TwoWay}" /> <TextBlock Text="{Binding Name, Mode=OneWay}" /> </StackPanel> </Page>
このプログラムを実行することで、入力した値が、下のテキストにPerson
クラスのName
プロパティを経由して同期されます。このときの値の変更タイミングの検知には、最初に開設したINotifyPropertyChanged
インターフェースが使用されています。
Bindingには、Modeがあります。Modeには以下のようなものがあります。
- OneWay: ソース(C#のオブジェクト)からターゲット(画面のコントロール側)への1方向同期
- TwoWay: ソースとターゲットの双方向同期
- OneTime: ソースからターゲットへの1度きりの同期
指定しない場合は、デフォルトでOneWayになります。
UWP固有の機能
UWP固有の機能としてコンパイル時データバインディングというものがあります。ここでは詳しく述べませんが、以下の記事を参照してみてください。
コマンド
Viewで起きたイベントをViewModelに伝える手段としてCommandというものが用いられます。Commandの実態は、ICommandインターフェースという、自身が実行可能かどうかという状態と、実行するというExecuteメソッドを持っただけのシンプルなインターフェースになります。
public interface ICommand { event EventHandler CanExecuteChanged; bool CanExecute(object parameter); void Execute(object parameter); }
ExecuteメソッドとCanExecuteメソッドをデリゲートで指定可能な以下のようなDelegateCommandというクラスを定義して使うのが一般的です。
using System; using System.Windows.Input; namespace UWPApp { public class DelegateCommand : ICommand { public event EventHandler CanExecuteChanged; private Func<bool> canExecute; private Action execute; public DelegateCommand(Action execute, Func<bool> canExecute = null) { this.execute = execute; this.canExecute = canExecute; } public bool CanExecute(object parameter) => this.canExecute?.Invoke() ?? true; public void Execute(object parameter) => this.execute(); public void RaiseCanExecuteChanged() => this.CanExecuteChanged?.Invoke(this, EventArgs.Empty); } }
Xamarin.Formsでは、何故かICommandの実装クラスであるXamarin.Forms.Command
クラスが提供されているので自分で実装する必要はありません。
Commandを使用したクラス例を見てみます。MainPageに対応するViewModelであるMainPageViewModelというクラスを以下のように定義します。
// Xamarin.Forms using System; using Xamarin.Forms; using XamarinApp.Models; namespace XamarinApp.ViewModels { public class MainPageViewModel : BindableBase { private string now; public string Now { get { return this.now; } set { this.SetProperty(ref this.now, value); this.UpdateNowCommand.ChangeCanExecute(); } } public Command UpdateNowCommand { get; } public MainPageViewModel() { this.UpdateNowCommand = new Command(() => { this.Now = DateTime.Now.ToString(); }, () => string.IsNullOrEmpty(this.Now)); } } }
// UWP using System; using UWPApp; using UWPApp.Models; namespace UWPApp.ViewModels { public class MainPageViewModel : BindableBase { private string now; public string Now { get { return this.now; } set { this.SetProperty(ref this.now, value); this.UpdateNowCommand.RaiseCanExecuteChanged(); } } public DelegateCommand UpdateNowCommand { get; } public MainPageViewModel() { this.UpdateNowCommand = new DelegateCommand(() => { this.Now = DateTime.Now.ToString(); }, () => string.IsNullOrEmpty(this.Now)); } } }
Commandは、Buttonクラスが持っているCommandプロパティとデータバインディングすることで、ユーザーの操作をCommandに渡すことができます。XAMLを書いてみましょう。
<!-- Xamarin.Forms --> <?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:ViewModels="clr-namespace:XamarinApp.ViewModels" x:Class="XamarinApp.Views.MainPage"> <ContentPage.BindingContext> <ViewModels:MainPageViewModel /> </ContentPage.BindingContext> <StackLayout> <Button Text="Update now" Command="{Binding UpdateNowCommand}" /> <Label Text="{Binding Now}" /> </StackLayout> </ContentPage>
実行すると、以下のようになります。まず、ボタンだけが表示されます。
ボタンを押すと、現在時刻が表示されボタンが押せなくなります。
これはCommandの第二引数でNowが空の時しか押せないという条件を指定しているためです。また、Commandの状態が変わったことを通知するためにNowプロパティのsetterでコマンドの変更イベントを発行しているためUIとの同期がとられています。
UWP側もXAMLを見てみましょう。大体同じです。
<Page x:Class="UWPApp.MainPage" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="using:UWPApp" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:ViewModels="using:UWPApp.ViewModels" mc:Ignorable="d"> <Page.DataContext> <ViewModels:MainPageViewModel /> </Page.DataContext> <StackPanel Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"> <Button Content="Update now" Command="{Binding UpdateNowCommand}" /> <TextBlock Text="{Binding Now}" /> </StackPanel> </Page>
実行結果も同じになります。
ModelとViewModel間
MVVMでVMとVの間の連携はわかったのでModelとViewModelはどうなの?という疑問を持つかもしれません。
これは通常のC#のプログラミングになります。メソッド呼び出し、メソッドの戻り値、イベントなどを適切に使って実装することになります。
ここで書いてないけど必ずぶつかるもの
- コレクションのバインドどうするの?→INotifyCollectionChangedの実装クラスであるObservableCollectionを使ってListViewなどのリスト表示系コントロールを使う
- ViewModelからViewに何か通知をしたいんだけど→メッセンジャーパターンというものがあります。
まとめ
だらだらと書きましたが、まとめとしては…。 何かフレームワークを使いましょう。個人的にはPrismがおすすめです。ここで実装したようなBindableBaseクラスやDelegateCommandクラスが提供されている他に、画面遷移、ダイアログの表示など実際のアプリを作るうえで便利な機能が提供されています。Xamarin.FormsのPrismについては書いてないですが、こちらのリポジトリにWPFとUWPのPrismについては書いていたりします。
Xamarin.Forms版のPrismについては以下のBlogが詳しいです。
また、まだ正式リリース前にPrismについて発表した資料とかがあります。