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クラスの構造を以下に示します。
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を呼び出すようにしても良いでしょう。