かずきのBlog@hatena

すきな言語は C# + XAML の組み合わせ。Azure Functions も好き。最近は Go 言語勉強中。日本マイクロソフトで働いていますが、ここに書いていることは個人的なメモなので会社の公式見解ではありません。

Windows ストア アプリでページ間で共通の見た目を簡単に作りたい

昼間に書き殴ったあれですが、コードに落としてみます。

Windows ストアアプリでページの共通の見た目を部品化したい… - かずきのBlog@hatena

プロジェクトの作成

とりあえず、空のアプリケーションテンプレートを作成。したあとMainPage.xamlを消して、基本ページをMainPageという名前で作成します。CommonフォルダにNavigationHelperとかを使うか聞いてくるので、さくっとOKして作りましょう。

普通は、基本ページかなんらかのページをベースにして見た目を作っていくのですが、ページ間で共通の見た目やちょっとした挙動を作るのってめんどくさいですよね。一応基本ページには、同じような見た目になるようにXAMLは吐かれてるけど、そのままってわけにもいきませんしね。

テンプレートコントロール作成

本当はページのTemplateを差し替えてってやろうと思ったんですがPageはUserControlなのでTemplate差し替えれないって怒られたのでテンプレートコントロールを作りました。名前はとりあえずPageTemplateという名前で。

デフォルトでは、Controlを継承しているので、これをContentControlに変えます。

public sealed class PageTemplate : ContentControl
{
    public PageTemplate()
    {
        this.DefaultStyleKey = typeof(PageTemplate);
    }
}

次にThemesフォルダにできてるGeneric.xamlにあるStyleをいじります。Styleの中でControlTemplateが定義されているので、ここに共通の見た目を定義します。とりあえずMainPage.xamlのコードをこぴってちょっとだけカスタマイズします。

必要最低限でいくと、AppNameというStaticResourceがみつからないというエラーが出るので、これは{Binding AppName}のように、DataContextからもらうようにします。次に、Grid.Row="1"の箇所にContentPresenterを置きます。

<ControlTemplate TargetType="local:PageTemplate">
    <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
        <Grid.ChildrenTransitions>
            <TransitionCollection>
                <EntranceThemeTransition/>
            </TransitionCollection>
        </Grid.ChildrenTransitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="140"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>

        <!-- Back button and page title -->
        <Grid>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="120"/>
                <ColumnDefinition Width="*"/>
            </Grid.ColumnDefinitions>
            <Button x:Name="backButton" Margin="39,59,39,0" Command="{Binding NavigationHelper.GoBackCommand, ElementName=pageRoot}"
                Style="{StaticResource NavigationBackButtonNormalStyle}"
                VerticalAlignment="Top"
                AutomationProperties.Name="Back"
                AutomationProperties.AutomationId="BackButton"
                AutomationProperties.ItemType="Navigation Button"/>
            <TextBlock x:Name="pageTitle" Text="{Binding AppName}" Style="{StaticResource HeaderTextBlockStyle}" Grid.Column="1" 
                IsHitTestVisible="false" TextWrapping="NoWrap" VerticalAlignment="Bottom" Margin="0,0,30,40"/>
        </Grid>
        <!-- Content is here !! -->
        <ContentPresenter Grid.Row="1" />                        
    </Grid>
</ControlTemplate>

ビルドしてページに置いてみます。

<Page
    x:Name="pageRoot"
    x:Class="TemplateApp.MainPage"
    DataContext="{Binding DefaultViewModel, RelativeSource={RelativeSource Self}}"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:TemplateApp"
    xmlns:common="using:TemplateApp.Common"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d">
    <local:PageTemplate />
</Page>

ページタイトルが表示されないので、とりあえずコードからDefaultViewModelにAppNameを設定するようにします。

// MainPage.xaml.cs
private void navigationHelper_LoadState(object sender, LoadStateEventArgs e)
{
    this.DefaultViewModel["AppName"] = "My sample app";
}

これで、とりあえずそれっぽく動くようになりました。

f:id:okazuki:20140109001514p:plain

ツールボックスからドキュメントアウトラインのPageTemplateに対してGridをぽとりと落としてレイアウトをリセットすると、あとは普通に画面にコントロールをぽとぺたできます。

f:id:okazuki:20140109001825p:plain

XAMLはこんな感じ。

<Page
    x:Name="pageRoot"
    x:Class="TemplateApp.MainPage"
    DataContext="{Binding DefaultViewModel, RelativeSource={RelativeSource Self}}"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:TemplateApp"
    xmlns:common="using:TemplateApp.Common"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d">
    <local:PageTemplate >
        <Grid>
            <Button Content="Button" HorizontalAlignment="Left" Margin="254,106,0,0" VerticalAlignment="Top"/>
            <Button Content="Button" HorizontalAlignment="Left" Margin="331,267,0,0" VerticalAlignment="Top"/>
            <Image HorizontalAlignment="Left" Height="100" Margin="522,87,0,0" VerticalAlignment="Top" Width="100" Source="Assets/Logo.png"/>
        </Grid>
    </local:PageTemplate>
</Page>

基本ページを、NextPageという名前で作って同じようにPageTemplateを置いて、DefaultViewModelのAppNameにNextPageという文字列を設定したものを作ります。下の図は、適当にTimePickerを置いてみたのですが、コンテンツ部分以外は、MainPageと同じ見た目をさくっと作れます。

f:id:okazuki:20140109002228p:plain

MainPageに適当に置いたボタンをダブルクリックすると普通にコードビハインドにイベントハンドラが作成されることが確認できるので、そこでNextPageへ画面遷移する処理を書いてみました。

private void Button_Click(object sender, RoutedEventArgs e)
{
    this.Frame.Navigate(typeof(NextPage));
}

実行してみると、とりあえずちゃんと動いてるっぽい。

f:id:okazuki:20140109003014p:plain

f:id:okazuki:20140109003214p:plain

MainPageでは戻るボタンが出ずに、NextPageでは出てるあたりちゃんと動いてる感がします。とまぁ昼間思いついたのは以上でした。

あとは、アプリバーとかも共通なものは共通化したいなぁ。あ、あとはシンプルに、ページの中にFrameを置いて、そこの中で画面遷移するってのでもよさそうだけど、既存のNavigationHelperとかとうまいこと連携できるのかは謎。