dnackのブログ

コード書く仕事の間にコード書いてる知命目前のおっさんです。

Prism/XamarinでAndroidアプリの作成(4):画面の遷移は絶えずしてしかも元の画面にあらず

今回は画面遷移をやってみよう。

ページの追加

ということで、遷移するからには遷移先の画面を追加しないといけない。これはウィザードで便利にできる。
まず、ソリューションエクスプローラからViewフォルダを選んで右クリックメニューの追加→新しい項目からPrism ContentPageを選ぶ。名前入力のテキストボックスがえらく下のほうに出てきて見落としそうになるが、わすれないで適当な名前を付ける。
(名前を付け忘れても後で変更はできるけど変更箇所が多くて面倒くさい。)
f:id:dnack:20210903003151p:plain

ちゃんとViewのフォルダを選んで追加すると一緒にViewModelのファイルもできて、
f:id:dnack:20210903003440p:plain
App.xaml.csのコンテナの登録も追加してくれる。
f:id:dnack:20210903003545p:plain

なにこれむっちゃ便利じゃん。(逆に言うと、ウィザード使わなかったり、名前入れ間違えたりするとこれ全部手でやんなきゃいけないわけで、さっき言った面倒くさいっていったのはこれね。)

画面遷移の実装

画面作っただけでは何も変わらないので、画面遷移の実装を書く。もともとのMainPageに遷移のためのボタンを追加。遷移先のページは、Titleだけ書いておけば遷移したことがわかるだろう。
ボタンの追加は、前回やったのと同じ。

MainPage.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="SamplePrismApp.Views.MainPage"
             Title="{Binding Title}">

    <StackLayout HorizontalOptions="CenterAndExpand" VerticalOptions="CenterAndExpand">
        <Label Text="Welcome to Xamarin Forms and Prism!" />
        <Label Text="{Binding TextMessage.Value} "/>
        <Button Text="ButtonA" Command="{Binding ButtonACommand}"/>
        <Button Text="ButtonNextPage" Command="{Binding ButtonNextPageCommand}"/>
    </StackLayout>

</ContentPage>

ButtonNextPageを追加している。ButtonNextPageのコマンドは例によってViewModelに追加する。

MainPageViewModel.cs

using Prism.Commands;
using Prism.Mvvm;
using Prism.Navigation;
using Reactive.Bindings;
using SamplePrismApp.Views;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace SamplePrismApp.ViewModels
{
    public class MainPageViewModel : ViewModelBase
    {
        public ReactiveProperty<string> TextMessage { get; set; }

        public DelegateCommand ButtonACommand { get; set; }
        public DelegateCommand ButtonNextPageCommand { get; set; }

        private INavigationService _navigateionService;

        public MainPageViewModel(INavigationService navigationService)
            : base(navigationService)
        {
            Title = "Main Page";
            TextMessage = new ReactiveProperty<string>( "0000" );
            ButtonACommand = new DelegateCommand( ButtonAClicked );
            ButtonNextPageCommand = new DelegateCommand( NavigateSecondPageAsync );
            _navigateionService = navigationService;
        }

        private void ButtonAClicked()
        {
            TextMessage.Value = "XXXX";
        }

        private async void NavigateSecondPageAsync()
        {
            _ = await _navigateionService.NavigateAsync(nameof(SecondPage));
        }
    }
}

量的には大した変更はない。
まず、ButtonNextP上げCommandを追加して、コンストラクタでNavigateSecondPageAsync()を登録している。
そのNavigateSecondPageAsync() は中身が次のページに遷移するための処理たった一行だが、それが今回のキモ。

NavigateAsyncというのが画面遷移のメソッド。引数はいくつかのオーバーロードがあるが、一番簡単なのはここに挙げた移動先ページの名前だけを書くもの。これで新たに追加したSecondPageへの遷移ができる。
以上終了!
としたいところだがもうちょっとだけ御託を並べさせてほしい。

