shuhelohelo’s blog

C#、WPF、Xamarin.Formsが好きですが、ASP.NET Coreも好きです。

Xamarin.FormsでLottieを使ってアニメーションを表示する

www.youtube.com

LottieはAirBnBが開発した,アニメーションを表示するライブラリ. 表示させるアニメーションはこちらのサイトに公開されている.

lottiefiles.com

動画はiOSの例なので,Androidはどうしようかと探した.この記事はAndroidの情報です.参考にした下記の記事では,Android,iOSの両方の情報が書かれています.とても素晴らしい.

https://xamgirl.com/lottie-animations-step-by-step-in-xamarin-forms/

アニメーションをダウンロードする

アニメーションファイルはLottieFilesからダウンロードする.

ファイル形式はjson.

好みのものをダウンロードする.

lottiefiles.com

f:id:shuhelohelo:20200602224230p:plain

JSONファイルを各プロジェクトに配置する

以下はAndroidの例になるが,ファイルをAndroidプロジェクトのAssetsフォルダに入れる.ドラッグアンドドロップで良い.

ファイルのBuildActionをAndroid Assetに変更する.デフォルトでこれになっているはず.

f:id:shuhelohelo:20200228141453p:plain

Com.Airbnb.Xamarin.Forms.LottieをNugetでインストール.

現時点での最新バージョンは3.1.2だが,うまく動かすことができなかったので,今回はバージョン3.0.4をインストールした.

手順としては,MainActivity.csにAnimationViewRenderer.Init()を追加する,とのことだが,追加しなくても表示された.これはXamarin.Androidの場合に必要なのだろうか.わからない.

        protected override void OnCreate(Bundle savedInstanceState)
        {
            TabLayoutResource = Resource.Layout.Tabbar;
            ToolbarResource = Resource.Layout.Toolbar;

            base.OnCreate(savedInstanceState);

            Xamarin.Essentials.Platform.Init(this, savedInstanceState);
            global::Xamarin.Forms.Forms.Init(this, savedInstanceState);

            AnimationViewRenderer.Init();//これを追加

            LoadApplication(new App());
        }

ビュー(XAML)の編集

ContentPageに以下のようにLottieの名前空間を追加しておく.

<ContentPage
...
    xmlns:lottie="clr-namespace:Lottie.Forms;assembly=Lottie.Forms"
...>

アニメーションを表示させたい箇所に以下のようにAnimationViewを記述する.

            <lottie:AnimationView
                x:Name="AnimationView"
                Animation="5834-pouring.json"
                AutoPlay="True"
                HorizontalOptions="FillAndExpand"
                Loop="true"
                Speed="1.0"
                VerticalOptions="FillAndExpand" />

Animationプロパティにはアニメーションのファイル名を指定する.

再生速度や自動再生など様々な指定ができる.どんなプロパティがあるのかはソースコードを見たほうがわかりやすい.

github.com

ソースコード

github.com

Xamarin.Formsでスプラッシュスクリーン

スプラッシュスクリーンは起動時に表示する画面.

以下のアプリを参考にする.

github.com

このアプリでは以下の流れでスプラッシュスクリーンを使っていた.

  1. App.xaml.csでMainPage = new SplashScreen();
  2. SplashScreen.xamlは中央にpngを表示させるだけ
  3. コードビハインドではそのpngをアニメーション
  4. 同時にサンプルデータの読み込みなどを行わせる
  5. メインページをセット.
            MainThread.BeginInvokeOnMainThread(() =>
            {
                Application.Current.MainPage = new MainPage();
            });

なるほど.

ロゴのアニメーションの部分は以下のとおりなのだけれど,この書き方について調べたい.

        void StartPulseAnimation()
        {
            var animation = new Animation();

            animation.WithConcurrent((f) => logo.Scale = f, logo.Scale, logo.Scale, Easing.Linear, 0, 0.1);
            animation.WithConcurrent((f) => logo.Scale = f, logo.Scale, logo.Scale * 1.05, Easing.Linear, 0.1, 0.4);
            animation.WithConcurrent((f) => logo.Scale = f, logo.Scale * 1.05, logo.Scale, Easing.Linear, 0.4, 1);

            Device.BeginInvokeOnMainThread(() =>
            {
                logo.Animate("Pulse", animation, 16, 1000, repeat: () => true);
            });
        }

