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

前回にLayoutAwarePage.StartLayoutUpdateメソッドが、Loadedイベントで呼び出されることを説明しました。このメソッドは、以下の定義になっていました。。

 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(); 
 }
 this._layoutAwareControls.Add(control);
  // コントロールの最初の表示状態を設定します 
 VisualStateManager.GoToState(control,
       DetermineVisualState(ApplicationView.Value), false); 
}

ここで気を付けておくべきことは、「 _layoutAwareControls」というメンバー変数です。この変数は「List<Control>」型になっており、LayoutAwarePageクラスがLoadedイベントにより登録されます。これは、LayoutAwarePageがPageクラスを継承しており、Pageクラスが UserControl を継承し、UserControlが Controlクラスから派生しているためです。この機能により、LayoutAwarePageクラスを継承したGroupedItemsPage.xamlで定義されたVisualStateManagerのVisualStateが呼び出されるようになっているのです。 この特徴を理解しておくことは、とても重要です。何故なら、ユーザーコントロールを自作して組み合わせる場合に、LoadedとUnLoadedイベントで「StartLayoutUpdatesメソッドとStopLayoutUpdatesメソッド」を呼び出すように設定することで、ユーザーコントロール内に記述したVisualStateManagerの定義が呼び出されるようになるからです。実際に、グリッドアプリケーションのItemDetailPage.xamlではUserControlを定義してLoadedとUnLoadedイベントハンドラーを設定しています。

GroupedItemsPageクラスで次に説明することは、データソースについてです。データソースの説明に入る前に、GroupedItemsPage.xamlのCollectionViewSourceの定義を以下に再掲します。

 <CollectionViewSource
    x:Name="groupedItemsViewSource"
    Source="{Binding Groups}"
    IsSourceGrouped="true"
    ItemsPath="TopItems"
    d:Source=
       "{Binding AllGroups, 
         Source={d:DesignInstance Type=data:SampleDataSource, 
         IsDesignTimeCreatable=True}}"/>

d:Source」属性は、デザイン時のデータソースを指定しています(コード上は意図的に改行しています)。ここに指定されていることの意味は、以下のようになります。

  • Binding:AllGroupsというプロパティ名を指定しています。
  • Source:デザイン時のデータソースを指定しています。
    d:DesignInstance で、型を「SampleDataSource」クラスと指定し、IsDesignTimeCreatableをtrueと指定することで、デザイン時にデータソースのインスタンスを作成することを指示しています。

このことから理解できるのは、SampleDataSource.AllGroupsプロパティが返すコレクションをカスタマイズすることで、デザイン時に表示されるサンプルデータを変更することができるということです。それでは、SampleDataSourceクラスの構造を以下に示します。
SampleDataSource

d:Source属性で説明したように、SampleDataSourceクラスがAllGroupsプロパティを持っていることを確認することができます。また、ObservableCollection<SampleDataGroup>を返すこともクラス図から把握することができます。それでは、SampleDataSourceクラスのコードを以下に示します。

 /// <summary>
/// ハードコーディングされたコンテンツを使用して、グループおよびアイテムのコレクションを作成します。
/// 
/// SampleDataSource はライブ プロダクション データではなくプレースホルダー データを使用して初期化するので
/// サンプル データは設計時と実行時の両方に用意されています。
/// </summary>
public sealed class SampleDataSource
{
    private static SampleDataSource _sampleDataSource = new SampleDataSource();

    private ObservableCollection<SampleDataGroup> _allGroups = 
             new ObservableCollection<SampleDataGroup>();
    public ObservableCollection<SampleDataGroup> AllGroups
    {
        get { return this._allGroups; }
    }

    public static IEnumerable<SampleDataGroup> GetGroups(string uniqueId)
    {
        if (!uniqueId.Equals("AllGroups")) throw 
             new ArgumentException("Only 'AllGroups' is supported as a collection of groups");
        
        return _sampleDataSource.AllGroups;
    }

    public static SampleDataGroup GetGroup(string uniqueId)
    {
        // サイズの小さいデータ セットでは単純な一方向の検索を実行できます
        var matches = _sampleDataSource.AllGroups.
                     Where((group) => group.UniqueId.Equals(uniqueId));
        if (matches.Count() == 1) return matches.First();
        return null;
    }

    public static SampleDataItem GetItem(string uniqueId)
    {
        // サイズの小さいデータ セットでは単純な一方向の検索を実行できます
        var matches = _sampleDataSource.AllGroups.
                      SelectMany(group => group.Items).
                      Where((item) => item.UniqueId.Equals(uniqueId));
        if (matches.Count() == 1) return matches.First();
        return null;
    }

