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

前回は、GroupedItemsPageのLoadStateメソッドまでを説明しました。今回は、GroupedItemsPageのビューに関して説明します。ビューとしては、前回に説明したようにGridViewコントロールとListViewコントロールの2つを用意しています。最初に、GridViewコントロールの定義とデザイン画面を以下に示します。

 <!-- ほとんどのビューステートで使用される水平スクロール グリッド-->
<GridView
    x:Name="itemGridView"
    AutomationProperties.AutomationId="ItemGridView"
    AutomationProperties.Name="Grouped Items"
    Grid.RowSpan="2"
    Padding="116,137,40,46"
    ItemsSource="{Binding Source={StaticResource groupedItemsViewSource}}"
    ItemTemplate="{StaticResource Standard250x250ItemTemplate}"
    SelectionMode="None"
    IsSwipeEnabled="false"
    IsItemClickEnabled="True"
    ItemClick="ItemView_ItemClick">

    <GridView.ItemsPanel>
        <ItemsPanelTemplate>                        
            <VirtualizingStackPanel Orientation="Horizontal"/>
            </ItemsPanelTemplate>
    </GridView.ItemsPanel>
    <GridView.GroupStyle>
        <GroupStyle>
            <GroupStyle.HeaderTemplate>
                <DataTemplate>
                   <Grid Margin="1,0,0,6">
                        <Button
                            AutomationProperties.Name="Group Title"
                            Click="Header_Click"
                            Style='{StaticResource TextPrimaryButtonStyle}' >
                        <!-- 以下 省略 -->
                    </Grid>
                </DataTemplate>
            </GroupStyle.HeaderTemplate>
            <GroupStyle.Panel>
                <ItemsPanelTemplate>
                    <VariableSizedWrapGrid Orientation='Vertical' Margin='0,0,80,0'/>
                </ItemsPanelTemplate>
            </GroupStyle.Panel>
        </GroupStyle>
    </GridView.GroupStyle>
</GridView>

Grid Landscape

GridViewの定義で注目するのが、ItemsSourceとItemsTemplate属性の定義になります。

  • ItemSource : CollectionViewSourceの名前であるgroupedItemsViewSource をデータソースとして指定しています。
  • ItemTemplate : StandardStyle.xamlで定義されているデータテンプレートである Standard250x250ItemTemplate を指定しています

今度は、ListViewコントロールの定義とデザインを以下に示します。

 <!-- スナップの場合のみ使用される垂直スクロール リスト -->
<ListView
    x:Name="itemListView"
    AutomationProperties.AutomationId="ItemListView"
    AutomationProperties.Name="Grouped Items"
    Grid.Row="1"
    Visibility="Collapsed"
    Margin="0,-10,0,0"
    Padding="10,0,0,60"
    ItemsSource="{Binding Source={StaticResource groupedItemsViewSource}}"
    ItemTemplate="{StaticResource Standard80ItemTemplate}"
    SelectionMode="None"
    IsSwipeEnabled="false"
    IsItemClickEnabled="True"
    ItemClick="ItemView_ItemClick">

    <ListView.GroupStyle>
        <GroupStyle>
            <GroupStyle.HeaderTemplate>
                <DataTemplate>
                    <Grid Margin="7,7,0,0">
                        <Button
                            AutomationProperties.Name="Group Title"
                            Click="Header_Click"
                            Style='{StaticResource TextPrimaryButtonStyle}'>
                            <StackPanel Orientation='Horizontal'>
                                <TextBlock Text='{Binding Title}' Margin='3,-7,10,10' Style='{StaticResource GroupHeaderTextStyle}' />
                                <!-- 以下省略 --/>
                            </StackPanel>
                        </Button>
                    </Grid>
                </DataTemplate>
            </GroupStyle.HeaderTemplate>
        </GroupStyle>
    </ListView.GroupStyle>
</ListView>

GridSnapped 

デザインを見れば理解できるようにListViewコントロールは、スナップビューとして使用するものになっています。従って、ListViewの定義で注目するのは、ItemSourceとItemTemplateとVisibility属性になります。

  • Visibility :Collapsedを指定して非表示にしています。
  • ItemSource :GridViewと同じgroupedItemsViewSource をデータソースとして指定しています
  • ItemTemplate :StandardStyle.xamlで定義されているデータテンプレートである Standard80ItemTemplate を指定しています

