shuhelohelo’s blog

Xamarin.Forms多めです.

Xamarin.FormsのShellのRouteとページ遷移について

docs.microsoft.com

Xamarin.Forms ShellはTabページ, Master-Detailページに加えて,最近のアプリケーションでよく使われるドロワー(左からのスワイプで出てくるメニュー)といった基本的なナビゲーションを統合した仕組みです.

これまでのページ遷移の方法に加えてウェブアプリケーションでよくみられるURIベースの遷移の仕組みも提供しています.

これはURIベースのページ遷移についてのメモです.

Shellを使う方法はこちらを参考に.

shuhelohelo.hatenablog.com

Routeとは

  • Route
    • Shell内の階層構造に属するコンテンツへの道筋のこと.
  • Page
    • Shellの階層構造に属さず,Shellアプリケーションの任意の箇所においてNavigationスタックに追加されるもの.
    • 例えば「詳細ページ」は通常Shellの階層構造内には定義されないが,必要に応じてNavigationスタックに追加される.
  • クエリパラメータ
    • クエリパラメータは遷移先のページに渡される.

ナビゲーションURIの一例:

//route/page?queryParameters

Routeの登録

RouteはFlyoutItem,Tab,ShellContentRouteプロパティによって定義される.

<?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での遷移先指定の種類

docs.microsoft.com

github.com

  • //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としているのかというと,ContentPageIdというプロパティを持っていて,値の受け渡しに失敗するため)

公式のサンプルコードでは,受け取ったパラメータの値を使ってSetter内でデータソースを検索して対象のオブジェクトを取得し,それをBindingContextに入れている.

docs.microsoft.com

[QueryProperty("Name", "name")]
public partial class ElephantDetailPage : ContentPage
{
    public string Name
    {
        set
        {
            BindingContext = ElephantData.Elephants.FirstOrDefault(m => m.Name == Uri.UnescapeDataString(value));
        }
    }
    ...
}

なるほど.

クエリパラメータから受け取った日本語の表示

devblogs.microsoft.com

クエリパラメータの値として日本語の文字列を渡すとどうなるかというと,以下のように正しく表示されません.

f:id:shuhelohelo:20200601020929p:plain

たま → %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();
            }
        }

これで日本語が表示されました.

f:id:shuhelohelo:20200601021400p:plain

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("..")で一つ前のページに戻ることができる.

devblogs.microsoft.com

この場合もクエリーパラメータをつけることができる.

            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));
            }
        }
...