ちょっとやってみる

ContentPageでSplashPage用意する.

SplashPageで1.5秒間インジケータを表示させた後,メインのアプリケーションとなるShellを表示させる.

App.xaml.csファイルで,開始ページとしてSplashPageを指定する.

        public App()
        {
            InitializeComponent();

            MainPage = new SplashPage();
        }

SplashPage.xamlはActivityIndicatorを表示するだけ.

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage
    x:Class="XFShellLoginFlow.Views.SplashPage"
    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">
    <ContentPage.Content>
        <StackLayout>
            <ActivityIndicator
                HorizontalOptions="CenterAndExpand"
                IsRunning="True"
                IsVisible="True"
                VerticalOptions="CenterAndExpand"
                Color="Red" />
        </StackLayout>
    </ContentPage.Content>
</ContentPage>

Splash.xaml.csではOnAppearingをオーバーライドして,ページが表示されてから1.5秒間待った後にShellを表示する.

    public partial class SplashPage : ContentPage
    {
        public SplashPage()
        {
            InitializeComponent();
        }
        
        protected override async void OnAppearing()
        {
            base.OnAppearing();

            //Initialize something you want here
            await Task.Delay(1500);

            //Transition to FirstPage
            App.Current.MainPage = new AppShell();
        }
    }

SplashPageに表示する内容は自由にアレンジすればよい.

ロゴでも良いし,アニメーションでもよいし,挨拶文でもよいだろう.

Lottieでアニメーションなんていいかも.

Xamarin.Formsアプリのパフォーマンス向上について

docs.microsoft.com

UIについてだけでなく,様々な点についてパフォーマンス向上に関するTIPSが書かれている.

  • 子要素が1つだけなのにStackLayoutを使わない
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="DisplayImage.HomePage">
    <StackLayout>
        <Image Source="waterfront.jpg" />
    </StackLayout>
</ContentPage>
  • できるだけ階層は浅くする

同じ見た目を実現できるのであれば,できるだけ階層は浅くする.

階層が深いと計算の負荷も高くなる.

階層深い:

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="Details.HomePage"
             Padding="0,20,0,0">
    <StackLayout>
        <StackLayout Orientation="Horizontal">
            <Label Text="Name:" />
            <Entry Placeholder="Enter your name" />
        </StackLayout>
        <StackLayout Orientation="Horizontal">
            <Label Text="Age:" />
            <Entry Placeholder="Enter your age" />
        </StackLayout>
        <StackLayout Orientation="Horizontal">
            <Label Text="Occupation:" />
            <Entry Placeholder="Enter your occupation" />
        </StackLayout>
        <StackLayout Orientation="Horizontal">
            <Label Text="Address:" />
            <Entry Placeholder="Enter your address" />
        </StackLayout>
    </StackLayout>
</ContentPage>

階層浅い:

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="Details.HomePage"
             Padding="0,20,0,0">
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="100" />
            <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="30" />
            <RowDefinition Height="30" />
            <RowDefinition Height="30" />
            <RowDefinition Height="30" />
        </Grid.RowDefinitions>
        <Label Text="Name:" />
        <Entry Grid.Column="1" Placeholder="Enter your name" />
        <Label Grid.Row="1" Text="Age:" />
        <Entry Grid.Row="1" Grid.Column="1" Placeholder="Enter your age" />
        <Label Grid.Row="2" Text="Occupation:" />
        <Entry Grid.Row="2" Grid.Column="1" Placeholder="Enter your occupation" />
        <Label Grid.Row="3" Text="Address:" />
        <Entry Grid.Row="3" Grid.Column="1" Placeholder="Enter your address" />
    </Grid>
