かずきのBlog@hatena

日本マイクロソフトに勤めています。XAML + C#の組み合わせをメインに、たまにASP.NETやJavaなどの.NET系以外のことも書いています。掲載内容は個人の見解であり、所属する企業を代表するものではありません。

Universal Windows Platform appでFrameをWindow.Current.Contentに持たないアプリの作り方

Windows Insider Preview 10074 + VS2015 RC時点の情報です

UWP appやストアアプリ等はWindow.Current.ContentにFrameを持つアプリが一般的です。ですが、Frameを持ったページをWindow.Current.Contentに持ったアプリもちらほら見かけます。ハンバーガーメニューを持ったアプリなどは特にその傾向が見受けられます。

ということでやり方を紹介します。

Frameを持ったページの作成

まず、Frameを持ったページを作成します。

<Page
    x:Class="App30.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:App30"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d">

    <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition />
        </Grid.RowDefinitions>
        <!-- Header -->
        <StackPanel Orientaion="Horizontal">
            <Button x:Name="ButtonBack"
                    Style="{ThemeResource NavigationBackButtonNormalStyle}" />
            <TextBlock x:Name="TextBlockTitle"
                       Style="{ThemeResource TitleTextBlockStyle}"
                       Text="RootFrame App" />
        </StackPanel>
        <!-- 今回の主役 -->
        <Frame x:Name="RootFrame"
               x:FieldModifier="public" 
               Grid.Row="1"/>
    </Grid>
</Page>

ポイントは、ページ内にFrameを作ってx:Nameをつけることと、x:FieldModifierでpublicにするところです。

初期表示用ページの作成

MainPageにホストされたFrame内に表示するページを作成します。FirstPageとSecondPageという名前で適当に作ります。

XAMLは以下のような感じです。どのページにいるかわかる必要最低限にしてます。

<Page
    x:Class="App30.FirstPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:App30"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d">

    <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
        <TextBlock Style="{ThemeResource HeaderTextBlockStyle}" Text="FirstPage" />
    </Grid>
</Page>

App.xaml.csの編集

App.xaml.csがWindow.Current.ContentにFrameを設定するようになっているのでMainPageをWindow.Current.Contentに設定するように直します。

protected override void OnLaunched(LaunchActivatedEventArgs e)
{

    // MainPageが作成済みか
    var mainPage = Window.Current.Content as MainPage;
    if (mainPage == null)
    {
        // MainPageが作成されてなかったら作成する
        mainPage = new MainPage();

        mainPage.RootFrame.NavigationFailed += OnNavigationFailed;

        if (e.PreviousExecutionState == ApplicationExecutionState.Terminated)
        {
            //TODO: Load state from previously suspended application
        }

        // Window.Current.ContentをMainPageにする(普通はFrameになってるところ)
        Window.Current.Content = mainPage;
    }

    if (mainPage.RootFrame.Content == null)
    {
        // MainPageのRootFrameを使ってFirstPageに画面遷移する
        mainPage.RootFrame.Navigate(typeof(FirstPage), e.Arguments);
    }
    Window.Current.Activate();
}

実行して動作確認

こんな感じにFirstPageがMainPage内に表示されます。

f:id:okazuki:20150510191745p:plain

画面遷移の作成

これで、基本的には出来上がりなのですが画面遷移もついでに作りこんでいきます。ListViewに遷移先の画面を表示させて、クリックで遷移するという形にします。まず、画面遷移先のページと表示用のラベル文字列を持ったクラスを定義します。

using System;

namespace App30
{
    public class NavigationPageInfo
    {
        public Type PageType { get; set; }
        public string Title { get; set; }
    }
}

そして、このクラスを使ってMainPage.xaml.csに画面の定義を持った配列を作成します。

public NavigationPageInfo[] NavigationPageInfos { get; } = new[]
{
    new NavigationPageInfo { Title = "FirstPage", PageType = typeof(FirstPage) },
    new NavigationPageInfo { Title = "SecondPage", PageType = typeof(SecondPage) },
};

そして、これをItemsSourceにバインドするListViewをMainPageのヘッダー部に作ります。要素は横並びな感じにしました。

<Page
    x:Class="App30.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:App30"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d">

    <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition />
        </Grid.RowDefinitions>
        <!-- Header -->
        <StackPanel Orientation="Horizontal">
            <Button x:Name="ButtonBack"
                    Style="{ThemeResource NavigationBackButtonNormalStyle}" />
            <TextBlock x:Name="TextBlockTitle"
                       Style="{ThemeResource TitleTextBlockStyle}"
                       Text="RootFrame App" />
            <ListView x:Name="ListViewNavigation"
                      ItemsSource="{x:Bind NavigationPageInfos}"
                      SelectionMode="None"
                      Tapped="{x:Bind Navigate}">
                <ListView.ItemTemplate>
                    <DataTemplate x:DataType="local:NavigationPageInfo">
                        <TextBlock Style="{ThemeResource BodyTextBlockStyle}" Text="{x:Bind Title}" />
                    </DataTemplate>
                </ListView.ItemTemplate>
                <ListView.ItemsPanel>
                    <ItemsPanelTemplate>
                        <VirtualizingStackPanel Orientation="Horizontal" />
                    </ItemsPanelTemplate>
                </ListView.ItemsPanel>
            </ListView>
        </StackPanel>
        <!-- 今回の主役 -->
        <Frame x:Name="RootFrame"
               x:FieldModifier="public"
               Grid.Row="1"/>
    </Grid>
</Page>

Tappedイベントのイベントハンドラは以下のような感じです。タップされた要素のDataContextからNavigationPageInfoクラスを取り出して、現在表示中のページと異なる場合は遷移しています。

public void Navigate(object sender, TappedRoutedEventArgs e)
{
    var info = (e.OriginalSource as FrameworkElement)?.DataContext as NavigationPageInfo;
    if (info == null) { return; }
    if (this.RootFrame.Content.GetType() == info.PageType) { return; }

    this.RootFrame.Navigate(info.PageType);
}

これで実行すると以下のようになります。

f:id:okazuki:20150510193335p:plain

SecondPageをクリックするとページ遷移します。

f:id:okazuki:20150510193414p:plain

戻るボタンの実装

ずっと存在だけして働いてなかった戻るボタンの処理を実装します。以下のような感じでイベントハンドラを紐づけて

<Button x:Name="ButtonBack"
        Style="{ThemeResource NavigationBackButtonNormalStyle}"
        Click="{x:Bind GoBack}"/>

こんな感じで戻る処理を追加します。

public void GoBack()
{
    if (this.RootFrame.CanGoBack)
    {
        this.RootFrame.GoBack();
    }
}

このままだと、戻れないときも戻るボタンが表示されるため、FrameのCanGoBackとButtonのIsEnabledを紐づけておきます。

<Button x:Name="ButtonBack"
        Style="{ThemeResource NavigationBackButtonNormalStyle}"
        IsEnabled="{x:Bind RootFrame.CanGoBack, Mode=OneWay}"
        Click="{x:Bind GoBack}"/>

実行すると以下のようになります。初期状態では、戻るボタンは押せない。

f:id:okazuki:20150510193830p:plain

SecondPageに行くと戻るボタンが押せるようになる。

f:id:okazuki:20150510194030p:plain

戻るボタンを押すとFirstPageに戻って、戻るボタンが押せなくなる。

f:id:okazuki:20150510194116p:plain