読者です 読者をやめる 読者になる 読者になる

みかづきメモ

学習したことのメモとか、日記とか、備忘録。

Xamarin.Forms で TabbedPage の子に NavigationPage を追加してみる

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

ただ、

NavigationPage
|- TabbedPage

といった構成はよく見かけるものの、その逆は見かけません。
また、上のような構成は、非推奨となっているようです。

たとえば、ほとんどのプラットフォームでは NavigationPage の子ページとして TabbedPage を追加しないよう推奨されます。 https://msdn.microsoft.com/ja-jp/magazine/dn904669.aspx

そこで、その逆、 TabbedPage の子ページとして NavigationPage を追加してみます。


単純に書くとしたら、下のような XAML になるはず。

MainPage.xaml

<TabbedPage x:Class="Mikazuki.MainPage"
            xmlns="http://xamarin.com/schemas/2014/forms"
            xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
            xmlns:local="clr-namespace:Mikazuki">
    <TabbedPage.Children>
        <local:NavPage />
    </TabbedPage.Children>
</TabbedPage>


NavPage.xaml

<NavigationPage x:Class="Mikazuki.NavPage"
                xmlns="http://xamarin.com/schemas/2014/forms"
                xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml">
    <Label Text="Hello!" 
           HorizontalOptions="CenterAndExpand"
           VerticalOptions="CenterAndExpand" />
</NavigationPage>


ただ、こうすると、

InvalidOperationException: NavigationPage must have a root Page before being used. Either call PushAsync with a valid Page, or pass a Page to the constructor before usage.

と、例外が発生してしまいます。
Root ページを設定しろと怒られているので、 NavigationPage(Page) を呼び出す必要があります。

NavigationPage(Xamarin.Forms.Page) - Xamarin


ということで、書き換えます。

MainPage.xaml

<TabbedPage x:Class="Mikazuki.MainPage"
            xmlns="http://xamarin.com/schemas/2014/forms"
            xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
            xmlns:local="clr-namespace:Mikazuki">
    <TabbedPage.Children>
        <local:NavPage>
            <x:Arguments>
                <Label Text="Hello!" 
                       HorizontalOptions="CenterAndExpand"
                       VerticalOptions="CenterAndExpand" />
            </x:Arguments>
        </local:NavPage>
    </TabbedPage.Children>
</TabbedPage>

NavPage.xaml のコンテンツを、 x:Arguments の子供として記述します。


NavPage.xaml

<NavigationPage x:Class="Mikazuki.NavPage"
                xmlns="http://xamarin.com/schemas/2014/forms"
                xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml">
    <x:Arguments />
</NavigationPage>


NavPage.xaml.cs

using Xamarin.Forms;

namespace Mikazuki
{
    public partial class NavPage : NavigationPage
    {
        public NavPage(Page page) : base(page)
        {
            InitializeComponent();
        }
    }
}

うえで示した、コンストラクタに対して、 MainPage.xaml のコンテンツを渡します。
こうすることで、TabbedPage の子として NavigationPage を追加できました。

でも、このままの状態だと、 MainPage.xaml が非常に大きくなります。
それはあれですので、多少使いやすいように改造します。

構造的にはこんな感じ。

MainPage (TabbedPage)
|- NavPage (NavigationPage)
|  |- FirstPage (ContentPage)
|
|- ...


MainPage.xaml

<TabbedPage x:Class="Mikazuki.MainPage"
            xmlns="http://xamarin.com/schemas/2014/forms"
            xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
            xmlns:local="clr-namespace:Mikazuki">
    <TabbedPage.Children>
        <local:NavPage>
            <x:Arguments>
                <local:FirstPage />
            </x:Arguments>
        </local:NavPage>
    </TabbedPage.Children>
</TabbedPage>


NavPage.xaml

<NavigationPage x:Class="Mikazuki.NavPage"
                xmlns="http://xamarin.com/schemas/2014/forms"
                xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml">
    <x:Arguments />
</NavigationPage>

これは、さっきのものと変わりません。


NavPage.xaml.cs

using Xamarin.Forms;

namespace Mikazuki
{
    public partial class NavPage : NavigationPage
    {
        public NavPage(Page page) : base(page)
        {
            InitializeComponent();
            Title = page.Title;
            Icon = page.Icon;
        }
    }
}

親である TabbedPage にアイコンとタブを表示するために、
TitleIcon を設定しておきます。


FirstPage.xaml

<ContentPage x:Class="Mikazuki.FirstPage"
             xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml">
    <Label Text="Hello!" 
           HorizontalOptions="CenterAndExpand"
           VerticalOptions="CenterAndExpand" />
</ContentPage>

これで、やりたいことを達成することができました。
あとは、前回書いた NavigationService を、いい感じに切り替えることで、
自然に使えるのではないかと思っています。


参考


2015/11/10 追記
BindingContext を使ってどうのこうのする場合は、

page.BindingContextChanged += (sender, args) =>
{
    var viewModel = page.BindingContext as HogeViewModelBase; // Title, Icon プロパティがある。
    if (viewModel == null)
        return;
    Title = viewModel.Title;
    Icon = viewModel.Icon;
};

みたいなものを、 NavBar.xaml.cs のコンストラクタに追加すると幸せになれる気がします。