</ContentPage>
  • 階層は浅くする.
  • Gridのサイズ指定でAutoはできるだけ少なくする.
  • VerticalOptionsとHorizontalOptionsは必要なときだけ設定する.デフォルト値をわざわざ設定しなおすだけでも負荷になる.
  • RelativeLayoutはできるだけ使わない
  • AbsoluteLayoutを使うときはAbsoluteLayout.AutoSizeをできるだけ使わない
  • StackLayoutを使うとき,LayoutOptions.Expandsを指定する子要素は1つだけにする.
  • Layoutクラスのメソッドを使わないこと.
  • Labelを不必要に更新しないこと.Labelのサイズが変わったときにレイアウトが再計算されるため.
  • Label.VerticalTextAlignmentは必要なとき以外は設定しない.
  • 可能な限りLineBreakModeを使って,折返しが発生しないようにする.レイアウトの再計算が発生するため.
  • AsynchronousなAPIをできるだけ使うこと.UIスレッドがブロックされずにすむ
  • Asynchronousな処理によるUIの更新の際は例外の発生に気をつけること.
  • ただし,ListView.ItemsSourceプロパティの更新は自動的にUIスレッド上で行われるので大丈夫.
  • すべてのデータバインディングによる更新はUIスレッド上で行われるので大丈夫.
  • Shellを使え.
  • ShellはViewの生成をオンデマンドで行うこともできるので,起動時の負荷が小さい
  • ListViewの代わりにCollectionViewを使え
  • Visual Treeのサイズを小さくする
<StackLayout>
    <StackLayout Padding="20,20,0,0">
        <Label Text="Hello" />
    </StackLayout>
    <StackLayout Padding="20,20,0,0">
        <Label Text="Welcome to the App!" />
    </StackLayout>
    <StackLayout Padding="20,20,0,0">
        <Label Text="Downloading Data..." />
    </StackLayout>
</StackLayout>

<StackLayout Padding="20,35,20,20" Spacing="25">
  <Label Text="Hello" />
  <Label Text="Welcome to the App!" />
  <Label Text="Downloading Data..." />
</StackLayout>
  • ApplicationのResourceDictionaryのサイズを小さくする
  • 特定のページでしか使わない設定は,ApplicationではなくContentPage内のResourceDictionaryで設定すること.

Xamarin.FormsでGoogle認証を使う

https://www.xamboy.com/2019/11/19/social-media-authentication-google-login-in-xamarin-forms/

[追記] ライブラリ公式のREADMEの方がわかりやすいかも.

github.com

Plugin.GoogleClientをインストールする.

f:id:shuhelohelo:20200602131858p:plain

Firebaseでプロジェクトを作成する

Plugin.GoogleClientのドキュメントに従って作業する.

github.com

Firebase Consoleへ移動する.

console.firebase.google.com

プロジェクトを追加(もしくは作成)する.

f:id:shuhelohelo:20200602132423p:plain

プロジェクトに名前をつけて.

f:id:shuhelohelo:20200602132506p:plain

Google Analyticsは適当にやって,プロジェクト作成完了.

プロジェクトのページへ移動して,「Authentication」を選択する.

f:id:shuhelohelo:20200602132813p:plain

「ログイン方法を設定」ボタンをクリックする.

f:id:shuhelohelo:20200602132849p:plain

Google」を選択する.

「有効にする」をオンにする. f:id:shuhelohelo:20200602133011p:plain

プロジェクトの公開名などを適当につけて,最後に「保存」ボタンを押す.

Googleが有効になりました」

f:id:shuhelohelo:20200602133328p:plain

他にも色々なサービス(Microsoft,Twitter,Githubなどなど)で認証できる.

左上のハンバーガーメニューを開いて,「⚙」マークを押して「プロジェクトを設定」を選択する.

f:id:shuhelohelo:20200602133649p:plain

これからプロジェクトにアプリを追加するので,まずはAndroidを選択する.

f:id:shuhelohelo:20200602133739p:plain

Android パッケージ名」を入力する.

f:id:shuhelohelo:20200602134241p:plain

Androidパケージ名は,Visual StudioAndroidプロジェクトのプロパティを開き,「Android Manifest」タブ内の「Package name」の項目の値.

f:id:shuhelohelo:20200602133950p:plain

デバッグ用の署名証明書」には以下のコマンドを実行して表示されるSHA1の値を貼り付ける.

Windows:

keytool -list -v -keystore "C:\Users\[USERNAME]\AppData\Local\Xamarin\Mono for Android\debug.keystore" -alias androiddebugkey -storepass android -keypass android

「アプリを登録」ボタンを押す.

google-services.json」ファイルをダウンロードしておく.

