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

久し振りのエントリーになります。これから、何回かに渡ってVisual Studio 2012に含まれる Windows ストア アプリケーションのグリッド アプリケーション テンプレートを紐解いて行きます。

グリッド アプリケーションのナビゲーションは、以下に示すように3階層になっています。
Grid Application Navigation

ナビゲーションに含まるXAMLページは、以下のようになっています。

  • GroupedItemsPage.xaml
    ハブとなるページで、GridViewコントロールとListViewコントロールを持ちます。
  • GroupDetailPage.xaml
    セクションとなるページで、GridViewコントロールとListViewコントロールを持ちます。
  • ItemDetailPage.xaml
    詳細となるページで、FlipViewコントロールを持ちます。

また、各ページとLayoutAwarePageクラスの関係は、以下のようになっています。
Page UML

各ページは、LayoutAwarePageという抽象クラスを継承した構造になっています。このことから、明確になることが1つあります。それは、新しいページを作成した場合は、必ずLayoutAwarePageクラスを継承するようにした方が良いということです。具体的には、検索コントラクトで追加されるSearchResultsPage.xamlなどです。この理由は、LayoutAwarePageを説明していくことで解説をしていきます。
それでは、GroupedItemPage.xaml にある、LayoutAwrePageの定義を見ていきます。

 <common:LayoutAwarePage
    x:Name="pageRoot"
    x:Class="App6.GroupedItemsPage"
    DataContext="{Binding DefaultViewModel, RelativeSource={RelativeSource Self}}"
    xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:App6"
    xmlns:data="using:App6.Data"
    xmlns:common="using:App6.Common"
    xmlns:d="https://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="https://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d">

    <Page.Resources>

        <!--
            このページで表示されるグループ化されたアイテムのコレクションは、グループ内のアイテムを
            仮想化できないため、完全なアイテム リストのサブセットにバインドされます
        -->
        <CollectionViewSource
            x:Name="groupedItemsViewSource"
            Source="{Binding Groups}"
            IsSourceGrouped="true"
            ItemsPath="TopItems"
            d:Source="{Binding AllGroups, Source={d:DesignInstance Type=data:SampleDataSource, IsDesignTimeCreatable=True}}"/>
    </Page.Resources>

この定義で注目したいのが、「DataContext="{Binding DefaultViewModel, RelativeSource={RelativeSource Self}}" 」 です。ここで指定されているDefaultViewModel が何処で定義されているかといえば、LayoutAwarePage クラスになります。その定義を以下に示します。

 /// <summary>
/// <see cref="DefaultViewModel"/> 依存関係プロパティを識別します。
/// </summary>
public static readonly DependencyProperty DefaultViewModelProperty =
         DependencyProperty.Register("DefaultViewModel", 
           typeof(IObservableMap<String, Object>),
           typeof(LayoutAwarePage), null);



/// <summary>
/// <see cref="IObservableMap<String, Object>"/> の実装です。これは、
/// 単純なビュー モデルとして使用されるように設計されています。
/// </summary>
protected IObservableMap<String, Object> DefaultViewModel
{
    get
    {
       return this.GetValue(DefaultViewModelProperty)
                 as IObservableMap<String, Object>;
    }

    set
    {
       this.SetValue(DefaultViewModelProperty, value);
    }
}

この定義をみれば理解できるように、DefaultViewModelは依存プロパティとして、IObservableMap<string, Object> 型として定義されています。そして、IObservableMap<string, Object>の実装として ObservableDictionary<K, V>クラスを定義しています。この型を簡単に説明するとすれば、ディクショナリー型と同じであり、キーに文字列でデータソースの名前を指定して、値にオブジェクトのコレクションを設定して使用します。DefaultViewModelが、Dictionary<string, object>を使って実装していない理由は、ディクショナリーに変更があった場合の変更通知にあります。変更通知を実装するために、 IObservableMap<string, Object>を継承してObservableDictionary<K, V>クラス を定義しています。GroupedItemPage.xamlのCollectionViewSource定義のSource属性に「 {Binding Groups}」 と定義されていたことを思い出してください。ここで定義されている「Groups」と文字列が、ObservableDictionary<K, V>クラスのキーになっています。このことから、理解できることは以下のようになります。

  • DataContextで定義しているDefaultViewModel は、依存プロパティであり、LayoutAwarePageクラスで定義している。
  • CollectionViewSourceのSource属性に、IObservableMap<string, Object>型のキーを指定する。
  • ページ定義XAMLのコードビハインドで、DefaultViewModel ["キー"] に対してオブジェクトコレクションを設定する必要がある

