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

前回にGroupDetailPageの説明を行いました。今回は、最後のItemDetailPageの説明を行います。最初に、ItemDetailPageがナビゲーションを使って呼び出されることから、LoadStateメソッドを以下に示します。

 protected override void LoadState(Object navigationParameter, 
                          Dictionary<String, Object> pageState)
{
    // 保存されたページの状態で、表示する最初のアイテムをオーバーライドすることを許可します
    if (pageState != null && pageState.ContainsKey("SelectedItem"))
    {
        navigationParameter = pageState["SelectedItem"];
    }

    // TODO: 問題のドメインでサンプル データを置き換えるのに適したデータ モデルを作成します
    var item = SampleDataSource.GetItem((String)navigationParameter);
    this.DefaultViewModel["Group"] = item.Group;
    this.DefaultViewModel["Items"] = item.Group.Items;
    this.flipView.SelectedItem = item;
}

navigationParameterに、SampleDataItem.UniqueIdが格納されていて、SampleDataSourceの静的メソッドであるGetItemメソッドを呼び出しています。このメソッドにより、目的のSampleDataItemのインスタンスを取得して、DefaultViewModelのGroupキーとItemsキー、FilvepViewのSelectedItemを設定しています。CollectionViewSourceの定義は、GroupDetailPage.xamlと同一になっており、レイアウトのルートとなるGridに対するDataContextやd:DataContextもGroupDetailPage.xamlと同-です。 異なるのは、詳細情報をFlipViewコントロールで行っていることです。
FlipView

FlipViewコントロールは、コレクションの要素を右端と左端の矢印によってナビゲーションすることができます。従って、FlipViewコントロールを使うのはコレクションの要素を列挙するような用途に向いているということになります。それでは、ItemDetailPage.xamlのFlipViewの定義を以下に示します。

 <!--
    このページの残りは 1 つの大きな FlipView です。ここには、一度に
     1 つのアイテムの詳細が表示され、ユーザーは選択されたグループ内のすべてのアイテムを見ることが
    できます
