A certain engineer "COMPLEX"

Xamarinメモ その17 TabbedPageとNavigationPageの組み合わせ

前回はAndroidのUnzipped Failedを解決しました。

Problem


以前、TabbedPageについて記事を書きましたが、もう一つ重要なNavigationPageを組み合わせて見ます。
ただ、NavigationPageとTabbedPageを組み合わす際、NavigationPageの子要素として、TabbedPageを加えるのはよくないとのこと。

三日月 ふゆの氏の記事。

Xamarin.Forms(XAML) を使って、 NavigationPage + TabbedPage を作ってみます。 Twitter for iPhone や TweetBot みたいな感じの UI ですね。 ただ、 NavigationPage |...

元ネタ。

これを踏まえ、TabbedPageの下に、NavigationPage、NavigationPageの下に2つのContentPageというシンプルな実装を試しました。
ContentPageの最初でNextボタンがあり、それを押下すると、次のページにNavigationされる、というものです。
また、それぞれのPageにViewModelが紐付きます。つまり、4つのViewと4つのViewModelです。
そこまで大それたものではないはずです。

その実現のために、Webの各所を見ていきました。
下記は主に参考にさせていただいた記事です。

かずき氏の記事。

Prism.Formsは、地味にネストした画面遷移みたいなのをサポートしています。 画面遷移のINavigationService#Navigateメソッドに渡すURLに"/HogePage/FugaPage/BarP...

Prismの開発者の一人Brian Lagunas氏の記事。

Learn what's new in Prism for Xamarin.Forms 6.2.0 Preview. Check out the all new navigation service features such as URI support and deep linking.

で、いろいろ試行錯誤しましたが、実現できませんでした。
ページが遷移しない、クラッシュする、タブが出ないetcです。

注意

逃げ口上ではないですが、注意事項です。

      Prism.Forms 6.1.0-pre4
      Prism.Unity.Forms 6.2.0-pre4
      Xamarin.Forms 2.2.0.31

の環境の話です。
特に、Prism.Forms、Prism.Unity.Formsはプリリリース版なので、正式版では私が遭遇した問題は治っている可能性があります。

多分私の手法は間違っているだろうし、MVVMの観点からもよくないような気がします。
正しいやり方があると思いますが、動く方法が見つかったので、私は良しとしました。
動かないプロダクトよりも動くプロダクトのが何百倍も価値があります。

UnityApplicationは使わない

まず、Prism.Unity.UnityApplicationを継承して、Appを実装しますが、この方法は使いません。
なぜかというと、OnInitializedをオーバーライドし、NavigationSerice.Navigateメソッドで一番最初に表示するページを指示するのですが、TabbedPageを指示するとクラッシュします。ContentPageである、NavigationPageの子要素ならOKですが、そうすると、TabbedPageとNavigationPageが見えなくなります。

ですので、Obsoleteではありますが、Prism.Unity.UnityBootstrapperを継承したクラスを作成し、そのクラスのRunメソッドにAppクラスのインスタンスを渡す方法をとります。
これならば、CreateMainPageをオーバーライドする際に、TabbedPageを指定することができます。

NavigationServiceは使わない

NavigationServiceのインスタンスには、明示的に指定しない限り、Prism.Unity.Navigation.UnityPageNavigationServiceが使われますが、無視します。
このクラスを使えば、本来はNavigationする際の、履歴を管理して、Backしたりできるはずなのですが、私にはどうやってもそれができませんでした。
ソースを見ると、UnityPageNavigationServiceのコンストラクタに、UnityContainerを渡しているのですが、途中でページの履歴の基準がMainPageになってしまっており、これによりNavigationがうまくいかなくなっているように見えます。ページは遷移するのですが、左上に本来は表示されるであろう直前のページへの遷移ボタンが表示されません。

以上を踏まえ今回のソースです。

Sample source code for Demonstration, Experiment and Test - takuya-takeuchi/Demo

Resolution


ViewModel

まず、ViewModelはすべて引数をとりません。またViewModelとViewの紐付けはViewModelLocator.AutowireViewModelで実現しています。
TabbedPageとNavigationViewに対応するViewModelは空なので無視します。

FirstPageViewModel.cs


using Prism.Commands;
using Prism.Common;
using Prism.Mvvm;
using Xamarin.Forms.Portable8.Views;

namespace Xamarin.Forms.Portable8.ViewModels
{
public sealed class FirstPageViewModel : BindableBase, IPageAware
{

public Page Page
{
get;
set;
}

private DelegateCommand _NextCommand;
public DelegateCommand NextCommand
{
get
{
if (this._NextCommand == null)
{
this._NextCommand = new DelegateCommand(() =>
{
this.Page.Navigation.PushAsync(new NextPage());
});
}

return this._NextCommand;
}
}
}
}

NextPageViewModel.cs


using Prism.Commands;
using Prism.Common;
using Prism.Mvvm;

namespace Xamarin.Forms.Portable8.ViewModels
{
public sealed class NextPageViewModel : BindableBase, IPageAware
{
public Page Page
{
get;
set;
}

private DelegateCommand _BackCommand;
public DelegateCommand BackCommand
{
get
{
if (this._BackCommand == null)
{
this._BackCommand = new DelegateCommand(() =>
{
this.Page.Navigation.PopAsync();
});
}

return this._BackCommand;
}
}

}
}

ポイントはPrism.Common.IPageAwareの実装です。単純にPageプロパティを持つだけですが。
これによって、PageのNavigationプロパティにViewModelからアクセスできるようになり、PopAsyncとPushAsyncを呼び出せるようになります。
これでページの遷移が可能になります。

