かずきのBlog@hatena

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

Windows 8.1のストアアプリのテンプレートの構造

Windows 8.1 RTM + Visual Studio 2013時点の情報です

Windows ストア アプリの開発は、テンプレートに従ってやるのが一番の近道です。ただ、Windows 8とWindows 8.1でテンプレートの中身が結構変わりました。どれくらい変わったかというと、Commonフォルダの中身が以下のように変わってます。

Windows 8

  • BindableBase.cs
  • BooleanNegationConverter.cs
  • BooleanToVisibilityConverter.cs
  • LayoutAwarePage.cs
  • RichTextColumns.cs
  • StandardStyles.xaml
  • SuspensionManager.cs

Windows 8.1

  • NavigationHelper.cs
  • ObservableDictionary.cs
  • RelayCommand.cs
  • SuspensionManager.cs

何が変わったの?

むしろ何が変わってないのかを探すほうが早い感じですがSuspensionManagerは機能的には同じです。コードのdiffをとったわけではないですが、どちらも258行のコードでした。あとは変わってます。Converterや、LayoutAwarePageやStandardStyles.xamlのことは忘れましょう。

LayoutAwarePageはどこにいった

Windows 8の頃は、基本的にLayoutAwarePageを継承したクラスを使って画面を作っていました。Windows 8.1では、これを辞めて普通のPageクラスを継承したクラスを使います。では、LayoutAwarePageで提供していた画面遷移の履歴の保持etc...の様々な機能はどこにいったのかというと、NavigationHelperクラスに移動されています。

継承から委譲へ

Windows 8のページのテンプレートの困ったところは、LayoutAwarePageに依存しないページを作ろうと思ったらプレーンな何も無い空白のページをベースに作らなければなりませんでした。その他のページは全てLayoutAwarePageを継承しているため、これを書き換えて普通のPageや自前のPageの基本クラスに差し替えるのは結構骨の折れる作業です。LayoutAwarePageに依存しない開発をするなら、アイテムテンプレートを自作しないと現実的じゃないくらいです。

実際にPrism for Windows Runtimeでは、登場当初はテンプレートの書き換え手順を示していましたが、暫くして中の人がアイテムテンプレートとプロジェクトテンプレートを提供してくれました。これのおかげで大分楽に作れるようになりました。

Windows 8.1では、PageクラスからNavigationHelperクラスを使うことで画面遷移の履歴保持などの機能を提供してくれています。参考までに、Windows 8.1の一番シンプルなブランクページのコードを以下に示します。

public sealed partial class BasicPage : Page
{

    private NavigationHelper navigationHelper;
    private ObservableDictionary defaultViewModel = new ObservableDictionary();

    /// <summary>
    /// これは厳密に型指定されたビュー モデルに変更できます。
    /// </summary>
    public ObservableDictionary DefaultViewModel
    {
        get { return this.defaultViewModel; }
    }

    /// <summary>
    /// NavigationHelper は、ナビゲーションおよびプロセス継続時間管理を
    /// 支援するために、各ページで使用します。
    /// </summary>
    public NavigationHelper NavigationHelper
    {
        get { return this.navigationHelper; }
    }


    public BasicPage()
    {
        this.InitializeComponent();
        this.navigationHelper = new NavigationHelper(this);
        this.navigationHelper.LoadState += navigationHelper_LoadState;
        this.navigationHelper.SaveState += navigationHelper_SaveState;
    }

    /// <summary>
    /// このページには、移動中に渡されるコンテンツを設定します。前のセッションからページを
    /// 再作成する場合は、保存状態も指定されます。
    /// </summary>
    /// <param name="sender">
    /// イベントのソース (通常、<see cref="NavigationHelper"/>)>
    /// </param>
    /// <param name="e">このページが最初に要求されたときに
    /// <see cref="Frame.Navigate(Type, Object)"/> に渡されたナビゲーション パラメーターと、
    /// 前のセッションでこのページによって保存された状態の辞書を提供する
    /// セッション。ページに初めてアクセスするとき、状態は null になります。</param>
    private void navigationHelper_LoadState(object sender, LoadStateEventArgs e)
    {
    }