-->
<FlipView
    x:Name="flipView"
    AutomationProperties.AutomationId="ItemsFlipView"
    AutomationProperties.Name="Item Details"
    TabIndex="1"
    Grid.RowSpan="2"
    ItemsSource="{Binding 
           Source={StaticResource itemsViewSource}}">
    <FlipView.ItemContainerStyle>
        <Style TargetType="FlipViewItem">
            <Setter Property="Margin" Value="0,137,0,0"/>
        </Style>
    </FlipView.ItemContainerStyle>
    <FlipView.ItemTemplate>
        <DataTemplate>
            <!--
                表示状態管理をサポートしているため、テンプレート化されたアイテムとして選択された UserControl
                読み込まれた/アンロードされたイベントが、ページからのビューステートの更新を明示的に定期受信します
            -->
            <UserControl 
                  Loaded="StartLayoutUpdates" 
                  Unloaded="StopLayoutUpdates">
                <ScrollViewer x:Name="scrollViewer" 
                     Style='{StaticResource HorizontalScrollViewerStyle}'
                     Grid.Row='1'>
                    <!-- コンテンツは、必要な数の列をフローできます -->
                    <common:RichTextColumns x:Name='richTextColumns'
                            Margin='117,0,117,47'>
                        <RichTextBlock x:Name='richTextBlock'
                             Width='560' 
                             Style='{StaticResource ItemRichTextStyle}' 
                             IsTextSelectionEnabled='False'>
                            <Paragraph>
                                <Run 
                                   FontSize='26.667' 
                                   FontWeight='Light' 
                                   Text='{Binding Title}'/>
                                <LineBreak/>
                                <LineBreak/>
                                <Run 
                                   FontWeight='Normal' 
                                   Text='{Binding Subtitle}'/>
                            </Paragraph>
                            <Paragraph LineStackingStrategy='MaxHeight'>
                                <InlineUIContainer>
                                    <Image x:Name='image' 
                                        MaxHeight='480' Margin='0,20,0,10' 
                                        Stretch='Uniform' 
                                        Source='{Binding Image}' 
                                        AutomationProperties.Name='{Binding Title}'/>
                                </InlineUIContainer>
                            </Paragraph>
                        <!-- 以下省略 -->
                    </common:RichTextColumns>

                    <VisualStateManager.VisualStateGroups>
                        <!-- 表示状態には、FlipView 内のアプリケーションのビューステートが反映されます -->
                        <VisualStateGroup x:Name='ApplicationViewStates'>
                            <VisualState x:Name='FullScreenLandscape'/>
                            <VisualState x:Name='Filled'/>
                            <!-- 縦方向に対して、より狭い 100 ピクセルの余白の規則を優先します -->
                            <VisualState x:Name='FullScreenPortrait'>
                                <Storyboard>
                                    <!-- 以下省略 -->
                                </Storyboard>
                            </VisualState>

                            <!-- スナップの場合、コンテンツは再フォーマットされ、垂直にスクロールします -->
                            <VisualState x:Name='Snapped'>
                                <Storyboard>
                                    <ObjectAnimationUsingKeyFrames 
                                      Storyboard.TargetName='richTextColumns' 
                                      Storyboard.TargetProperty='Margin'>
                                        <DiscreteObjectKeyFrame KeyTime='0' 
                                                   Value='17,0,17,57'/>
                                    </ObjectAnimationUsingKeyFrames>
                                    <ObjectAnimationUsingKeyFrames 
                                        Storyboard.TargetName='scrollViewer' 
                                        Storyboard.TargetProperty='Style'>
                                        <DiscreteObjectKeyFrame KeyTime='0' 
                                           Value='{StaticResource VerticalScrollViewerStyle}'/>
                                    </ObjectAnimationUsingKeyFrames>
                                    <!-- 以下省略 -->
                                </Storyboard>
                            </VisualState>
                        </VisualStateGroup>
                    </VisualStateManager.VisualStateGroups>
                </ScrollViewer>
            </UserControl>
        </DataTemplate>
    </FlipView.ItemTemplate>
</FlipView>

 
FlipViewのItemSource属性に、CollectionViewSourceである ItemViewSourceを指定しています(データバインディンの設定)。次に説明することは、FlipView.ItemTemplateの定義になります。

  • UserControl:ここでのポイントは、LoadedとUnloadedイベントにLayoutAwarePageのStartLayoutUpdates、StopLayoutUpdatesイベントハンドラーを指定していることです。既に説明したように、LayoutAwarePage.StartLayoutUpdatesイベントハンドラーはControlオブジェクトに対してVisualStateManagerを使ってビューの切り替えを行います。つまり、ItemTemplateに対してビューの切り替えを行いたいためにItemTemplateのルート要素としてUserControlを定義しているのです。
  • ScrollViewer:水平スクロールビューアー スタイルが指定されていることに注意してください。フルなどのビューにおいて、水平スクロールバーを表示してコンテンツをスクロールさせます。
  • common:RichTextColumns:これはカスタムコントロールで、Common\RichTextColumn.csで定義されています。イメージ的には、リッチテキストボックスに近いコントロールですが、異なるのは追加の列をColumnTemplateで生成できることでしょう。
  • VisualStateManager:UserControlに対するVisualStateManagerの定義で、ポートレイトとスナップ時の定義を行っています。細かな定義を読めば理解できますが、単純に説明すればMaginやWidth、Heightなどをビューによって設定しています。

  そしてItemDetailPageに対するVisualStateManagerの定義が行われています。この定義は、ポートレイトとスナップにおけるタイトルなどの設定で、これまでに説明してきたGroupedItemPage.xamlやGroupDetailPage.xamlと同じものとなります。

ItemDetailPage固有のロジックとしては、SaveSateメソッドがあります。

 /// <summary>