NavigateAsyncの引数は”SecondPage"とリテラルで書く流儀も見かけるが、リテラルの書き間違いはコンパイルを通ってしまって見つけづらいバグになるので(それで泣いたことが何度かあるので)、ここではそれを避けてnameofを使っている。ただ、この記法のためにusing句でSamplePrismApp.Viewsを追加しなければいけないのはViewとViewModelの分離の観点からどうなのかという点はとても悩ましいところ。私はメリットのほうが大きいとみるのでこの書き方を採用している。

awaitは処理が終わるまでロックせずに待つための演算子。このawaitを含むためこのメソッドが非同期(async)になり、またこのメソッド名の末尾のAsyncは非同期のメソッド名の末尾にはこれをつけましょうという慣例によってついているもの。要するに非同期処理の仕組みなのだが、非同期処理は今回の処理の本筋ではないし、話し出すと深いし、その気になればブログやなんかで丁寧にわかりやすく説明しているところはいっぱい見つかるので、例によってここでは説明しない。

さて説明を最後にしてしまったがこの一行の本当のキモがここから。
_navigateionServiceとは何ぞや。たどってみると、コンストラクタの引数として渡されたものをprivateフィールドに保存したもので、出所はコンストラクタの引数、つまりオブジェクト生成時に渡されたものである。
このオブジェクト(MainPageViewModel)で使うであろうオブジェクト(navigationService)をオブジェクト生成時に外部から渡すというのは、Dependency Injection(依存性の注入/DI)といわれている構造によるもの。
MVVMはView/ViewModel/Modelの間を疎結合にするのが大事という話を前にしたが(してないかもしれない)、これも、モジュールの間を疎結合にするための仕組みの一つである。

以下蛇足

ちなみにDIもこれまた突き詰めればそれなりにややこしい概念ではあるのだが、理解をことさら難解にしているのは「依存性の注入」という日本語訳だと思う。
英語のWikipediaを見ると冒頭の説明に”dependency injection is a technique in which an object receives other objects that it depends on, called dependencies. "の記載。

en.wikipedia.org

dependencyというのは、”そのオブジェクトが依存する別のオブジェクト”の意味で、依存性と訳してしまったのはよくある技術用語の誤訳っぽい。依存性を注入するのではなく(そのオブジェクトが依存する、すなわちそのオブジェクトで使う)オブジェクトを注入するわけです。
オブジェクト作成時に、作ろうとしているオブジェクトが使うオブジェクトを渡しますよ という話なら、それ自体はそんなに難しい話ではないだろう。(それを実現するための仕組みだとか何のためにだとかメリットデメリットと言い出すとややこしいのだけれど)
ところが、日本語の”依存性”から”オブジェクト”という意味を想像するのは難しく、それが理解の妨げになっているのだろう。名前って大事だよね。
そういうこともあって、私は”依存性の注入”というタームは極力使わず、DIまたはDependency Injectionを使うようにしている。まあ、そういうタームが出てくる小難しい話はそもそもあんまりしないのだが(台無し)。

蛇足終了

話はそれたが、そういうことで、ViewModelに渡されたnavigationServiceにある、NavigateAsyncというメソッドで画面遷移を実現しているわけである。

とまあ、この1行でこれだけ話せてしまう程度には内容の濃い一行だったわけです。
使う分にはそんな小難しいこと全部無視してこの行コピペして遷移先のページだけ望みのページに書き換えればOKなのだけど(台無し)。

さて、お次に遷移先ページのxamlファイルですが、こっちは遷移したことが見た目で分かりやすいようにタイトルだけ書いています。最初のスケルトンアプリでは、MainPageのTitleが変数になってたけど、まあ、普通はここって変数にしないよね。

SecondPage.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="http://prismlibrary.com"
             prism:ViewModelLocator.AutowireViewModel="True"
             x:Class="SamplePrismApp.Views.SecondPage"
             Title="SecondPage">

</ContentPage>

ウィザードが書いてくれたコードに Title="SecondPage" だけを追加。
ViewModelは変更なし。

これで画面遷移の実装は完了。

実行結果は次の通り。

ちゃんとボタンが出てて、BUTTONNEXTPAGEをタップすると

ちゃんと表示されたタイトルがSecondPageになっている。

めでたしめでたし。