WPFを中心に書いています。Technologies。(その1)5日目のエントリーです。これまで何度かPrismを利用する場合の画面遷移について記述してきました。blogこれらを踏まえていろいろ考え悩んだ結果、たどり着いたベストプラクティスを紹介したいと思います。Xamarin.Formsの拡張ライブラリを作ってNuGetに公開してみました。www.nuget.orgまだPreview版で破壊的変更を想定していますが、画面遷移フレームワークの一つのヒントにはなると思いますので、ご紹介したいと思います。言う訳で、本エントリーの主な内容は以下の通りです。Xamarin.Formsで画面遷移する際のベストプラクティスと、その解説ベストプラクティスの具体的な実装例の紹介なお、目次がちょっと引くレベルで長いです。10のベストプラクティス」の章だけであれば一目で読み終えられるので、こちらはぜひ目を通して頂けたらと思います。それで興味を持っていただけたら、実装例や詳細解説をご覧ください。10のベストプラクティスさて、結論から行きましょう。私が考えるベストプラクティスは以下の通りです。個々の詳細な解説は後半に記載します。本章ではベストプラクティスを具体的な実装を紹介しながら説明していきましょう。フレームワーク(Prism.Forms.Toolkit)とサンプルのソースは、いずれも以下のリポジトリにありますので、参考にしてください。github.comサンプルの前提条件について本サンプルはMVVMパターンを適用して実装しています。MVVMパターンについてあまり良く分からないという方は、以下をご一読されることを強くお勧めします。blogまた本サンプルのModel部分は以下のサイトの考え方に近いと思っています。fertilityサンプルの設計に対して、もう少しサンプルを拡充してから詳細に述べたいと思いますが、現時点で疑問に思った場合は、こちらをご一読いただけると解決することもあるかもしれません。Xamarin.Formsの画面遷移に関する基礎知識がある方を対象としています。知らん!て人はまずはこちらをお読みください。Xamarin.Forms以外のサードパーティライブラリは意図的に使用せず作成しています。この為、特にView-ViewModelの設計実装が最適解にはなっていませんがご了承ください。今回紹介するサンプルアプリケーションは、「EmployeeManager」というアプリになります。従業員管理アプリとなっていますが、現時点ではそこまでの機能はありません。扱っているデータはEmployee(従業員)と、それの所属するSection(部署)です。アプリの画面イメージは以下の通りです。部署詳細画面では部署の名称と、所属する従業員のリストが表示されています。メニューからは直接DeepLinkを使って部署詳細画面への遷移も可能としています。以下がその動作イメージです。Packを使って作成したプロジェクトを作成しています。その上で、Prism.Forms.ToolkitをNuGetから取得して利用しています。Prism.Forms.ToolkitはPCLプロジェクトのみにインストールすれば十分です。6.2.0にのみ対応しています。6.3.0-pre1などへの対応は今後予定しています。コード解説usingについてPrism.Forms.Toolkitは基本的に拡張メソッドで機能が提供されています。その為、利用個所では必要に応じて以下のusingを追加してください。Prism.Forms.Toolkit;DIコンテナへViewを登録するさて、ViewModelで画面遷移する場合、ViewをDIコンテナに登録する際に、その旨伝えて登録する必要があります。具体的には以下のように実施します。SectionPageViewModel>();}NavigationPageは該当するViewModelを作成する必要が無いので通常の方式で登録しています。RegisterTypeForViewModelNavigationメソッドでViewとViewModelを型パラメーターで指定している箇所が、今回のケースになります。前述のメニュー画面から部署一覧画面への遷移は以下のように実装しています。画面遷移をCommandとして実装しており、ViewのCommandに該当のCommandをバインドしています。NavigationSectionListCommand}"/>シンプルで明快ですね。部署詳細画面へ遷移する際、部署IDをパラメーターで渡しています。_navigationService.NavigateAsync<SectionPageViewModel>(navigationParameter);}パラメーターのキーの指定に注目してください。キーは遷移先のViewModel側の定数として定義しています。画面遷移する以上、遷移先の存在と、遷移時に必要な情報は遷移元でも認知している必要があるこの為、遷移先のViewModelを参照するデメリットは少ない(物理的な参照が無くても、それを表す論理情報は知る必要があり、物理と論理を切り離すメリットが薄い)したがって、パラメーターキーの定義も遷移先のViewModelで定義するのがシンプルで良いといった理由に基づいています。それ以外については、通常のPrismの画面遷移と大きな違いはありません。ViewModel指定と文字列指定の混在によるメニュー画面への遷移NavigationPage内にMainPageを初期画面として表示する場合、素のPrismでは以下のように指定します。NavigationService.NavigateAsync("NavigationPage/MainPage");型パラメーターでViewModelを指定する方式では、遷移先を任意の数、指定することはできません。そこでPrism.Forms.Toolkitでは、画面遷移をNavigationオブジェクトとして扱い、遷移時に複数指定できる機能を提供しています。具体的な利用コードは以下の通りです。Navigation<MainPageViewModel>());NavigationPageには該当するViewModelが無いので、Viewの名称で遷移先を指定しています。MainPageへの遷移はViewModelの型パラメータで指定しています。Prism.Forms.Toolkitではベストプラクティスで記載した通り、内部的にはINavigationServiceをそのまま利用しています。前述のコードは内部的に次のように処理されています。animated);}Navigationオブジェクトで遷移指定されても、それを同等の意味を持つ文字列に変更した上で、標準提供のNavigateAsyncを呼び直しています。トリックとしては単純ですね。NavigationクラスのToStringメソッドが肝ですがその点に興味がある方はソースを直接ご覧ください。ちなみに一点、悩んでいるところがあります。相対パスによるDeepLinkを実行すると謎の挙動を示します。́・ω・`)現在Prism.Forms.Toolkitでは、複数のNavigationが指定された場合、強制的に絶対パス指定でDeepLinkするようにしています。これは私が何か勘違いしているのか、PrismかXamarin.Formsに何らかの不都合があるのか、現時点では調べ切れていません。この為、将来的に上の実装は変更する可能性があります。遷移パラメーターを伴うViewModelベースのDeepLinkさて、上記の例ではパラメーターが含まれていませんが、実際のケースでは当然パラメーターが必要となることがあるでしょう。ViewModel」ボタンをクリックした際に実行されるCommandは、以下のように実装しています。sectionPageNavigation);前述の通り、DeepLinkは絶対パスでしか正しく動作させられていない為、NavigationPageから遷移を指定しています。Literal」ボタンを押下した場合、次のようなパスで画面遷移を実施しています。"/NavigationPage/MainPage/SectionListPage/SelectedSection?sectionId=5"));これは前述の「遷移パラメーターを伴う、ViewModelベースのDeepLink」と完全に同一の意味を持っています。見てもらいたいのは最後の遷移名が「SelectedSection」になっている点です。該当のViewModelはSectionPageViewModelであり、名前が一致していません。ViewModelの実装を見てみましょう。INavigationAwareViewModelにNavigationName属性を指定することで任意の遷移名を指定できるようにしています。これはDeepLinkのURLが外部に発行された後、ViewModelに対する仕様変更が発生した場合に、リンク名を維持したい場合などを想定しています。そんな事が本当に必要なのかちょっと疑問がないでもないですが、一つの解法として検討しておく価値はあると考えています。Prism.Forms.Toolkitの設計概要フレームワークはおおよそ以下のような設計になっています。Navigation画面遷移を表すクラス。遷移先名と遷移パラメーターを保持している。遷移パラメーターはPrismのNavigationParameterを直接保持して利用している。Navigation<TViewModel>Navigationのサブクラス。ViewModelベースの画面遷移時に利用する。NavigationNameProviderViewModelに対応する遷移名を提供するクラス。本クラスに命名規則を決定するResolverを独自に設定することでViewModelに割り当てる遷移名ルールを変更することができる。UnityExtensionsProxyPrismの提供するUnityExtensionsをラップするプロキシークラス。UnityExtensionsは拡張メソッドの集合なため、UnityContainerExtensionsから直接UnityExtensionsを利用してしまうとテスト容易性が激しく低下する。このため、UnityContainerExtensionsからはUnityExtensionsを単純ラップする本クラスを利用して、UnityContainerExtensions側のテスト容易性を確保する。本クラス側は単純ラップしているだけの本クラスの自動テストは行わないものとしている。詳細解説さてここからは、ベストプラクティスを一つずつ詳細に解説していきたいと思います。言え、本エントリーだけでは解説しきれていないものもありますが、それはおいおい追加エントリーを書きたいと思います。本章は、人によっては助長だったりすることでしょうから、必要そうな部分を拾い読む事をお勧めします。すべて読んでいただけると泣いて喜びます。画面遷移処理はViewModelに実装するPrismを利用した以上、当たり前のことでもありますが念のため。これにはいくつかの理由がありますが、以下が主な理由です。Viewに画面遷移ロジックを含めるとテスト容易性が低下する画面構成・遷移はPresentation(View+ViewModel)レイヤーの関心ごとであり、Modelからは分離するべきである後者はそもそもModelの定義の通りですが、もう少し分かりやすく補足しましょう。iPhoneとiPadで同一機能を持ったアプリを提供するとした場合、Modelは共有できるケースが高いでしょう。少なくともベストなPresentationは異なるケースが多いでしょう。Presentationが異なるのですから、Modelから共通のロジックで制御するのは困難です。これらを総合すると、画面遷移指示はViewModelから行わせるのがベストとなります。画面遷移の指定に文字列を使用しない最も良くないのは、以下のようなパターンです。navigationService.NavigateAsync("DetailPage");文字列で指定している上に、画面遷移の箇所ごとにマジックストリングとして埋め込まれています。遷移(画面)名やパラメーターが変更になる都度、関連箇所を全て適切に改修する必要があり問題があります。定数を定義すれば良いかと言うと、どこに定義すべきか?と言う問題が発生します。遷移先の論理的な名称(文字列)と、物理的な実装(View)は1:1で安全に保たれている必要があります。MVVMの場合、View(遷移の対象)には大抵、対となるViewModelがあり、そこに定義するのが第一候補でしょう。この場合、ViewModelに対して依存関係が発生します。あれば、次のアプローチの方が(文字数が増えることを除き)あらゆる点でスマートです。First的な画面遷移アプローチを採用する詳細はこちらの過去記事も参考にしてください。要は、遷移先を指定するにあたり、文字列ではなくViewModelそのものを利用しようというアプローチです。navigationService.NavigateAsync<DetailPageViewModel>();ViewとそのViewのRootレベルのViewModelが1:1である限り、非常にシンプルです。遷移元のViewModelから遷移先のViewModelへ依存関係が発生してしまいますが「どこに遷移するか?」という論理的な遷移名は知っている必要がありそれは≒ViewModelを知っていることになる定義先がViewModelになるのであればどうせ依存関係は発生すると言うことで、問題とすべき話ではないでしょう。ViewModelによる画面遷移の実装は、実のところPreview段階のPrism本体には存在したのですが、正式リリース前に削除された経緯があります。it?これは主に4つの理由がありました。Stackの復元がサポートされない上記が問題にならない場合、簡単な拡張メソッドを用意すればPrism側でサポートしなくてもアプリケーション側で対処できる(上のリンク先参照)さて、上の3つが致命的に思えるかも知れませんが、これらは次の対策を行うことでまとめて解消することが可能です。ViewModel指定と文字列指定の双方に対応できるフレームワークを用意するアプリケーションからの画面遷移呼び出しは以下で実装します。navigationService.NavigateAsync<DetailPageViewModel>();フレームワークは上記の呼び出しに対して、Prismを以下の形で呼び出します。Stackの復旧時などに、結果的にViewModelで指定された場合と、同じ振る舞いを取る事になります。参照がないModuleへの遷移時には、アプリケーションからも文字列による呼び出しで妥協する必要があります。ViewModelの存在しないViewもあります。代表的なのはトップレベルのNavigationPageでしょう。この場合も、無理にViewModelを作っても良いですが、文字列方式で対応してもまぁ許される妥協の範囲かな?とも思ったのですが、「NavigationPageの文字列はどこに定義すべきか?」という問題がまた発生してしまいます。この為、仮のViewModelを割り当てた方がやはり良いのかもしれません。遷移名はViewModelそのままではなく、変更可能な仕組みを設けるBrian氏によって提供されているこちらの方法では、遷移名にViewModelクラスのFullNameが使用されています。安全性が高いのですが、如何せん長すぎます。DeepLinkを考慮すると厳しいです。言うわけで、ViewModelを指定した場合にPrismへ伝達する遷移先の文字列は、以下の2種類の変更可能な手段が提供されていることが好ましいでしょう。FirstによるDeepLinkに対応する相対パスによるDeepLinkがサポートされていない場合、利用ケースは限定的です。アプリケーション起動時にNavigationPage内のContentPageへDeepLinkするパターン等は、ごく一般的にありえます。ポイントとなるのは、以下の様にNavigationAsyncの型パラメーターで指定した場合、当然DeepLinkに対応することができないと言う点です。navigationService.NavigateAsync<DetailPageViewModel>();またその際に、各遷移ごとにパラメーターを指定できる仕組みも、検討しておいた方が好ましいでしょう。*3具体的な方法は、前述の実装例の中で紹介させていただいていますので、そちらをご覧ください。画面遷移フレームワークとしてはINavigationServiceをそのまま利用するもちろん、アプリケーション個別でNavigationServiceを改変したり拡張することは構いません。画面遷移フレームワークとしては標準のNavigationServiceに手を入れることを前提とせず、INavigationServiceをそのまま利用できるよう設計するべきでしょう。これは、Prismのバージョンアップへの追随や、画面遷移フレームワークの再利用を考慮した場合、絶対に必要なことです。画面遷移時のパラメーターは、Model層のコラボレーションと合わせて最低限にとどめるさて、画面遷移するたびに、次の画面が必要な情報を全てパラメーターとして渡す必要があるかと言うと、決してそんなことはありません。画面遷移間のパラメーター数が多すぎると、変更容易性や開発効率が低下します。そこで、Modelとのコラボレーションと併用して最低限に収めるべきです。具体的には以下の二つのパターンが、利便性が高いでしょう。遷移前にModelに状態を保持しておいて、遷移後に利用する遷移時にはIDのみ渡し、遷移後にその他の値を再取得する(一覧から詳細画面など)もともとModelとは、アプリケーションの状態そのものを表すクラス群です。この為、同一のModelを共有するView間であれば、Modelで保持しておけば良いケースも存在します。アプリケーション全体の状態管理するModelを作れば、そもそもパラメーター渡しなんて要らないんじゃないの?と言う疑問がわくかも知れませんが、さすがにそれはModelが大きくなりすぎます。言うガイドラインでパラメーター渡しと、Modelでの保持を使い分ければ良いのか?私個人の考えとしては、多くの場合、ViewModelから直接利用するModelはUMLで言う「ユースケース」単位で作成し一覧から詳細や、参照から編集などの場合はID渡しユースケースをまたがる画面遷移ではパラメーター渡しユースケース内の画面遷移はModel渡しにしたら良いと思っています。例外は多数ありますがガイドラインとしては利用できるでしょう。ここでいうユースケースとは「ユーザーにとって価値(本質的な目的の達成)のある一連のアクション」を言います。この辺りの詳しい話は、話出すと長くなるので別のエントリーをそのうち書きたいと思います。*4画面遷移時のパラメーターキーは遷移先のViewModelで定義するPrismでは画面間のパラメーター渡しの際に、パラメーターのキーを文字列で指定する必要があります。それらのキー文字列は遷移先のViewModelに定義すると良いでしょう。必要なパラメーターを規定するのは遷移先自体である為です。画面遷移時のパラメーター値の型安全性は諦めるさて、PrismではパラメーターをDictionaryに詰め込んで受け渡すことが可能で、その際に値は文字列のみではなくオブジェクトを渡すことも可能です。その際、受け取り側は、objectからダウンキャストする必要がありますが。ここに型安全性を求めるのはちょっと難しいです。この辺はまた今度別のエントリーにしたいと思います。0:00に間に合いました。ここまでお付き合いいただけた方は、本当にありがとうございます!あとできれば本エントリーについて、どこかのセッションで話したいなと思っています。どこかいい場があったら、ご紹介くださるとうれしいです。いう訳で、今回はここまで。それではまた!*1:ちなみに、PrismではViewが確定すればViewModelは決定されます。特殊なViewModelのマッピングを行っていない今回の例では、本来ViewModelを指定する必要は無いはずです。Prism内部のViewとViewModelのマッピング情報を、外部から簡単に取得する方法がないため現時点では今回の例の通りになっています。将来的にはViewModelの指定はなしでできるように対応したいのですが、やるかどうかは少し悩んでいます。*2:これ実のところ、次の自分自身の疑問が解消されていません。NavigationPageの名称をどこで定義すべきか決めかねている。Firstに則るのであれば、NavigationPageにも仮のViewModelを定義すべきで混在は不要なのではないか?という根本的な話です。だれかこの辺の意見ある方は、ぜひ聞かせてほしいです。*3:しかしこれは、具体的な利用ケースはごく限られている可能性が高いです。*4:MVVMの設計と密接に絡むのでマサカリがめっちゃ怖いんですけどね。(1)はてなブログをはじめよう!nuitsjpさんは、はてなブログを使っています。ブログを報告するスターをつけました引用をストックしましたストック一覧を見る閉じる引用するにはまずログインしてくださいログイン閉じる引用をストックできませんでした。再度お試しください閉じる限定公開記事のため引用できません。