/// アプリケーションが中断される場合、またはページがナビゲーション キャッシュから破棄される場合、
/// このページに関連付けられた状態を保存します。値は、
/// <see cref="SuspensionManager.SessionState"/> のシリアル化の要件に準拠する必要があります。
/// </summary>
/// <param name="pageState">シリアル化可能な状態で作成される空のディクショナリ。</param>
protected override void SaveState(Dictionary<String, Object> pageState)
{
    var selectedItem = (SampleDataItem)this.flipView.SelectedItem;
    pageState["SelectedItem"] = selectedItem.UniqueId;
}

     
SaveSateメソッドのコメントで、何を行うものかは理解できることでしょう。問題は、どのタイミングで呼び出されるのかという点になります。それでは、LayoutAwarePage.OnNavigatedFromメソッドを以下に示します。

 /// <summary>
/// このページがフレームに表示されなくなるときに呼び出されます。
/// </summary>
/// <param name="e">このページにどのように到達したかを説明するイベント データ。Parameter 
/// プロパティは、表示するグループを示します。</param>
protected override void OnNavigatedFrom(NavigationEventArgs e)
{
    var frameState = SuspensionManager.SessionStateForFrame(this.Frame);
    var pageState = new Dictionary<String, Object>();
    this.SaveState(pageState);
    frameState[_pageKey] = pageState;
}

フレームが表示されなくなるタイミングでOnNavigateFromイベントハンドラーが呼びされます。このコードでSuspensionManagerのSuspensionStateforFrameメソッドが呼び出されていることに注意してください。簡単に説明するなら、SusupensionManagerを使ってこのページへナビゲートしてきた状態を保存するということです。この機能が定義されていて、App.xaml.csのOnSuspendingイベントハンドラーを見てみましょう。

 private async void OnSuspending(object sender, SuspendingEventArgs e)
{
    var deferral = e.SuspendingOperation.GetDeferral();
    await SuspensionManager.SaveAsync();
    deferral.Complete();
}

OnSupendingイベントハンドラーで、「SuspensionManager.SaveAsync() 」と記述して、Frameオブジェクトによる状態を保存しています。このメソッドで、pageState["SelectedItem"] に設定したナビゲーション パラメーターも一緒に保存されます。つまり、Windowsストアアプリにおける一時停止とFrameオブジェクトのイベントハンドラーは以下のような関係にあります。

  1. Frame.OnNavigateFromイベントハンドラーが発生する。
    Frameが表示されなくなる
  2. OnSuspendingイベントハンドラーが発生する。

このような動きをすることで、Windows ストアアプリケーション固有の動きである、「一時停止から強制終了」、次に起動したタイミングで「状態を復旧」(最後に表示していあたページへ復帰させる)を実現しています。また、SampleDataSourceの説明で行ったようにSampleDataGroupとSampleDataItemは循環参照を持っています。この循環参照持っていることで、LoadStateメソッドに記述されている「this.DefaultViewModel["Group"] = item.Group」が成り立つようになっています。
ここまで説明したなら、LayoutAwarePage.OnNavigatedToイベントハンドラーも解説すべきでしょう。

 /// <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]);
    }
}

OnNavigatedToイベントハンドラーでは、ナビゲーションに発生するナビゲーションパラメーターなどを設定したり、状態をクリアーしたりしています。このようにページの状態を保持する仕組みを用意していることで、GoBackイベントハンドラーで自由に戻るというナビゲーションを実現しています。

このようにグリッドアプリケーション テンプレートは、単純なテンプレートではありません。むしろ、Windows ストアアプリケーション開発が容易になるように作り込まれたサンプルであると説明した方が適切でしょう。 これが、グリッドアプリケーション テンプレートの解説を記述しようと考えたきっかけです。 このエントリーは、後1回位を予定しています。次回は、カスタマイズする場合のポイントを取り上げる予定です。