Windowsストアアプリにおける グリッドアプリケーションについて(4)

前回にデータソースの説明をしました。SampleDataSourceの構造が理解できましたので、改めてGroupedItemPage.xaml.csのLoadStateのコードを振り返ります。

 protected override void LoadState(
                        Object navigationParameter, 
                        Dictionary<String, Object> pageState)
{
    // TODO: 問題のドメインでサンプル データを置き換えるのに適したデータ モデルを作成します
    var sampleDataGroups = SampleDataSource.GetGroups(
                               (String)navigationParameter);
    this.DefaultViewModel["Groups"] = sampleDataGroups;
}

SampleDataSource.GetGroupsメソッドは、静的メソッドになっていますから、呼び出されるとメンバー変数の_sampleDataSourceのAllGroupsプロパティの値を返します。そして、 _sampleDataSourceメンバー変数は静的変数となっており、SampleDataSourceクラスのインスタンスを保持しています。_sampleDataSourceメンバー変数が静的変数となっている点が重要です。何故なら、静的変数はアプリケーションドメイン単位に保持されますから、異なるページのインスタンスへナビゲーションしたとしても_sampleDataSourceは、同一のインスタンスを保持し続けるからです(要は、キャッシュの効果があるのです)。

また、AllGroupsメソッドは引数が"AllGroups"であることが要求されますので、このパラメータがどのように渡されるかを確認するためにApp.xaml.cs の OnLaunchedイベントハンドラーを以下に示します。

 /// <summary>
/// アプリケーションがエンド ユーザーによって正常に起動されたときに呼び出されます。他のエントリ ポイントは、
/// アプリケーションが特定のファイルを開くために呼び出されたときに
/// 検索結果やその他の情報を表示するために使用されます。
/// </summary>
/// <param name="args">起動要求とプロセスの詳細を表示します。</param>
protected override async void OnLaunched(LaunchActivatedEventArgs args)
{
    Frame rootFrame = Window.Current.Content as Frame;

    // ウィンドウに既にコンテンツが表示されている場合は、アプリケーションの初期化を繰り返さずに、
    // ウィンドウがアクティブであることだけを確認してください
    
    if (rootFrame == null)
    {
        // ナビゲーション コンテキストとして動作するフレームを作成し、最初のページに移動します
        rootFrame = new Frame();
        //フレームを SuspensionManager キーに関連付けます                                
        SuspensionManager.RegisterFrame(rootFrame, "AppFrame");

        if (args.PreviousExecutionState == 
                 ApplicationExecutionState.Terminated)
        {
            // 必要な場合のみ、保存されたセッション状態を復元します
            try
            {
                await SuspensionManager.RestoreAsync();
            }
            catch (SuspensionManagerException)
            {
                //状態の復元に何か問題があります。
                //状態がないものとして続行します
            }
        }

        // フレームを現在のウィンドウに配置します
        Window.Current.Content = rootFrame;
    }
    if (rootFrame.Content == null)
    {
        // ナビゲーション スタックが復元されていない場合、最初のページに移動します。
        // このとき、必要な情報をナビゲーション パラメーターとして渡して、新しいページを
        // を構成します
        if (!rootFrame.Navigate(typeof(GroupedItemsPage), "AllGroups"))
        {
            throw new Exception("Failed to create initial page");
        }
    }
    // 現在のウィンドウがアクティブであることを確認します
    Window.Current.Activate();
}

/// <summary>
/// アプリケーションの実行が中断されたときに呼び出されます。アプリケーションの状態は、
/// アプリケーションが終了されるのか、メモリの内容がそのままで再開されるのか
/// わからない状態で保存されます。
/// </summary>
/// <param name="sender">中断要求の送信元。</param>
/// <param name="e">中断要求の詳細。</param>
private async void OnSuspending(object sender, SuspendingEventArgs e)
{
    var deferral = e.SuspendingOperation.GetDeferral();
    await SuspensionManager.SaveAsync();
    deferral.Complete();
}

OnLaunchedイベントハンドラーでは、「if (rootFrame == null) 」 で2つのことをしています。

  • SuspensionManagerのRegisterFrameメソッドを呼び出している。
    Frame オブジェクトのナビゲーション履歴を保存(Frame.GetNavigationState)するためです。
  • 強制終了させられた場合の状態の復旧
    これは、強制終了させられた時に表示されていたページやナビゲーション履歴を復旧(Frame.SetNavigationState)しています。

