かずきのBlog@hatena

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

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とかとうまいこと連携できるのかは謎。