    /// <summary>
    /// アプリケーションが中断される場合、またはページがナビゲーション キャッシュから破棄される場合、
    /// このページに関連付けられた状態を保存します。値は、
    /// <see cref="SuspensionManager.SessionState"/> のシリアル化の要件に準拠する必要があります。
    /// </summary>
    /// <param name="sender">イベントのソース (通常、<see cref="NavigationHelper"/>)</param>
    /// <param name="e">シリアル化可能な状態で作成される空のディクショナリを提供するイベント データ
    ///。</param>
    private void navigationHelper_SaveState(object sender, SaveStateEventArgs e)
    {
    }

    #region NavigationHelper の登録

    /// このセクションに示したメソッドは、NavigationHelper がページの
    /// ナビゲーション メソッドに応答できるようにするためにのみ使用します。
    /// 
    /// ページ固有のロジックは、
    /// <see cref="GridCS.Common.NavigationHelper.LoadState"/>
    /// および <see cref="GridCS.Common.NavigationHelper.SaveState"/> のイベント ハンドラーに配置する必要があります。
    /// LoadState メソッドでは、前のセッションで保存されたページの状態に加え、
    /// ナビゲーション パラメーターを使用できます。

    protected override void OnNavigatedTo(NavigationEventArgs e)
    {
        navigationHelper.OnNavigatedTo(e);
    }

    protected override void OnNavigatedFrom(NavigationEventArgs e)
    {
        navigationHelper.OnNavigatedFrom(e);
    }

    #endregion
}

少しずつ解説していきます。

DefaultViewModelプロパティ

DefaultViewModelというObservableDictionary型のプロパティが定義されています。ObservableDictionaryクラスはCommon名前空間で定義されている、変更通知機能を持ったDictionaryです。Windows 8の頃はLayoutAwarePageで持っていたプロパティになりますが、Windows 8.1では、ページに直接定義されるようになりました。

これはただ単に、XAML側でDataContextにBindingされているプロパティになります。該当箇所を以下に示します。

<Page
    ... 省略 ...
    DataContext="{Binding DefaultViewModel, RelativeSource={RelativeSource Self}}"
    ... 省略 ...
    mc:Ignorable="d">

DefaultViewModelのプロパティのコメントにあるとおり、ここの型をObservableDictionary型から変えることで、任意の型をページのViewModelとして割り当てることができます。

private ObservableDictionary defaultViewModel = new ObservableDictionary();

/// <summary>
/// これは厳密に型指定されたビュー モデルに変更できます。
/// </summary>
public ObservableDictionary DefaultViewModel
{
    get { return this.defaultViewModel; }
}

例えばMyViewModelというViewModelを定義して、それを使いたい場合は以下のようにコードを書き換えます。

private MyViewModel defaultViewModel = new MyViewModel();

public MyViewModel DefaultViewModel
{
    get { return this.defaultViewModel; }
}

基本クラスではなく、ページに定義されるようになったことで、このように簡単に型を変えることができるようになりました。この点はとってもいいと思います。Windows 8のLayoutAwarePageの場合は、ページに独自プロパティを定義して、それをDataContextに紐づけることで同様のことができますが、LayoutAwarePageに定義されたDefaultViewModelプロパティは残りっぱなしなので、なんだか気持ち悪い感じになってました。

NavigationHelper

このクラスは、名前が指し示す通りナビゲーションでWindows ストア アプリが持つべき一般的な動きをページクラスに付与するヘルパークラスです。NavigationHelper.csを開くと、クラスのドキュメントコメントに使用方法が書かれています。そこにある通り、PageクラスにNavigationHelperを組み込むには以下のようなコードをPageクラスに追記します。

private NavigationHelper navigationHelper;

public コンストラクタ()
{
    this.InitializeComponent();
    this.navigationHelper = new NavigationHelper(this);
    this.navigationHelper.LoadState += navigationHelper_LoadState;
    this.navigationHelper.SaveState += navigationHelper_SaveState;
}

public NavigationHelper NavigationHelper
{
    get { return this.navigationHelper; }
}

private void navigationHelper_LoadState(object sender, LoadStateEventArgs e)
{
}

private void navigationHelper_SaveState(object sender, SaveStateEventArgs e)
{
}

protected override void OnNavigatedTo(NavigationEventArgs e)
{
    navigationHelper.OnNavigatedTo(e);
}

protected override void OnNavigatedFrom(NavigationEventArgs e)
{
    navigationHelper.OnNavigatedFrom(e);
}

