Xamarin.FormsとMVVMに触れてみたい話
はじめに
この記事は [初心者さん・学生さん大歓迎!] Xamarin その1 Advent Calendar 2017 - Qiita の四日目の記事です。
前回に引き続き Xamarin.Forms に触ってみたお話ですが、今回は MVVM にも入門してみた話を中心にまとめます。
MVVMについて
Xamarin について調べると必ずと言っていいほど登場する MVVM について。
詳しくは参照サイトを見ていただくとして超おおざっぱにまとめると、
プログラムを下記の3つに分けて設計しましょう、という話ですね。
- View: 画面に表示する部分を担当する。ボタンクリックなどのイベントを検知して ViewModel に伝える。
- ViewModel: 1.View と、3.Model とをつなぎ合わせる。通常 1.View と一対の関係となる。
Model: 計算処理などを行う。処理の内容によってクラス数は増減する。
1.View -> 2.ViewModel -> 3.Model の順に呼び出され、矢印の先にあるクラスのことを知ることはできるが、
逆はできない(例: 1.View は 2.ViewModel を知っていて呼び出すことができるが、逆は×)
なるほどね。完全に理解した(わかってない)。という感じなのですが、特に気になったことが2つありました。
Viewをコントロールするものは誰か
実はこの MVVM を Unity でも試してみたのですが、その時に迷ったのがコレ。
- View: uGUI の Canvas
- ViewModel: ViewModel の機能を担当するクラス
- Model: Model の機能を担当するクラス
とすると、誰が 1.View の表示・非表示を切り替えるの?また、ページを表示した直後の処理って誰がするの?という話です。
よくよく見てみると、 Xamarin.Forms で View を担当するのは、 (例えば) MainPage.xaml だけではなく、
MainPage.xaml.cs がいます。
Android でみると Activity ですね。
またページ遷移を担うのはまた別のクラス(例えば NavigationPage に関連するクラス)です。
これらをすべて View としてまとめてしまって良いのか疑問はありますが、
Canvas や Xaml の表示・非表示やページを開いた時の処理を実行するクラスは別にある、ということですね。
状態は誰が持つのか
表示だけを担う View はともかく、 ViewModel と Model の内、誰が状態を持つのか、
というのが2つ目の疑問でした。
結論としては、 View に関連する状態は ViewModel が、 Model の各処理に関連する状態はそれぞれのクラスが持つ、
ということのようです。
考えてみれば処理の中心となるクラスが情報を持ち、それ以外のクラスへは必要な情報だけを渡す、
というシンプルな考え方と言えそうです。
INotifyPropertyChangedによる通知
さて、 2.ViewModel は 1.View を知ることはできない、と書きましたが、
例えば クリックイベント発火 -> 処理 -> 完了後に表示を切り替え としたい場合は、
View に処理が完了したことを伝えたくなります。
View で Obervable を使うなどなど方法は色々ありますが、
INotifyPropertyChanged を使う方法も便利だと思いました。
呼ぶ側 (View)
private SubPageOneViewModel viewModel; public partial class SubPageOne : ContentPage { viewModel = new SubPageOneViewModel(); // ViewModelからの通知を購読. viewModel.PropertyChanged += (_, e) => { // 処理完了後に何かする. }; } publci void OnDoSomethingButtonClicked(object sender, EventArgs e) { viewModel.DoSomething(); }
呼ばれる側 (ViewModel)
private string calcResult; public string CalcResult { get => calcResult; set { // 値をセットするときに購読者に通知を送る. calcResult = value; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(CalcResult))); } } public void DoSomething(){ // 何か計算処理. // CalcResult の set が実行される. CalcResult = "計算結果"; }
- 上記では CalcResult に全く同じ値を代入した場合も通知が送られるため、
set で必要に応じて不要な通知が送られないようにします。 - ViewModel 側では プロパティではなく変数 calcResult に値を入れることもできますが、
その場合通知が送られないので注意が必要です。
DataBindingってみる
MVVM によるクラス同士の疎結合化を助けてくれる機能の一つに DataBinding があります。
xaml.cs クラスでボタンなどのインスタンスを持つことなく、
Model で変更された値をそのまま View (xaml) に反映したり、クリックなどのイベントを直接取得することができます。
SubPageOne.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="XamarinSample.View.SubPageOne"> < ContentPage.Content> < StackLayout BackgroundColor="#0078d7"> < Label Text="{Binding CalcResult}" /> < Button BackgroundColor="Yellow" Command="{Binding CalcCommand}">< /Button> < /StackLayout> < /ContentPage.Content> < /ContentPage>
- クリックイベントとして、 Clicked= に DataBinding を設定することはできず、
ICommand を使ってコマンドとして登録する必要があります(コマンドについては後述)。
SubPageOne.xaml.cs
using Xamarin.Forms; using XamarinSample.ViewModel; namespace XamarinSample.View { public partial class SubPageOne : ContentPage { public SubPageOne() { InitializeComponent(); // ViewModel クラスを DataBinding として設定. BindingContext = new SubPageOneViewModel(); } } }
と、ここまでは良かったのですが。。。
失敗
最初、 DataBinding する値を下記のように書いていました。
SubPageOneViewModel.cs
using System.Windows.Input; using Xamarin.Forms; namespace XamarinSample.ViewModel { public class SubPageOneViewModel { public ICommand CalcCommand { get; private set; } public string CalcResult{ get; set; } public SubPageOneViewModel() { CalcResult = "Starts"; // ボタンに Binding しているコマンド. CalcCommand = new Command(() => { var subtraction = DependencyService.Get(); CalcResult = subtraction.Calc(0, 1).ToString(); }); CalcResult = "Start2"; } } }
実行してみるとエラーは発生せず、ラベルには「Start2」と表示され、
ボタンを押すと CalcCommand の中の処理が実行されます。
しかし、なぜか CalcCommand の中にある「CalcResult = subtraction.Calc(0, 1).ToString();」がラベルに反映されないorz..
(なお「CalcResult = "Start2";」は実行されているのもよくわからず。。。)
Android などではイベントが別スレッドで発火するため、
下記のようにメインスレッドで実行してみては...?と思いましたが、うまくいかず。
// これでも反映されず. CalcCommand = new Command(() => { Device.BeginInvokeOnMainThread(() => { var subtraction = DependencyService.Get(); CalcResult = subtraction.Calc(0, 1).ToString(); }); });
実は、 DataBinding で変更された値を View に反映するためには、
INotifyPropertyChanged で変更を通知する必要があるのでした。
SubPageOneViewModel.cs
using System.ComponentModel; using System.Windows.Input; using Xamarin.Forms; namespace XamarinSample.ViewModel { public class SubPageOneViewModel : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; public ICommand CalcCommand { get; private set; } private string calcResult; public string CalcResult { get => calcResult; set { calcResult = value; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(CalcResult))); } } public SubPageOneViewModel() { CalcResult = "Starts"; CalcCommand = new Command(() => { var subtraction = DependencyService.Get(); CalcResult = subtraction.Calc(0, 1).ToString(); }); CalcResult = "Start2"; } } }
また、 PropertyChanged を実行するときの引数「PropertyChangedEventArgs(nameof(CalcResult))」は Binding しているプロパティ名を渡す必要があり、
例えばローカル変数である「PropertyChangedEventArgs(nameof(calcResult))」などでは反映されないようなので注意が必要です。
DataBinding を使うことで、 xaml.cs クラスからほとんど処理を省くことができ、
表示する部分と処理を実行する部分とがより簡単に分離できるようになりました。
おわりに
まだ慣れないせいか、 xaml や xaml.cs と実行される処理が(自分で書いたコード上では)切り離されているのは、
どこかふわふわして不安な感じもあります。
ただ、だからこそ仕様の変更に強いコードになる、ということだとは思うので、
ゆっくり内容を理解していきつつ、使いどころや効果的な使い方を模索していきたいと思います。
参照
Xamarin
MVVM
- THE MODEL-VIEW-VIEWMODEL (MVVM) DESIGN PATTERN FOR WPF
- MVVMのModelにまつわる誤解 - the sea of fertility
- 今さら入門するMVVMに必要な技術要素(Xamarin.Forms & UWP) - かずきのBlog@hatena