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

nuits.jp blog

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

Xamarin.FormsのPage遷移の詳細に迫る

なんかちょっと久しぶりに書いている気がします。
みなさん、いかがお過ごしでしょうか。私は諸々忙しくて辛いです。

今回はXamarin.Formsの画面遷移、正確には画面遷移スタックについて掘り下げてみたいと思います。
ここまで理解しておく必要があるケースというのは少ないでしょうけど、アプリケーションを設計する立場、アーキテクトであるという認識がある人は理解しておいてほしいなと思います。
なお、私はつい最近正しく理解しました。ハハハ。

さて、以下にXamarin.Formsの画面遷移スタックについての全てを図示しました。

f:id:nuitsjp:20170213114653p:plain

鋭い人はこれで理解出来ちゃったかもしれませんが、まぁそう言わず最後まで付き合って頂けると幸いです。

概略

さてXamarin.Formsの画面遷移はスタックにて管理されています。
この時、スタックは2種類存在します。

  1. ModalStack
  2. NavigationStack

気が付かれましたよね?そうです。PushModalAsyncを呼び出すとModalStackに、PushAsyncを呼び出すとNavigationStackにPageはスタックされます。
この時、上の図からもわかるとおり、二つの重要なポイントがあります。

  1. ModelStackはシステム全体で(論理的*1には)一つしか存在しない
  2. NavigationStackは、ModalStack上の画面ごとに存在する

そして細かい話ですが次の二つの事も分るでしょう。

  1. AppクラスのMainPageに設定した画面(つまり最初の画面遷移)は、ModalStack上に存在しない*2
  2. 実質、(現在のところ)NavigationStackはNavigationPageの為だけに存在し、それ以外の画面はNavigationStackの扱いはプラットフォームによって異なる*3

つまり驚きの事実ですが(そうでもないか?)、画面遷移Stackへの画面追加を画面遷移と定義すると(どうもそう扱っているフォーラムが多いようですが)TabbedPageでのタブの選択や、MasterDetailでDetailの画面変更は、画面遷移ではないという事です。びっくり!

これが理由でPrismのINavigationAwareのイベントが、TabbedPageなどでの画面切り替え時に発生しないわけです。

詳細

さて、ここからは実際に実装と、スタック内容の解析を行いつつもう少し詳しく見ていきましょう。
検証に利用したアプリケーションはこちらに公開していますので良かったらご覧ください。作りが惨いのは目をつむってくださいw

github.com

MainPageの表示

次のようなコードを実行しアプリを起動します。

    public partial class App : Application
    {
        public App()
        {
            InitializeComponent();

            MainPage = new XFNavigationStack.MainPage();
        }

画面にはMainPageが表示され、スタックと画面表示は次のような状態となります。

f:id:nuitsjp:20170213125220p:plain

次のようなコードでNavigationPageへ遷移します。

Navigation.PushModalAsync(new MyNavigationPage(new MyContentPage()));

するとスタックと画面表示は次のような状態となります。

f:id:nuitsjp:20170213131657p:plain

NavigationPageは表示されていないのか?という突っ込みはご遠慮いただきまして。。。
Androidエミュレーターの画面を見てみてください。
ModalStackにNavigationPageが、そしてNavigationStackにContentPageが積まれていることが見て取れるかと思います。

かみ砕くいていうと、NavigationPage内での画面遷移です。
先ほどの画面の「PushAsync」ボタンを押下し、以下のロジックを実行します。

await Navigation.PushAsync(new MyContentPage());

これを2回繰り返した後の状態が以下のとおりです。

f:id:nuitsjp:20170213132544p:plain

NavigationStack上にContentPageが3つスタックされていることが見て取れます。
Androidの画面上でもそのことが見て取れるでしょう。

再度NavigationPageへのモーダル遷移

さて、再びモーダル遷移です。
画面上で、「PushModalAsync」ボタンを1回、「PushAsync」ボタンを2回押します。
コードは既出のコードと同じなので省略しましょう。
遷移後の状態は以下のようになります。

f:id:nuitsjp:20170213133257p:plain

皆さんの想像通りだったでしょうか?
ちょっと面白いのはここからです。アプリ上で、ModalStackのItemは選択可能になっています。
MyNavigationPage1を選択してみます。

f:id:nuitsjp:20170213133642p:plain

Android画面のNavigation Stackの欄をご覧ください。 MyNavigationPage1~3が表示されます。
ちゃんとNavigationStackが維持されていることが見て取れるでしょう。

PopModalAsync

さてこの状態で、「PopModalAsync」ボタンを押下します。
コードは以下のとおりです。

Navigation.PopModalAsync();

結果は次の通り。

f:id:nuitsjp:20170213132544p:plain

モーダルスタックをポップしますので、まるっとNavigationPage1つ分、画面遷移的には3画面分戻ります。
もちろん、PopAsyncを利用すると、NavigationPage内のNavigationStackを一つづつ戻ります。

モーダルスタックをポップできるのは、例えば何らかの機能の実現に複数の画面遷移が含まれるような場合に、機能が完了したら初期画面へ戻りたい。
といったシーンで便利に利用できそうですね。

PopToRootAsync

さて、一応最後に触れておきましょう。
この状態から以下のコードを実行します。

Navigation.PopToRootAsync();

するとアプリケーションはMainPageまで戻ります。

f:id:nuitsjp:20170213125220p:plain

まとめ

さて改めてまとめてみましょう。

  1. Xamarin.Formsの画面遷移は次の2種類のスタックを用いて管理されている
    1. ModalStack
    2. NavigationStack
  2. ModalStackは論理的にシステム上一つのみ存在する
  3. NavigationStackはModalStackの画面ごとに独立して存在し、複数存在する事ができる
  4. NavigationStackの利用は、現時点ではNavigationPageに限られる
  5. 画面を戻る際、PopModalAsyncを利用すると、NavigationPageで複数遷移していても一気に前のModalStack上へ戻る
  6. PopToRootAsyncを利用すると、ModalStackをすべて破棄し、MainPageまで一気に戻る

こんなところでしょうか?
今回は結構、調べていて自分自身の理解にもつながり非常に満足しています。
皆さんのお役にも立つと良いのですが。。。

という訳で、今回はここまで。
それではまた!

*1:ちなみに上で論理的にはと言っているのは、もちろん実装上はそんな簡単になっていなくて、解読しようとすると泣きたくなるし、解説しようとすると眩暈がするため今回は割愛します。気になる人は、ApplicationクラスのMainPageプロパティや、LoadApplication周りの処理や、NavigationProxyとかPlatformのソースコードを追いかけて、私と一緒に泣きたくなりましょう。

*2:恐らくPopRootAsyncで先頭画面へ戻れるようにしたい等々、先頭画面を特殊に扱いたいからだと推測しています。

*3:UWPだとModalStack上のPageがNavigationStackに設定されているようですが、AndroidではNavigationStackは空でした。