GridViewコントロールとListViewコントロールに対して同じデータソースを指定して、ランドスケープとスナップにおいて表示・非表示を切り替えることで2つのレイアウトに対応しています。Visual Studio 2012 と Blend for Visual Studio のデバイスタブを使って、ランドスケープ、スナップなどを切り替える場合に重要な役割を持っているのが、VisualStateManagerになります。

 <VisualStateManager.VisualStateGroups>
    <!--表示状態には、アプリケーションのビューステートが反映されます -->
    <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="itemListView" 
                        Storyboard.TargetProperty="Visibility">
                    <DiscreteObjectKeyFrame KeyTime="0" 
                        Value="Visible"/>
                </ObjectAnimationUsingKeyFrames>
                <ObjectAnimationUsingKeyFrames 
                        Storyboard.TargetName="itemGridView" 
                        Storyboard.TargetProperty="Visibility">
                    <DiscreteObjectKeyFrame KeyTime="0" 
                        Value="Collapsed"/>
                </ObjectAnimationUsingKeyFrames>
            </Storyboard>
        </VisualState>
    </VisualStateGroup>
</VisualStateManager.VisualStateGroups>

VisualState の Snappedの定義をみれば、Visibilityを切り替えていることは明らかです。これらの表示状態に関わらずに一貫したデータを表示しているのが、同じデータソースにデータバインドして いる効果になります。実は、VisualStateManagerは Visual Studio 2012やBlend for Visual Studioのデザイナーがデバイスタブで使用しており、実行時にはユーザーコードでXAMLに定義した情報を呼び出す必要があります。この呼び出しを行っているのが、LayoutAwarePageクラスになります。LayoutAwarePageクラスのコンストラクタを以下に示します。

 /// <summary>
/// <see cref="LayoutAwarePage"/> クラスの新しいインスタンスを初期化します。
/// </summary>
public LayoutAwarePage()
{
    if (Windows.ApplicationModel.DesignMode.DesignModeEnabled) return;

    // 空の既定のビュー モデルを作成します
    this.DefaultViewModel = new ObservableDictionary<String, Object>();

    // このページがビジュアル ツリーの一部である場合、次の 2 つの変更を行います:
    // 1) アプリケーションのビューステートをページの表示状態にマップする
    // 2) キーボードおよびマウスのナビゲーション要求を処理する
    this.Loaded += (sender, e) =>
    {
        this.StartLayoutUpdates(sender, e);

        // キーボードおよびマウスのナビゲーションは、ウィンドウ全体を使用する場合のみ適用されます
        if (this.ActualHeight == Window.Current.Bounds.Height &&
            this.ActualWidth == Window.Current.Bounds.Width)
        {
            // ウィンドウで直接待機するため、フォーカスは不要です
            Window.Current.CoreWindow.Dispatcher.AcceleratorKeyActivated +=
                CoreDispatcher_AcceleratorKeyActivated;
            Window.Current.CoreWindow.PointerPressed +=
                this.CoreWindow_PointerPressed;
        }
    };

    // ページが表示されない場合、同じ変更を元に戻します
    this.Unloaded += (sender, e) =>
    {
        this.StopLayoutUpdates(sender, e);
        Window.Current.CoreWindow.Dispatcher.AcceleratorKeyActivated -=
            CoreDispatcher_AcceleratorKeyActivated;
        Window.Current.CoreWindow.PointerPressed -=
            this.CoreWindow_PointerPressed;
    };
}

コンストラクタで Loadedイベントハンドラで StartLayoutUpdates メソッドを呼び出しているのが確認できます。StartLayoutUpdatesメソッドを以下に示します。

 /// <summary>