f:id:shuhelohelo:20200602134339p:plain

ここまでで,Firebase側の作業は完了.

Androidプロジェクト側の作業

指示にあるように,「google-services.json」ファイルをAndroidプロジェクトに追加する.

パーミッションを追加する.

f:id:shuhelohelo:20200602135518p:plain

MainActivity.csのOnCreateメソッド内で,base.OnCreate(savedInstanceState);の下にGoogleClientManager.Initialize(this);を追加する.

            base.OnCreate(savedInstanceState);
            GoogleClientManager.Initialize(this);

同じくMainActivity.csにOnActivityResultをオーバーライドして以下のようにする.

        protected override void OnActivityResult(int requestCode, Result resultCode, Intent data)
        {
            base.OnActivityResult(requestCode, resultCode, data);
            GoogleClientManager.OnAuthCompleted(requestCode, resultCode, data);
        }

これでAndroid側は準備完了.

共有コード側

このGoogle認証機能を使うように,ボタンを配置する.

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage
    x:Class="XFGoogleAuth.MainPage"
    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">

    <StackLayout>
        <Button Clicked="Button_Clicked" Text="Login with Google" />
    </StackLayout>

</ContentPage>

コードビハインドのイベントハンドラで,ログインメソッドを呼び出す.これは以下の一行でよい.

var loginUser = await CrossGoogleClient.Current.LoginAsync();

このメソッドの呼び出しで,以下のようにGoogleアカウントを選択するダイアログが表示される.

f:id:shuhelohelo:20200602150143p:plain

後は認証後の処理などを付け足して,イベントハンドラ全体としては以下のようになる.

        private async void Button_Clicked(object sender, EventArgs e)
        {
            //ログイン成功した後はAccessTokenに値が入る.
            //それを使って初回のみログインを求めるか,毎回求めるかを選択できる
            //今回は毎回ログインを求めるようにしている
            if (!string.IsNullOrEmpty(CrossGoogleClient.Current.AccessToken))
            {
                //認証済みの場合の処理
                //例えば他のページに遷移するなど.
                //App.Current.MainPage=new MainPage();

                CrossGoogleClient.Current.Logout();
            }

            var loginUser = await CrossGoogleClient.Current.LoginAsync();

            //認証後の処理.
            //例えば,他のページに遷移するなど.
            App.Current.MainPage = new MainPage();
        }

他にもPlugin.FacebookClientがあるようだ.

とても便利なライブラリでした.

ソースコード

github.com

Xamarin.Forms: Azure CosmosDBを使う

docs.microsoft.com

チュートリアルのとおりに.

Cosmos DB側の作業

Azure PortalでCosmosDBのリソース作成とサンプルデータの追加を行う.

リソースを作る

f:id:shuhelohelo:20200601113427p:plain

「Azure Cosmos DB」を検索して作成する.

f:id:shuhelohelo:20200601113533p:plain

以下のように入力する.

  • Subscription
    • 利用しているAzureのアカウント名
  • Resource Group
    • リソースグループには適当に名前を
  • Account Name
    • データベースサーバーの名前になるので重要
    • この文字列がリソースのURIの一部に使われる.例:<Account Name>.documents.azure.com
    • 一意でなければならないので,使用可能かどうかがチェックされる
  • API
    • Core(SQL)を選択する
    • SQL構文でクエリを発行できる
  • Location
    • 適当に
  • Apply Free Tier Discount
    • 最も重要
    • 無料プランを使うかどうか
    • もちろん使うのでApplyを選択
  • ほかはデフォルトのままで.

f:id:shuhelohelo:20200601114245p:plain

一番下の「Review + Create」 ボタンを押す.

最後に「Create」ボタンを押す.これでリソースの作成が完了. f:id:shuhelohelo:20200601115319p:plain

コンテナの作成

Cosmos DBのリソースへ移動し,左側のブレードから「Data Explorer」を選択し,「New Container」を押す.

f:id:shuhelohelo:20200601120342p:plain

  • Database Id
    • データベースの名前を入力する
  • Throughput
    • 「400」と入力
  • Container Id
  • Partition Key

f:id:shuhelohelo:20200601121406p:plain

あとはそのままにして「OK」ボタンを押す.