SuspensionManagerのコードの提示を行いませんが、上記の処理とOnSuspendingイベントハンドラーのSuspensionManager.SaveAsyncメソッドにより、Frameオブジェクトのナビゲーション履歴などが保存されると理解しておいてください。このようになっている理由は、Windowsストアアプリの望ましい起動方法に起因しています。具体的には、以下のような動作が望まれます。

  • 以前に起動していない場合は、トップページが表示される。
  • 強制終了させられた場合は、ユーザーが作業を再開できるように最後に操作をしていたページが表示される。

OnLaunchedイベントハンドラーの後半の「if rootFrame.Content == null」で、通常起動(強制終了後の起動であれば、この条件は不成立)でナビゲーション パラメータとして「AllGroups」を指定してGroupedItemPageへナビゲートします。

残ったGroupedItemPageで説明していないことは、GroupDetailPageとItemDetailPageへのナビゲーションになります。ナビゲーションのイベントハンドラーを以下に示します。

 /// <summary>
/// グループ ヘッダーがクリックされたときに呼び出されます。
/// </summary>
/// <param name="sender">ボタンは、選択されたグループのグループ ヘッダーとして使用されます。</param>
/// <param name="e">クリックがどのように開始されたかを説明するイベント データ。</param>
void Header_Click(object sender, RoutedEventArgs e)
{
    // ボタン インスタンスがどのグループを表すかを確認します
    var group = (sender as FrameworkElement).DataContext;

    // 適切な移動先のページに移動し、新しいページを構成します。
    // このとき、必要な情報をナビゲーション パラメーターとして渡します
    this.Frame.Navigate(typeof(GroupDetailPage), 
                        ((SampleDataGroup)group).UniqueId);
}

/// <summary>
/// グループ内のアイテムがクリックされたときに呼び出されます。
/// </summary>
/// <param name="sender">クリックされたアイテムを表示する GridView (アプリケーションがスナップ
/// されている場合は ListView) です。</param>
/// <param name="e">クリックされたアイテムを説明するイベント データ。</param>
void ItemView_ItemClick(object sender, ItemClickEventArgs e)
{
    // 適切な移動先のページに移動し、新しいページを構成します。
    // このとき、必要な情報をナビゲーション パラメーターとして渡します
    var itemId = ((SampleDataItem)e.ClickedItem).UniqueId;
    this.Frame.Navigate(typeof(ItemDetailPage),
                        itemId);
}

Header_Clickでは、グループヘッダーのDataContextがSampleGroupオブジェクトであることから、グループのIDを指定してGroupDetailPageへナビゲートしています。ItemView_ItemClickでは、ClickedItemがSampleDataItemオブジェクトであることから、アイテムのIDを指定してItemDetailPageへナビゲートしています。これは、データバインドしていることから、データバインドされた特徴を生かしたコードになっています。

ここまでで、GroupedDetailPageの説明ができました。説明してきたことから、カスタマイズする場合の特徴を以下の記載します。

  • 新しいページを作成する場合は、LayoutAwarePageを継承させる。
  • データソースをカスタマイズする場合は、SampleDataItem、SampleDataGroupへのプロパティの追加や修正と一緒に、本番データを読み込むメソッドを用意する。
  • データソースを変更すれば、ItemTemplateなどを変更内容に応じて変更する。
  • ビューの切替は、VisualStateManagerを使って行っている(実行時とVisual Studioのデバイスタブ)。
    ユーザーコントロールには、LoadedとUnloadedイベントハンドラーにLayoutAwarePageのイベントハンドラーを設定する。
  • SuspensionManager.RegisterFrameメソッドで、Frameのナビゲーション履歴が保存され、復旧される。
    復旧時はナビゲーション履歴だけなので、データソースは自分で復旧させる必要があることを意味しています。
    また、検索コントラクトのように新しいページで起動される場合は、SuspensionManager.RegisterFrameメソッドでFrameを登録する必要があることを意味しています。(認定要件 3.6 システムが提供するメカニズムを備えた機能をアプリがサポートする場合は、システムが提供するメカニズムを使用しなければならない)。

認定要件3.6は必須事項ではありませんが、グリッドアプリケーションがSuspensionManager.RegisterFrameメソッドでナビゲーション履歴を使った強制終了時の状態復旧に対応しているため、データソースの復旧をした方が良いでしょう。逆に、ナビゲーション履歴を復旧させなければ、データソースを復旧させる必要もありません。次回以降は、GroupDetailPageの解説を行う予定です。