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

Problem

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

元ネタ。

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

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

かずき氏の記事。

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

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

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

Resolution

ViewModel

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

FirstPageViewModel.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<?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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
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

Tab1のFirst page
Tab1のFirst page


Tab1のNext page


Tab2のFirst page

Android

Tab1のFirst page
Tab1のFirst page

Tab1のNext page
Tab1のNext page

Tab2のFirst page
Tab2のFirst page

UWP


Tab1のFirst page

Tab1のNext page
Tab1のNext page

Tab2のFirst page
Tab2のFirst page

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

Conclusion

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

Source Code

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