読者です 読者をやめる 読者になる 読者になる

nuits.jp blog

C#, Xamarin, WPFを中心に書いています。Microsoft MVP for Visual Studio and Development Technologies。なお掲載内容は個人の見解であり、所属する企業を代表するものではありません。

【Xamarin.Forms】Prism & ReactivePropertyでMVVM開発 解説回

ここ何回か、なんちゃって書籍ビューアを題材にした、Xamarin.Formsアプリを紹介してきました。

github.com

今回は、そのコードの解説を試みたいと思います。
今回紹介するコードは、以下の要素が含まれています。

  • Xamarin.Forms
  • Prism for Xamarin.Forms
  • ReactiveProperty
  • MVVM Pattern
  • Moq

ところで皆さんは、MVVMパターンを取り扱った以下のエントリーを読まれたことはあるでしょうか?

ugaya40.hateblo.jp

こちらのエントリーの中で以下のようなに書かれています。

  • ViewModelはModelの影(そしてまたViewはViewModelの影)
  • ModelについてViewModelが行うことは、イベントに対する反応と戻り値のないメソッドの呼び出ししかない事

実はこの二つのことを、今回紹介するアプリケーションで説明することができると私は考えています。
というわけで、さっそく進めましょう。

アプリケーション概要

さて、今回開発したアプリケーションは、書籍をページングして表示する簡易書籍ビューアです。
具体的な動作イメージは以下の通りです。

f:id:nuitsjp:20160920140349g:plain

アプリケーションは本文を表示するテキスト画面と、章タイトルの一覧を表示する目次画面の二画面構成となっており、テキスト画面と目次画面は同期して表示されます。

テキスト画面でページを移動することで章を移動すると、目次画面での章表示も追随して更新されます。
逆に、目次画面で章を選択すると、選択章の先頭ページがテキスト画面に表示されます。

一般的な電子書籍ビューアと変わりありません。

ViewModelはModelの影である

さて、今回のアプリケーションで最も留意すべき点は、テキスト画面と目次画面の操作と表示が同期される点にあります。
ここで前述のMVVMの説明について、一つ思い出してください。

  • ViewModelはModelの影(そしてまたViewはViewModelの影)

である、という点です。
「ViewModelはModelの影であり、ViewはViewModelの影である。
したがってViewはModelの影である。」

そして、影はどの方向から光を当てるかで、姿を変えますよね?

「読書中の書籍というModelがあり、ある視点から見ると目次画面というViewとして表現され、別の視点から見ると本文のテキスト画面というViewとして表現される。」

f:id:nuitsjp:20160924141550p:plain

そう考えるのが自然で結果的にいろいろとシンプルにもなります。
そういった考えをベースに設計した結果、今回公開しているコードをクラス図に落としたもが以下になります。

f:id:nuitsjp:20160924141047p:plain

BookというModelクラスがあり、アプリケーション内ではBookインスタンスは一つだけ生成します。
インスタンスの管理はDIコンテナにお任せです。
BookViewer/App.xaml.cs at master · nuitsjp/BookViewer · GitHub

ContentsPageが目次画面、TextPageが本文画面で、それぞれ1:1に対応するViewModelが存在します。
そして、それぞれのViewModelはIBookをDIコンテナからインジェクションしてもらう事で、同一のインスタンスを参照するわけです。
BookViewer/ContentsPageViewModel.cs at master · nuitsjp/BookViewer · GitHub BookViewer/TextPageViewModel.cs at master · nuitsjp/BookViewer · GitHub

ViewModelがModelに対して行う事は戻り値のないメソッドを呼び出すのみ

さて次の点です。

  • ModelについてViewModelが行うことは、イベントに対する反応と戻り値のないメソッドの呼び出ししかない事

これはどういう事でしょうか?
今回のケースで考えてみましょう。

例えば、目次画面から特定の章が選択された場合、本文画面でも表示するページを切り替える必要があります。
逆も然りで、本文画面でページが進められて章を移動した場合も、目次画面の選択行を更新する必要があります。

Modelに状態を変更するメソッドがあったとしても、状態変更を戻り値で返すような設計であった場合、呼び出した側は変更を感知できますが、反対側のViewでは自分自身を適切な状態に保つことができません。

つまり以下のように、目次画面で特定の章が選択された場合、その結果はいずれの画面もModelから通知を受けて更新するように設計すべきなのです。
f:id:nuitsjp:20160924163925p:plain

本文画面でページをめくって章が移動した場合も同様です。
f:id:nuitsjp:20160924165153p:plain