基本的に必要な操作は、NavigationHelperクラスをフィールドに持ち、コンストラクタでNavigationHelperのインスタンスを作成します。そのときNavigationHelperのコンストラクタにページ自身のインスタンスを渡します。こうすることでページの各種イベントなどを適切に処理してLayoutAwarePageが提供してくれてた機能の一部(ショートカットや戻るボタンなど)を提供してくれます。NavigationHelperクラスにはGoBackCommandというコマンドが提供されていて、それが戻るボタンのCommandとバインドされています。この点もLayoutAwarePage内のGoBackイベントハンドラなどでやっていた処理がコマンドでシンプルにあらわされていて個人的に好きです。(中身はそんなに変わらないですが…)

次に、ページの履歴やページの一時データの保持などですが、LayoutAwarePageのときはLoadStateとSaveStateというメソッドをオーバーライドして利用していました。NavigationHelperでは、LoadStateイベントとSaveStateイベントが定義されていて、それを購読することで同様の処理が行えます。LoadStateイベントとSaveStateイベントは、いまどき珍しく独自のdelegateとして定義されていて、以下のようなシグネチャを持ちます。

/// <summary>
/// <see cref="NavigationHelper.LoadState"/> イベントを処理するメソッドを表します
/// </summary>
public delegate void LoadStateEventHandler(object sender, LoadStateEventArgs e);
/// <summary>
/// <see cref="NavigationHelper.SaveState"/> イベントを処理するメソッドを表します
/// </summary>
public delegate void SaveStateEventHandler(object sender, SaveStateEventArgs e);

LoadStateイベントのイベント引数であるLoadStateEventArgsクラスは以下のプロパティを持っています。

  • public object NavigationParameter { get; }
    • 画面遷移で前の画面から渡されたパラメータを取得できます。
  • public Dictionary<string, Object> PageState { get; }
    • 以前にこのページでSaveStateのときに設定した値が入ったDictionaryを取得します。はじめてページにアクセスするときはnullになります。

SaveStateイベントのイベント引数であるSaveStateEventArgsクラスは以下のプロパティを持っています。

  • public Dictionary<string, Object> PageState { get; }
    • ページで一時的に保存したいデータを詰めるDictionaryを取得します。ここで設定した値がLoadStateイベントのときに取得できます。

このイベントを処理することで、LayoutAwarePageのときにLoadStateメソッドとSaveStateメソッドで行っていたのと同じ処理が可能になります。

ObservableDictionaryクラス

通知機能をもったDictionaryです。デフォルトのページのテンプレートでDefaultViewModelプロパティに指定されるためだけに存在してると思われます。

RelayCommandクラス

ICommandインターフェースの、よくある実装です。ExecuteとCanExecuteをデリゲートで指定可能です。NavigationHelperのGoBackCommandプロパティなどで使用されています。

StandardStyles.xamlはどこにいった?

様々な基本的なスタイルが定義されていたStandardStyles.xamlですがいなくなりました。戻るボタンなどのような基本的なよくあるスタイルは、以下のXAMLで定義されています。

C:\Program Files (x86)\Windows Kits\8.1\Include\winrt\xaml\design\generic.xaml

テーマごとの色などの定義や、コントロールのテンプレートも定義されています。AppBarButtonは、専用クラス定義され、そのコントロールにアイコンの設定機能などが追加されています。XAMLでボタンのテンプレートを差し替えるだけでAppBarのボタンを作れるとはいえ、専用クラスが用意されていたほうが楽ですね。

まとめ

プロジェクトテンプレートに含まれていたCommon名前空間のクラス構造が大きく変わった(そもそもCommonってフォルダがあって、そこに絶対必要な機能がプロジェクトテンプレートがあるのがおかしいと思ってる)ので、結構最初は戸惑うと思いますが、提供されてる機能はそんなに変わりありません。スナップとかが8.1になって無くなったぶん覚えることが減ってシンプルになったくらいです。

さらに、NavigationHelperの機能がいらない場合は委譲してる部分のコードを削除するだけでいいのでテンプレートに縛られないページ作りが楽になったので着実に進歩してると思います。とりあえず、Windows 8.1の一般公開及び、Visual Studio 2013の正式版の提供が楽しみですね。