Data Explorerには以下のようにDBが作成され,「Items」というコンテナが作成されていることがわかる.

f:id:shuhelohelo:20200601121659p:plain

サンプルデータを追加する

Data Explorer -> Items -> New Itemsと選択する.

f:id:shuhelohelo:20200601121959p:plain

以下のJSONデータを貼り付け,「Save」ボタンを押す.

{
    "id": "1",
    "category": "personal",
    "name": "groceries",
    "description": "Pick up apples and strawberries.",
    "isComplete": false
}

f:id:shuhelohelo:20200601122142p:plain

以下のようにデータが格納される.

f:id:shuhelohelo:20200601122403p:plain

もう一つ追加してみる.今度は日本語を使う.

{
    "id": "2",
    "category": "学習",
    "name": "CosmosDBのチュートリアルをやる",
    "description": "以下のチュートリアルをやる.\n https://docs.microsoft.com/en-us/azure/cosmos-db/create-sql-api-xamarin-dotnet",
    "isComplete": false
}

作成された.

f:id:shuhelohelo:20200601122730p:plain

データを検索する

「Edit Filter」ボタンを押す.

f:id:shuhelohelo:20200601122919p:plain

入力欄にSQLクエリを入力する.

例えば,id逆順で並べ替える場合は以下のようにする.

f:id:shuhelohelo:20200601123137p:plain

id2のアイテムを検索したければ以下のようにする.

f:id:shuhelohelo:20200601123319p:plain

Xamarin.Formsアプリケーション側

ソースコードの取得

以下のリポジトリからgit cloneで取得する

git clone https://github.com/Azure-Samples/azure-cosmos-db-sql-xamarin-getting-started.git

URI(エンドポイント:接続先)とAPI Keyを取得する

Azure Portalで作成したCosmosDBのリソースを開き,左側のブレードからKeysを選択する.

f:id:shuhelohelo:20200601164512p:plain

表示されたURIPrimary Keyをアプリケーション側で使用する.

アプリケーションにURIAPI Keyを設定する.

先程git cloneしてきたソースコードVisual Studioで開き,以下の場所にあるAPIKeys.csを開く.

f:id:shuhelohelo:20200601164710p:plain

    public class APIKeys
    {
        public APIKeys()
        {
        }

        public static readonly string CosmosEndpointUrl = "ここに<URI>を貼り付ける";

        public static readonly string CosmosAuthKey = "ここに<Primary Key>を貼り付ける";
    }

注意!

これはチュートリアルなので簡単にするためにコード内に直接URIAPI Keyを貼り付けているが,このようなことは実際のアプリケーションではしてはいけない. とちゃんと書いてある.

どのようにするかはこちら

DB名,コンテナ名の指定

Azure Cosmos DBにデータを書き込んだり,読み込んだりするためにはDB名とその中のSQLでいうところのテーブル名にあたるコンテナ名をアプリケーション側で指定する必要がある.

公式のサンプルプログラムだとCosmosDBService.csファイル内にある次の2行がそれ.公式チュートリアルのとおりに作業を進めていれば以下のようになっている.

        static readonly string databaseName = "Tasks";
        static readonly string collectionName = "Items";

私は今回このDB名をXFTodoWithCosmosDBとしたので,変更した.

アプリからのCosmosDB利用の初期化部分

CosmosDBServiceクラス内の以下の部分.

このようにAzureにCosmosDBのリソースを作成してエンドポイントとAPI Keyが取得できれば,後のDB,コンテナの作成はMicrosoft.Azure.Cosmos.Coreを使って行うことができる.

それぞれ「存在しない場合に作成する」というメソッドなので,これらはワンセットで実行しておくといい気がする.

                docClient = new DocumentClient(new Uri(APIKeys.CosmosEndpointUrl), APIKeys.CosmosAuthKey);

                // Create the database - this can also be done through the portal
                //Azureポータルで作成しなくても,ここで作成できる.
                //初回作成時にリソースが無いということで例外が発生する.
                //でも,データベースはしばらくすると作成され,次からは例外が発生しない.
                await docClient.CreateDatabaseIfNotExistsAsync(new Database { Id = databaseName });

                // Create the collection - make sure to specify the RUs - has pricing implications
                // This can also be done through the portal
                //ここもコレクション(コンテナ)がない場合は作成を試みるが,
                //初回作成時はDBと同じく例外が発生する.
                //これもしばらくするとコレクションが作成され,その後は例外が発生しない.
                await docClient.CreateDocumentCollectionIfNotExistsAsync(
                    UriFactory.CreateDatabaseUri(databaseName),
                    new DocumentCollection { Id = collectionName },
                    new RequestOptions { OfferThroughput = 400 }
                );