    public SampleDataSource()
    {
        String ITEM_CONTENT = 
                String.Format(
                 "Item Content: {0}\n\n{0}\n\n{0}\n\n{0}\n\n{0}\n\n{0}\n\n{0}",
                    "コンテンツ文字列");

        var group1 = new SampleDataGroup("Group-1",
                "Group Title: 1",
                "Group Subtitle: 1",
                "Assets/DarkGray.png",
                "Group Description: グループ説明");
        group1.Items.Add(new SampleDataItem("Group-1-Item-1",
                "Item Title: 1",
                "Item Subtitle: 1",
                "Assets/LightGray.png",
                "Item Description: アイテム説明",
                ITEM_CONTENT,
                group1));
        // 以下省略
        this.AllGroups.Add(group1);
        // 以下省略
    }
}

コードを見れば理解が進むと思いますが、コンストラクタ内でサンプルデータを作成して、AllGroupsプロパティを使ってサンプルデータを追加しています。これが、ObservableCollection<SampleDataGroup>コレクションへの追加となっているのです。これが、d:Source属性でデザイン時のデータソースを指定していることの意味になります。また、SampleDataSourceクラスが提供する静的メソッドには、以下のものがあります。

  • GetGroups:引数は"AllGroups"のみで、全てのコレクションを取得します。
  • GetGroup:引数はグループのユニークIDとなり、1つのグループを取得します。
    matches.Count() == 1」は間違いで、正しくは「matches.Count() >= 1」となります。
  • GetItem:引数はアイテムのユニークIDとなり、1つのアイテムを取得します。
    matches.Count() == 1」は間違いで、正しくは「matches.Count() >= 1」となります。

静的メソッドは、_sampleDataSourceという静的なメンバー変数を使ってSampleDataSourceクラスのインスタンスへアクセスしています。現実的なアプリに改造するには、特に以下の点に注意します。

  • コンストラクタで作成しているサンプルデータをWindows.ApplicationModel.DesignMode.DesignModeEnsbled が trueの場合だけに作成するようにします。
  • アプローチは色々とありますが、実際のデータを読み込むメソッドを追加します。この時に、読み込みが終了したかどうかを判定するために「IsInitialized」などのプロパティを追加した方が良いでしょう。

開発体験テンプレートのNewsReaderでは、説明した意図でIsInitializedプロパティとLoadRemoteDataAsyncメソッドを用意しています。SampleDataGroupは、Itemsプロパティ(ObservableCollection<SampleDataItem>)を持っています。これが、グループが持つアイテムのコレクションとなっています。また、クラス図から理解できるように、SampleDataGroupクラスとSampleDataItemクラスは、SampleDataCommonクラスを継承しており、SampleDataCommonクラスはBindableBaseクラスを継承しています。BindableBaseクラスは、「INotifyPropertyChanged」インターフェースを実装しています。またクラス図から理解できますが、ObservableCollectionもINotifyPropertyChangedインターフェースを実装しています。XAMLとのデータバインディングを行う上で、INotifyPropertyChangedインターフェースを実装していることが重要なポイントなります。この理由は、一回限りのバインディング(OneTime)だけではなく、一方向(OneWay、これがデフォルト)、双方向(TwoWay)バインディングを実現するために必須となるからです。データバインドしたコレクションのメンバーに対して、データを変更したり、メンバーの追加や削除を行う場合に、XAMLに対して変更通知を行うことでビュー(XAMLの視覚要素で、表示コントロール)が更新されるようになるのです。この変更通知を実現するのが、 INotifyPropertyChangedインターフェースを実装するという意味になります。これが、MVVMパターンにおけるビューモデル(VM)の特徴になっています。

SampleDataGroup、SampleDataItemの関係は、循環参照となっています。具体的には、SampleDataItemのプロパティにGroupが定義されており、これがSampleDataGroupのインスタンスを示すようになっています。このような構造になっている理由は、ItemDetailPage.xamlからGroupDetailPage.xamlへのナビゲーションを実現するためです。データソースを現実のアプリに合わせて改造するには、SampleDataGroup、SampleDataItem、SampleDataCommonクラスなどに対して、メンバーの追加や変更を行うことが多いことでしょう。この場合の行うべき定石のコードを以下にしめします。

         private string _description = string.Empty;
        public string Description
        {
            get { return this._description; }
            set { this.SetProperty(ref this._description, value); }
        }

プロパティ セッターで、必ず「 this.SetProperty(ref メンバー変数, value) 」を記述します。SetPropertyメソッドは、BindableBaseクラスで定義されており、OnPropertyChangedメソッドを使ってプロパティの変更イベントを呼び出します。この変更通知が行われることで、XAMLの視覚要素へ変更通知が送られるようになります。ですから、SetPropertyメソッドをセッターに記述することが重要なのです。また、SampleDataCommonクラスは、Imageプロパティを持っていますが、永続化を考えるとImageSourceのインスタンスをシリアライズすることはできません。よって、SetImage(イメージファイルへのパス)メソッドをデシリアイズでは呼び出すようにします。もしくは、ImagePathプロパティを追加して、セッターでImageプロパティに対してOnPropertyChangedを呼び出すようにしても良いでしょう。