このように理解することができれば、次にLayoutAwarePageクラスのOnNavigatedToメソッドを読み解いてみましょう。このメソッドは、Frameを使ったナビゲーションによって呼び出されるメソッドになります。

 /// <summary>
/// このページがフレームに表示されるときに呼び出されます。
/// </summary>
/// <param name="e">このページにどのように到達したかを説明するイベント データ。Parameter 
/// プロパティは、表示するグループを示します。</param>
protected override void OnNavigatedTo(NavigationEventArgs e)
{
    // ナビゲーションを通じてキャッシュ ページに戻るときに、状態の読み込みが発生しないようにします
    if (this._pageKey != null) return;
    var frameState = SuspensionManager.SessionStateForFrame(this.Frame);
    this._pageKey = "Page-" + this.Frame.BackStackDepth;

    if (e.NavigationMode == NavigationMode.New)
    {
        // 新しいページをナビゲーション スタックに追加するとき、次に進むナビゲーションの
        // 既存の状態をクリアします
        var nextPageKey = this._pageKey;
        int nextPageIndex = this.Frame.BackStackDepth;
        while (frameState.Remove(nextPageKey))
        {
            nextPageIndex++;
            nextPageKey = "Page-" + nextPageIndex;
        }
        // ナビゲーション パラメーターを新しいページに渡します
        this.LoadState(e.Parameter, null);
    }
    else
    {
        // ナビゲーション パラメーターおよび保存されたページの状態をページに渡します。
        // このとき、中断状態の読み込みや、キャッシュから破棄されたページの再作成と同じ対策を
        // 使用します
        this.LoadState(e.Parameter, 
            (Dictionary<String, Object>)frameState[this._pageKey]);
    }
}

/// <summary>
/// このページには、移動中に渡されるコンテンツを設定します。前のセッションからページを
/// 再作成する場合は、保存状態も指定されます。
/// </summary>
/// <param name="navigationParameter">このページが最初に要求されたときに
/// <see cref="Frame.Navigate(Type, Object)"/> に渡されたパラメーター値。
/// </param>
/// <param name="pageState">前のセッションでこのページによって保存された状態の
/// ディクショナリ。ページに初めてアクセスするとき、状態は null になります。</param>
protected virtual void LoadState(Object navigationParameter, 
                        Dictionary<String, Object> pageState)
{
}

OnNavigatedToメソッドで重要なことは、ページの状態をチェックすることとLoadStateメソッドを呼び出すことです。LoadStateメソッドの定義を見ると仮想メソッドとして定義されています。よって、LayoutAwarePageクラスを継承したクラスで実装する必要があります。GroupedItemPage.xaml.csのLoadStateメソッドを以下に示します。

 /// <summary>
/// このページには、移動中に渡されるコンテンツを設定します。前のセッションからページを
/// 再作成する場合は、保存状態も指定されます。
/// </summary>
/// <param name="navigationParameter">このページが最初に要求されたときに
/// <see cref="Frame.Navigate(Type, Object)"/> に渡されたパラメーター値。
/// </param>
/// <param name="pageState">前のセッションでこのページによって保存された状態の
/// ディクショナリ。ページに初めてアクセスするとき、状態は null になります。</param>
protected override void LoadState(Object navigationParameter, 
                        Dictionary<String, Object> pageState)
{
    // TODO: 問題のドメインでサンプル データを置き換えるのに適したデータ モデルを作成します
    var sampleDataGroups = SampleDataSource.GetGroups((String)navigationParameter);
    this.DefaultViewModel["Groups"] = sampleDataGroups;
}

このコードでは、SampleDataSource.GetGroupsメソッドを使ってグループ コレクションを取得してから、DefaultViewModelにキーと値を設定することで、Groupsというキーのデータソースを設定していることを理解することができます。

  • LoadStateメソッドは、目的のページにおけるデータソースを作成する責務を持つ
  • OnNavigatedToメソッドをオーバーライドする場合は、LayoutAwarePageクラスのOnNavigatedToメソッドを呼び出すか、同等の処理を記述しなければならない

ここまでで、GroupedItemsPageにおけるXAMLとコードを使ったデータソースの設定に関することを説明しました。まだまだ、説明しきれていないことがありますので、その辺りは次回に説明する予定です。