取得,追加,削除,更新

取得

        public async static Task<List<ToDoItem>> GetToDoItems()
        {
            var todos = new List<ToDoItem>();

            if (!await Initialize())
                return todos;

            var todoQuery = docClient.CreateDocumentQuery<ToDoItem>(
                UriFactory.CreateDocumentCollectionUri(databaseName, collectionName),
                new FeedOptions { MaxItemCount = -1, EnableCrossPartitionQuery = true })
                .Where(todo => todo.Completed == false)
                .AsDocumentQuery();

            while (todoQuery.HasMoreResults)
            {
                var queryResults = await todoQuery.ExecuteNextAsync<ToDoItem>();

                todos.AddRange(queryResults);
            }

            return todos;
        }

追加

public async static Task InsertToDoItem(ToDoItem item)
{
    if (!await Initialize())
        return;

    await docClient.CreateDocumentAsync(
        UriFactory.CreateDocumentCollectionUri(databaseName, collectionName),
        item);
}

更新

        public async static Task UpdateToDoItem(ToDoItem item)
        {
            if (!await Initialize())
                return;

            var docUri = UriFactory.CreateDocumentUri(databaseName, collectionName, item.Id);
            await docClient.ReplaceDocumentAsync(docUri, item);
        }

削除

        public async static Task DeleteToDoItem(ToDoItem item)
        {
            if (!await Initialize())
                return;

            var docUri = UriFactory.CreateDocumentUri(databaseName, collectionName, item.Id);
            await docClient.DeleteDocumentAsync(docUri);
        }

実行

以下のようにToDoタブに上で追加した2つのアイテムが表示される.

f:id:shuhelohelo:20200602010227p:plain

追加やタスクの完了といった操作に応じて,CosmosDB側のデータが更新されることを確認する.

タスクを追加する.

f:id:shuhelohelo:20200602010640p:plain

アプリのToDoにアイテムが追加される.

f:id:shuhelohelo:20200602010735p:plain

Azureで確認すると,アイテムが追加されている.

f:id:shuhelohelo:20200602010828p:plain

メモ

CosmosDBを扱うライブラリはMicrosoft.Azure.Cosmos.CoreMicrosoft.Azure.Cosmosのどちらを使うべきなんだろうか.

docs.microsoft.com

docs.microsoft.com

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

なるほど.

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

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

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);//URLデコードする
                OnPropertyChanged();
            }
        }

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

f:id:shuhelohelo:20200601021400p:plain

Xamarin.FormsのShellの遷移の種類(Modalかどうか)は遷移先の各ページで指定する

Shellは画面遷移の手段としてShell.Current.Navigation.PushAsyncなどのこれまでのNavigationno方法もShell.Current.GoToAsyncも使えます.

Navigation.PushAsync系の遷移方法にはModalなページとして開くPushModalAsyncもあります.

しかし,Shell.Current.GoToAsync系にはModalで開くメソッドはありません.ではこちらの方法でModalで開くにはどうするかというと,遷移先のページで指定します.

<Content>タグの開始タグに以下の1行を追加します.

    Shell.PresentationMode="ModalAnimated"

このような感じです.

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage
    x:Class="XFShellRouting.Views.ElephantDetailPage"
    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"

    Shell.PresentationMode="ModalAnimated"

    mc:Ignorable="d">
    <ContentPage.Content>
        <StackLayout>
            <Label
                HorizontalOptions="CenterAndExpand"
                Text="Elephant Detail Page"
                VerticalOptions="CenterAndExpand" />
        </StackLayout>
    </ContentPage.Content>
</ContentPage>

これで,このページはModalで開かれます.

1つのページをModalで開いたり,そうじゃなかったりということはまあ無い気がしますので,ページそれ自体で定義するのはわかりやすいなと思いました.