Views

正直、BindingContextを見て、挙動を変える方法はあまり好きではないですし、コードビハインドをいじるのも嫌なんですが...

FirstPage.xaml.cs


using Prism.Common;

namespace Xamarin.Forms.Portable8.Views
{
public partial class FirstPage : ContentPage
{
public FirstPage()
{
// BindingContext is updated before InitializeComponent
this.BindingContextChanged += (sender, args) =>
{
var pageAware = this.BindingContext as IPageAware;
if (pageAware != null)
{
pageAware.Page = this;
}
};

InitializeComponent();
}
}
}

NextPage.xaml.csも同じような感じです。
BindingContextChangeをサブスクライブして、BindingContextがIPageAwareを実装していたら、Pageプロパティに自分自身を設定します。
コメントにもありますが、なぜかInitializeComponentより前で、BindingContextが設定されていたので、苦肉の策でInitializeComponentの前でサブスクライブしてます。

Xamlですが、Main、つまりアプリ全体を表現する要のXamlだけ記載しておきます。他は単純なので...

MainPage.xaml


<?xml version="1.0" encoding="utf-8" ?>
<TabbedPage x:Class="Xamarin.Forms.Portable8.Views.MainPage"
xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:mvvm="clr-namespace:Prism.Mvvm;assembly=Prism.Forms"
xmlns:views="clr-namespace:Xamarin.Forms.Portable8.Views;assembly=Xamarin.Forms.Portable8"
mvvm:ViewModelLocator.AutowireViewModel="True">

<TabbedPage.Padding>
<OnPlatform x:TypeArguments="Thickness"
Android="0"
WinPhone="0"
iOS="0, 20, 0, 0" />
</TabbedPage.Padding>

<TabbedPage.Children>
<views:NavigationPage Title="Tab1">
<x:Arguments>
<views:FirstPage />
</x:Arguments>
</views:NavigationPage>
<views:NavigationPage Title="Tab2">
<x:Arguments>
<views:FirstPage />
</x:Arguments>
</views:NavigationPage>
<views:NavigationPage Title="Tab3">
<x:Arguments>
<views:FirstPage />
</x:Arguments>
</views:NavigationPage>
</TabbedPage.Children>
</TabbedPage>

Startup

最後に、スタートアップ周りのコードです。
冒頭のObsolete等はここです。

App.cs


namespace Xamarin.Forms.Portable8
{
public class App : Application
{
public App()
{
var bootstrapper = new Bootstrapper();
bootstrapper.Run(this);
}

protected override void OnStart()
{
// Handle when your app starts
}

protected override void OnSleep()
{
// Handle when your app sleeps
}

protected override void OnResume()
{
// Handle when your app resumes
}
}
}

Bootstrapper.cs


using System;
using Microsoft.Practices.Unity;
using Prism.Unity;
using Xamarin.Forms.Portable8.ViewModels;
using Xamarin.Forms.Portable8.Views;

namespace Xamarin.Forms.Portable8
{
public sealed class Bootstrapper : UnityBootstrapper
{
[Obsolete]
protected override Xamarin.Forms.Page CreateMainPage()
{
return this.Container.Resolve();
}

protected override void RegisterTypes()
{
// ViewModels
this.Container.RegisterType();
this.Container.RegisterType();
this.Container.RegisterType();
this.Container.RegisterType();

// Views
// Maybe, the following codes are meaningless because NavigationSerivce is not used.
//this.Container.RegisterType();
//this.Container.RegisterType();
//this.Container.RegisterType();
//this.Container.RegisterType();

//this.Container.RegisterTypeForNavigation();
//this.Container.RegisterTypeForNavigation();
//this.Container.RegisterTypeForNavigation();
}

protected override void OnInitialized()
{
// I guess it is meaningless.
//this.NavigationService.Navigate("/NavigationPage/MainPage");
}
}
}

コメントアウトした箇所は実行に影響がないので削除しました。

テスト


動かしてみました。

iOS

[wc_row][wc_column size="one-third" position="first"]
Tab1のFirst page

Tab1のFirst page

[/wc_column][wc_column size="one-third"]

Tab1のNext page

[/wc_column][wc_column size="one-third" position="last"]

Tab2のFirst page

[/wc_column][/wc_row]

Android

[wc_row][wc_column size="one-third" position="first"]
Tab1のFirst page

Tab1のFirst page

[/wc_column][wc_column size="one-third"]
Tab1のNext page

Tab1のNext page

[/wc_column][wc_column size="one-third" position="last"]
Tab2のFirst page

Tab2のFirst page

[/wc_column][/wc_row]

UWP

[wc_row][wc_column size="one-third" position="first"]

Tab1のFirst page

[/wc_column][wc_column size="one-third"]
Tab1のNext page

Tab1のNext page

[/wc_column][wc_column size="one-third" position="last"]
Tab2のFirst page

Tab2のFirst page

[/wc_column][/wc_row]

きちんとタブごとに遷移している状態を覚えています。

Conclusion


かなり苦労しましたが、やりきった感じでいっぱいです。
間違っているようなやり方ですが、動けばいい、という方はいると思うので、だれかの役に立つでしょう。

Source Code


https://github.com/takuya-takeuchi/Demo/tree/master/Xamarin.Forms.Portable8

コメントを残す

メールアドレスが公開されることはありません。

%d人のブロガーが「いいね」をつけました。