/// イベント ハンドラーとして呼び出されます。これは通常、ページ内の <see cref="Control"/> の
/// <see cref="FrameworkElement.Loaded"/> イベントで呼び出され、送信元が
/// アプリケーションのビューステートの変更に対応する表示状態管理の変更を受信開始する必要があることを
/// 示します。
/// </summary>
/// <param name="sender">ビューステートに対応する表示状態管理をサポートする 
/// <see cref="Control"/> のインスタンス。</param>
/// <param name="e">要求がどのように行われたかを説明するイベント データ。</param>
/// <remarks>現在のビューステートは、レイアウトの更新が要求されると、
/// 対応する表示状態を設定するためすぐに使用されます。対応する 
/// <see cref="FrameworkElement.Unloaded"/> イベント ハンドラーを
/// <see cref="StopLayoutUpdates"/> に接続しておくことを強くお勧めします。
/// <see cref="LayoutAwarePage"/> のインスタンスは、Loaded および Unloaded イベントでこれらのハンドラーを自動的に
/// 呼び出します。</remarks>
/// <seealso cref="DetermineVisualState"/>
/// <seealso cref="InvalidateVisualState"/>
public void StartLayoutUpdates(object sender, RoutedEventArgs e)
{
    var control = sender as Control;
    if (control == null) return;
    if (this._layoutAwareControls == null)
    {
        // 更新の対象となるコントロールがある場合、ビューステートの変更の待機を開始します
        Window.Current.SizeChanged += this.WindowSizeChanged;
        this._layoutAwareControls = new List<Control>();
    }
    this._layoutAwareControls.Add(control);

    // コントロールの最初の表示状態を設定します
    VisualStateManager.GoToState(
           control, 
           DetermineVisualState(ApplicationView.Value), 
           false);
}

private void WindowSizeChanged(object sender, WindowSizeChangedEventArgs e)
{
    this.InvalidateVisualState();
}


/// <summary>
/// 適切な表示状態を使用した表示状態の変更を待機しているすべてのコントロールを更新し
/// ます。
/// </summary>
/// <remarks>
/// 通常、ビューステートが変更されていない場合でも、別の値が返される可能性がある事を知らせるために
/// <see cref="DetermineVisualState"/> をオーバーライドすることで
/// 使用されます。
/// </remarks>
public void InvalidateVisualState()
{
    if (this._layoutAwareControls != null)
    {
        string visualState = DetermineVisualState(ApplicationView.Value);
        foreach (var layoutAwareControl in this._layoutAwareControls)
        {
            VisualStateManager.GoToState(
                   layoutAwareControl, visualState, false);
        }
    }
}

StartLayoutUpdatesメソッドを見ることで、VisualStateManager.GoToStateメソッドを呼び出しているのが、このメソッドとWindowSizeChangedイベントハンドラだということが理解できます。ApplicationViewStateが、現在のビューの状態(Snappedなど)を表しています。コメントにも記載されていますが、VisualStateManagerで切り替えるビューステートを変更したい場合は、DetermineVisualStateメソッドをオーバーライドして対象のビューステート名(VisualState のx:Name属性)を返すようにすれば良いということになります。ここまでの動きから、VisualStateManagerの動作をまとめると、以下のようになります。

  • デバイスタブは、XAML内に定義されたVisualStateManagerを使用する
  • 実行時は、自分でVisualStateManager.GoToStateメソッドを呼び出す必要がある。
  • LayoutAwarePagaeクラスは、Window.SizeChangedイベントハンドラーでVisualStateManager.GoToStateメソッドを呼び出す 
    (ポートレイトやスナップなどに切り替えると、Window.SizeChangedイベントが発生します)

このような仕組みで表示状態を切り替えていることから、グリッドアプリケーション テンプレートで作成したプロジェクトに新しいページ(xaml)を追加する時に「必ずLayoutAwarePageを継承してください」という説明をしていました。 当然のこととして、ここまでのビューの切替などの仕組みを理解して、自分でビューの切替を実装するのであれば、LayoutAwarePageクラスを継承する必要はありません。実際には、継承することを強くお勧めします。この理由は、次回以降で説明します。

最後に、VisualStateMamangerを使用したコントロールのカスタマイズに関して、簡単に説明します。VisualStateManagerは、最初にSilverlightに導入されて、WPF 4.0でデスクトップ環境にも導入されました。WPF4.0などのドキュメントを読めば理解することができますが、コントロールの外観をマウスポインターが入った時などをxamlだけでカスタマイズすることができます。コントロールの外観をカスタマイズするには、ControlTemplateを定義して、VisualStateManagerを定義する必要があります。またControlTemplate属性とは、Controlが持つプロパティなので、Controlを継承したコントロール(Buttonなど)がVisualStateManagerを使ったノンプログラミングの外観カスタマイズを行うことができます。これ以外のコントロール(たとえば、Gridなどのレイアウト系コントロール)は、何らかのコードを書かないと外観カスタマイズを行うことはできませんので、注意する必要があります。