もちろん、対象のモデルに依存している画面が1画面しかない場合も良くあることでしょう。
しかし、いつその仕様が変更になるかは、誰も保証できません。
つまり、Modelが画面から受ける影響を最小限に保ち続けるためには、ViewModelからModelへの呼び出しは戻り値を持たないメソッドの呼び出し(非同期処理のためのTaskなどは除きますが)のみに抑え、呼び出し結果はModelからの通知を受けるような設計が好ましいのです。

  • ModelについてViewModelが行うことは、イベントに対する反応と戻り値のないメソッドの呼び出ししかない事

ということです。

ReactivePropertyを使おう!

さて、設計上は前述の姿が好ましいことは理解していただけたのではないかと思います。
しかし、実際にはそう実装することはちょっと面倒です。
以下は、IBookのCurrentPageの変更を監視し、ViewModelの同名プロパティを更新するコードです。

        private IPage _currentPage;
        public IPage CurrentPage
        {
            get { return _currentPage; }
            set { SetProperty(ref _currentPage, value); }
        }
        public TextPageViewModel(IBook book)
        {
            _book = book;
            _book.PropertyChanged += (sender, args) =>
            {
                if (args.PropertyName == "CurrentPage")
                {
                    CurrentPage = _book.CurrentPage;
                }
            };
        }

これだけでも煩雑ですが、ObservableCollectionの変更も監視するとなるとさらに大変です。
そこで便利な神ライブラリがReactivePropertyです。
以下がReactivePropertyを適用したコードです。

        public ReadOnlyReactiveProperty<IPage> CurrentPage { get; }
        public TextPageViewModel(IBook book)
        {
            _book = book;
            CurrentPage = _book.ObserveProperty(b => b.CurrentPage).ToReadOnlyReactiveProperty();
        }

非常に簡潔に記述できます。
Xamarin.Formsに限らず、WPFやUWPのようなバインディングアーキテクチャを採用したUIフレームワーク全般で利用出来ますので、まだ利用されたことがない方は、これを期に是非利用してみてはいかがでしょうか?
ReactivePropertyについては、以下のブログをまずは目を通してみることをお勧めします。

blog.okazuki.jp

MoqでMockでUnitTest!

ところで、今回のアプリケーションのソースを公開したところ、以下のような質問を受けました。
「BookクラスがIBookインターフェースを実装しているのはなぜか?IBookはなぜ必要なのか?」

直接的な理由は、もちろんDIコンテナを利用して疎結合を保つことです。
DIについて、よく分からないという方は、良かったら以下のエントリーも読んでみてください。

www.nuits.jp

ただ、前述の回答は本質的な理由ではありません。
そもそもDIを利用して疎結合に保つ理由はなぜか?

「Bookクラスを利用するクラスを、BookクラスではなくMockを利用することで容易にテストできるようにすることで、メンテナンス性を保つ為」

です。
Bookクラスは今回は適当な作りですが、実際にはネットワークやストレージからロードされて利用されるケースが多いでしょう。
そうなると、Bookを直接利用していた場合、利用者側のテストは非常に困難になります。
特にXamarinの場合、動作プラットフォームの影響を受けますので、そもそもVSの単体テスト機能から直接実行すること自体が不可能に近いでしょう。

そこで、VMとModelをInterfaceとDIコンテナを利用することで疎結合を実現します。
その上でMockを利用してテストを実施することで、テスト容易性とメンテナンス性を同時に得ることができます。

今回のソースコードではMockの作成にMoq(もっきゅ?)を利用しています。
MoqはMockを作る際に、実装クラスを一々作成せずに、インターフェースから自動生成(というと語弊があって実際にはProxyを利用しているのですが。。。)してくれます。
以下のコードをご覧ください。

            var book = new Mock<IBook>();
            var vm = new MainPageViewModel(book.Object);
            
            Assert.IsTrue(vm.OpenCommand.CanExecute(null));

            vm.OpenCommand.Execute(null);
            book.Verify(b => b.Open(), Times.Once);

IBookをベースにMockを作成し、VMのOpenCommandが実行された場合に、ModelのOpenメソッドが一度呼び出されることを確認しています。
Moqについては、ここでは詳しく述べませんが、非常に便利ですので調べて利用されてみることをお勧めします。
ユニットテストを書くのであれば、類するものを利用しない手はありません。

最後に

というわけで、なんちゃって書籍ビューアのコードについて、ポイントをまとめてみました。
今回、MVVMについて大分首を突っ込んで書いているので、実のところマサカリが非常に怖いです。
おかげで書き上げるのに大分時間がかかりました。
けど、MVVM界の「名前を呼んではいけないあの人」は、今眠りについているそう(というか、本人がおっしゃってたw)なので、きっと命までは取られないことでしょうw

というわけで、今回はここまでです。
次回はまたPrismの解説に戻りたいと思います。
それではまた!