Xamarin.FormsのShellのRouteとページ遷移について
Xamarin.Forms ShellはTabページ, Master-Detailページに加えて,最近のアプリケーションでよく使われるドロワー(左からのスワイプで出てくるメニュー)といった基本的なナビゲーションを統合した仕組みです.
これまでのページ遷移の方法に加えてウェブアプリケーションでよくみられるURIベースの遷移の仕組みも提供しています.
これはURIベースのページ遷移についてのメモです.
Shellを使う方法はこちらを参考に.
Routeとは
- Route
- Shell内の階層構造に属するコンテンツへの道筋のこと.
- Page
- Shellの階層構造に属さず,Shellアプリケーションの任意の箇所においてNavigationスタックに追加されるもの.
- 例えば「詳細ページ」は通常Shellの階層構造内には定義されないが,必要に応じてNavigationスタックに追加される.
- クエリパラメータ
- クエリパラメータは遷移先のページに渡される.
ナビゲーションURIの一例:
//route/page?queryParameters
Routeの登録
RouteはFlyoutItem
,Tab
,ShellContent
のRoute
プロパティによって定義される.
<?xml version="1.0" encoding="utf-8" ?> <Shell x:Class="XFShellRouting.MainShell" xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns:d="http://xamarin.com/schemas/2014/forms/design" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d"> <FlyoutItem Route="animals"> <Tab Route="domestic"> <ShellContent Route="cats"/> <ShellContent Route="dogs"/> </Tab> <ShellContent Route="monkeys"/> <ShellContent Route="elephants"/> <ShellContent Route="bears"/> </FlyoutItem> <ShellContent Route="about"/> </Shell>
Shellの階層構造内のコンテンツはすべてRouteと対応している.
もし開発者によってRouteが設定されていない場合は,実行時に生成される.
しかし,生成されたRouteはすべてのアプリケーションのセッションに渡って一定であるとは限らない(←?)よくわかっていないけれど,間違いのないようにしたければRouteを明示しろということか.
上記のXamlでは以下のような階層構造になる
animals domestic cats dogs monkeys elephants bears about
Shell内に記述されているものがRoute.
catsまでのRouteは絶対URIで//animals/domestic/cats
となる.
つまりはShellContentまでがRouteということかな.アプリケーション全体のUI構成がここまでで定義される.
Page
Shell内には記述されず,ShellのRouteに続いて指定されるのがPageである.
ShellContentのページから先,例えばcatsページに表示されている猫のリストから1つ選択したときに表示される詳細ページや編集ページ,もしくは新規作成ページなどがこのPageにあたる.
URIでの遷移
仮にElephantDetailPage
を作成し,そこへの遷移を行う.
ShellContentのDogsPageにボタンを配置し,ElephantDetailPageへGoToAsyncで遷移する.
private async void Button_Clicked(object sender, EventArgs e) { await Shell.Current.GoToAsync("elephants/details"); }
URIで遷移するためには,このURIとElephantDetailPageを結びつけておく必要がある.
Routeの登録はRouting.RegisterRoute
メソッドで行う.
//ShellContentからのページ遷移 //詳細ページへのRouteの書き方は一般的にはこんな感じになる //どちらでもよい. //1つのページへの複数Routeを登録しても良い. Routing.RegisterRoute("elephants/details", typeof(ElephantDetailPage)); Routing.RegisterRoute("elephantDetails", typeof(ElephantDetailPage));
この登録はGoToAsyncで遷移が行われる前に実行されていなければならない.
Shellのコンストラクタ内でまとめて登録しておくとよい.
URIでの遷移先指定の種類
//root/route
- 絶対指定
route
- 相対指定
- 現在地から階層を上がりながら探す?
- ページがスタックに積まれる
/route
- rootから階層を下りながら探す?
- ページがスタックに積まれる
//route
- 現在表示中のrouteから階層を上がりながら探す?
- ページはスタックを置き換える(上書きする)?
///route
- 現在表示中のrouteから見つかるまで下っていく?
- rootから階層を下りながら探す.
- ページはスタックを置き換える?
値の受け渡し
ページ遷移は多くの場合,値の受け渡しを伴う.
例えばリストから選択されたアイテムの詳細情報を表示する場合,リストページ(CatsPage)から(CatDetails)ページへ選択された猫のIDにあたるものが渡される必要がある.
Xamarin.Formsにおける値の受け渡しは様々あるが,ShellではURIのリクエストパラメータとして文字列を渡す方法をとっている.
猫オブジェクト全体をまるっと渡せないのか,とも思うかもしれないが,このようなデータは何かしらの形式(DBやJSONや一時的なリストなど)で保持されているので,それらのデータソースから対象を特定できる情報(IDなど)を遷移先に渡せば十分のはず.
まずはCatDetailsPageへのRouteを登録しておく.
Routing.RegisterRoute("catDetails", typeof(CatDetailsPage));
クエリパラメータはページまでのURIの後に?
をつけ,その後にparameterName1=<value>
として渡したい値を名前=値
の形式でつなげる.
await Shell.Current.GoToAsync("catDetails?catId=10");
複数の値を渡す場合は,&
でつなげていく.
private async void Button_Clicked(object sender, EventArgs e) { int selectedCatId = 10; string selectedCatName = "たま"; await Shell.Current.GoToAsync($"catDetails?catId={selectedCatId}&name={selectedCatName}"); }
値を受け取る遷移先のページではコードビハインドで以下のように書く.
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using Xamarin.Forms; using Xamarin.Forms.Xaml; namespace XFShellRouting.Views { [XamlCompilation(XamlCompilationOptions.Compile)] [QueryProperty("CatId","catId")] [QueryProperty("Name","name")] public partial class CatDetailsPage : ContentPage { private string _catId; public string CatId { get => _catId; set { _catId = value; OnPropertyChanged(); } } private string _name; public string Name { get => _name; set { _name = value; OnPropertyChanged(); } } public CatDetailsPage() { InitializeComponent(); this.BindingContext = this; } } }
重要なところは以下の部分.
[QueryProperty("CatId","catId")] [QueryProperty("Name","name")]
ここで,CatDetailsPageのCatId
とクエリパラメータのcatId
を結びつけ,Name
プロパティとname
パラメータを結びつけている.
左がプロパティ名,右がクエリパラメータ名.
これで,クエリパラメータの値が遷移先のページのプロパティに渡される.
(なぜId
ではなくCatId
としているのかというと,ContentPage
がId
というプロパティを持っていて,値の受け渡しに失敗するため)
公式のサンプルコードでは,受け取ったパラメータの値を使ってSetter内でデータソースを検索して対象のオブジェクトを取得し,それをBindingContextに入れている.
[QueryProperty("Name", "name")] public partial class ElephantDetailPage : ContentPage { public string Name { set { BindingContext = ElephantData.Elephants.FirstOrDefault(m => m.Name == Uri.UnescapeDataString(value)); } } ... }
なるほど.
クエリパラメータから受け取った日本語の表示
クエリパラメータの値として日本語の文字列を渡すとどうなるかというと,以下のように正しく表示されません.
たま → %E3%81%9F%E3%81%BE
URLエンコードされた文字列が表示されます.
ですので,デコードする必要があります.
公式サンプルのほうでやっていますが,Uri.UnescapeDataString
メソッドを使ってデコードします.
Nameプロパティは以下のようになります.
private string _name; public string Name { get => _name; set { _name = Uri.UnescapeDataString(value ?? string.Empty);//URLデコードする OnPropertyChanged(); } }
これで日本語が表示されました.
ViewModelでクエリパラメータを受け取れる?
受け取れます.
コードビハインドのときと同じようにQueryProperty
属性を使って受け取る事ができます.
ViewModel側:
using MvvmHelpers; using System; using System.Collections.Generic; using System.Text; using Xamarin.Forms; namespace XFShellRouting.ViewModels { [QueryProperty("CatId", "catId")] [QueryProperty("Name", "name")] public class CatDetailPage2ViewModel : ObservableObject { private string _catId; public string CatId { get => _catId; set => SetProperty(ref _catId, value); } private string _name; public string Name { get => _name; set { SetProperty(ref _name, Uri.UnescapeDataString(value)); } } } }
View側:
using System.Text; using System.Threading.Tasks; using Xamarin.Forms; using Xamarin.Forms.Xaml; using XFShellRouting.ViewModels; namespace XFShellRouting.Views { [XamlCompilation(XamlCompilationOptions.Compile)] public partial class CatDetailPage2 : ContentPage { public CatDetailPage2() { InitializeComponent(); this.BindingContext = new CatDetailPage2ViewModel(); } } }
メモ
よくわかっていないけれどメモ
BindingContextに入れるのであれば,QueryProperty
経由でクエリーパラメータの値がプロパティに格納される.
BindingContextに入れると,クエリーパラメータの値がプロパティに格納される.
this.BindingContext = new CatDetailPage2ViewModel();
Back Navigationについて
戻る場合は以下のようにShell.Current.GoToAsync("..")
で一つ前のページに戻ることができる.
この場合もクエリーパラメータをつけることができる.
await Shell.Current.GoToAsync($"..?habitId=25");
戻る先のコードビハインドまたはBindingContextに入れたViewModel側で以下のようにしておけば値を受け取れる.
[QueryProperty(nameof(HabitId), "habitId")] public class HabitListViewModel : BaseViewModel { private string _habitId = string.Empty; public string HabitId { get => _habitId; set { SetProperty(ref _habitId, Uri.UnescapeDataString(value ?? string.Empty)); } } ...