複雑なデータをバインドするには?[Win 8/WP 8]:WinRT/Metro TIPS
複雑なデータ・クラスをコントロールにバインドする方法を解説。また、バインドしたコレクションの変更を反映させる方法も説明する。
powered by Insider.NET
ここまでのTIPSで紹介してきたデータ・バインドの例では、stringオブジェクトなどの単純なオブジェクトやデータの1つのコレクションをバインドしていた。では、それらをいくつかまとめて持っているような複雑なデータ・クラス(以降、「複合データ」と呼ぶ)の場合はどうしたらよいだろうか?
そこで本稿では、複合データをバインドする方法を解説する。また、前回では割愛した、バインドしたコレクションの変更を反映させる方法も説明する。本稿のサンプルは「Windows Store app samples:MetroTips #37(Windows 8版)」と「Windows Store app samples:MetroTips #37(WP 8版)」からダウンロードできる。
なお、掲載しているコードは特記なき場合はWindowsストア・アプリとWindows Phone 8(以降、WP 8)アプリで共通である。
●事前準備
Windows 8(以降、Win 8)向けのWindowsストア・アプリを開発するには、Win 8とVisual Studio 2012(以降、VS 2012)が必要である。これらを準備するには、第1回のTIPSを参考にしてほしい。本稿では64bit版Win 8 ProとVS 2012 Express for Windows 8を使用している。
WP 8向けのアプリを開発するには、SLAT対応CPUを搭載したPC上の64bit版Win 8 Pro以上とWindows Phone SDK 8.0(無償)が必要となる。
●バインドするデータ
これからアプリに表示させたい複合データとして、2種類のデータをプロパティとして持つSampleCompositeDataクラスを作る(次のコード)。2種類のデータとは、「WinRT/Metro TIPS: 文字列以外の値をコントロールにバインドするには?[Win 8/WP 8]」で作ったClockクラスと、前回で作ったSampleDataCollectionクラスだ。これらのクラスを実装するコードは各回の解説を参照してほしい。
public class SampleCompositeData
{
private Clock _sampleClock = new Clock();
public Clock SampleClock { get { return _sampleClock; } }
private SampleDataCollection _sampleCollection
= new SampleDataCollection();
public SampleDataCollection SampleCollection
{ get { return _sampleCollection; } }
}
Public Class SampleCompositeData
Private _sampleClock As Clock = New Clock()
Public ReadOnly Property SampleClock As Clock
Get
Return _sampleClock
End Get
End Property
Private _sampleCollection As SampleDataCollection _
= New SampleDataCollection()
Public ReadOnly Property SampleCollection As SampleDataCollection
Get
Return _sampleCollection
End Get
End Property
End Class
●複合データをバインドするには?
コードビハインドで、複合データの各プロパティを個別にコントロールにバインドしても実現できるが、コーディングが面倒である。XAMLコードだけでバインドできないだろうか?
それには、複合データとして上記のSampleCompositeDataクラスのインスタンスをページのデータ・コンテキストに割り当てておいて、各コントロールではデータ・コンテキストにバインドするとよい。次の図のようなイメージだ。図および以下に示すXAMLコードでは、複合データのSampleClockプロパティ(=Clockクラスのオブジェクト)のNowTimeプロパティをTextBlockコントロールに、SampleCollectionプロパティ(=SampleDataCollectionクラスのオブジェクト)をListViewコントロールに、それぞれバインドしている。
複合データをページのデータ・コンテキストに割り当て、各コントロールはデータ・コンテキストとバインドする(Win 8)
※ WP 8でも、考え方は同様だ。
(1)複合データSampleCompositeDataクラスのインスタンスを、ページのDataContextプロパティにセットする。
(2)画面上のコントロールは、ページのDataContextプロパティとデータ・バインドする。
(3)DataContextプロパティとデータ・バインドするときのパス指定によって、TextプロパティにはClockクラスのNowTimeプロパティを、ItemsSourceプロパティにはSampleDataCollectionクラスのインスタンスを、個別にバインドすることができる。
これを実際に行うコードは、次のようになる。まず、複合データSampleCompositeDataクラスのインスタンスをXAMLコードだけで生成する。
<Page.Resources>
<local:SampleCompositeData x:Key="SampleCompositeData" />
…… 省略 ……
</Page.Resources>
なお、省略部分には、前回に示したListViewコントロール(WP 8ではLongListSelectorコントロール)のためのDataTemplate定義が必要だ。
※ WP 8では、「Page.Resources」タグが「phone:PhoneApplicationPage.Resources」タグになる。また、ページの開始タグに「xmlns:local」名前空間の定義を追加する。
この複合データのインスタンスを、次のようにしてページのDataContextに設定する。
<Page.DataContext>
<StaticResource ResourceKey="SampleCompositeData"/>
</Page.DataContext>
これは「Page.Resources」タグの後に記述する
※ページの開始タグ内にDataContext属性が存在していたら、あらかじめ削除しておく。
※ WP 8では、「Page.DataContext」タグが「phone:PhoneApplicationPage.DataContext」タグになる。
ページのDataContextプロパティは、ページ内のコントロールに伝播(でんぱ)するので、各コントロールのDataContextプロパティにもSampleCompositeDataのオブジェクトが自動的に設定されている。そこで、次のようにしてデータ・バインドを記述できる。
最初は、Win 8でのXAMLコードの例だ。
<StackPanel …… 省略 ……>
<StackPanel Orientation="Horizontal">
<TextBlock Text="現在日時" FontSize="42" />
<TextBlock Text="{Binding SampleClock.NowTime}"
FontSize="42" Foreground="Orange" Margin="10,0,0,0" />
</StackPanel>
<StackPanel Margin="0,40,0,0">
<TextBlock Text="SampleDataCollection" FontSize="42" />
<ListView ItemsSource="{Binding SampleCollection}"
ItemTemplate="{StaticResource DataTemplate1}"
Height="300" Width="1000" HorizontalAlignment="Left"
ScrollViewer.VerticalScrollMode="Enabled"
>
<ListView.ItemContainerStyle>
<Style TargetType="ListViewItem">
<Setter Property="Width" Value="980" />
<Setter Property="HorizontalAlignment" Value="Left" />
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
</Style>
</ListView.ItemContainerStyle>
</ListView>
</StackPanel>
</StackPanel>
・TextBlockコントロールのTextプロパティを、DataContextに設定されているSampleCompositeDataオブジェクトのSampleClockプロパティ(=Clockオブジェクト)のNowTimeプロパティとバインドした(太字の部分)
・ListViewコントロールのItemsSourceプロパティには、同じくSampleCompositeDataオブジェクトのSampleCollectionプロパティ(=SampleDataCollectionオブジェクト)をバインドした(太字の部分)
次は、WP 8でのXAMLコードの例だ。
<StackPanel>
<StackPanel>
<TextBlock Text="現在日時" FontSize="42" />
<TextBlock Text="{Binding SampleClock.NowTime}"
FontSize="42" Foreground="Orange" Margin="0,0,0,0" />
</StackPanel>
<StackPanel Margin="0,40,0,0">
<TextBlock Text="SampleDataCollection" FontSize="42" />
<phone:LongListSelector
ItemsSource="{Binding SampleCollection}"
ItemTemplate="{StaticResource DataTemplate1}"
Height="360" Width="480" HorizontalAlignment="Left"
…… 省略 …… >
</phone:LongListSelector>
</StackPanel>
</StackPanel>
・WP 8ではListViewコントロールがないので代わりにLongListSelectorコントロールを使う
・TextBlockコントロールのTextプロパティを、DataContextに設定されているSampleCompositeDataオブジェクトのSampleClockプロパティ(=Clockオブジェクト)のNowTimeプロパティとバインドした(太字の部分)
・LongListSelectorコントロールのItemsSourceプロパティには、同じくSampleCompositeDataオブジェクトのSampleCollectionプロパティ(=SampleDataCollectionオブジェクト)をバインドした(太字の部分)
以上で完了だ。全てをXAMLコードで行ったので、次の画像のようにデザイン画面でバインド結果が反映される。
また、アプリ実行時にListViewコントロールに表示するデータを設定するコードは次のようになる。
var composite = this.DataContext as SampleCompositeData;
var sampleCollection = composite.SampleCollection;
// listView1に表示するデータを作ってバインド
sampleCollection.Add(new SampleData()
{
Title = "Metroスタイル・アプリの開発者が知るべき3つのこと",
Url = "http://www.atmarkit.co.jp/fdotnet/chushin/readyforwin8app_01/readyforwin8app_01_01.html",
});
sampleCollection.Add(new SampleData()
{
Title = "デザイン・ガイドラインに従って画面を作成するには?[Win 8]",
Url = "http://www.atmarkit.co.jp/ait/articles/1208/23/news131.html",
});
sampleCollection.Add(new SampleData()
{
Title = "メニューの代わりにアプリ・バーを使うには?[Win 8]",
Url = "http://www.atmarkit.co.jp/ait/articles/1208/30/news149.html",
});
Dim composite = DirectCast(Me.DataContext, SampleCompositeData)
Dim sampleCollection = composite.SampleCollection
' listView1に表示するデータを作ってバインド
sampleCollection.Add(New SampleData() With
{
.Title = "Metroスタイル・アプリの開発者が知るべき3つのこと",
.Url = "http://www.atmarkit.co.jp/fdotnet/chushin/readyforwin8app_01/readyforwin8app_01_01.html"
})
sampleCollection.Add(New SampleData() With
{
.Title = "デザイン・ガイドラインに従って画面を作成するには?[Win 8]",
.Url = "http://www.atmarkit.co.jp/ait/articles/1208/23/news131.html"
})
sampleCollection.Add(New SampleData() With
{
.Title = "メニューの代わりにアプリ・バーを使うには?[Win 8]",
.Url = "http://www.atmarkit.co.jp/ait/articles/1208/30/news149.html"
})
コードを記述する場所は、Win 8ではLoadStateメソッド(LayoutAwarePageクラスを継承した場合)またはOnNavigatedToメソッド(継承しない場合)、WP 8ではコンストラクタの末尾である。
※この例のように、VBでもインスタンス化と同時に値をプロパティにセットできる(Visual Studio 2008から導入された「オブジェクト初期化子」)。
●コレクションの変更を反映させるには?
INotifyPropertyChangedインターフェイスとINotifyCollectionChangedインターフェイスを実装する。
実行中にSampleDataCollectionオブジェクトの内容を変更し、それを画面に反映させたいとする。例えば次のようにして、「SampleDataCollection」という文字列を表示しているTextBlockコントロールのTappedイベント(WP 8ではTapイベント)で、データを変更してみよう(現在日時を表示しているほうのTextBlockコントロールではないことに注意)。
まず、XAMLコードでイベント・ハンドラを指定する。
<TextBlock Text="SampleDataCollection" FontSize="42"
Tapped="TextBlock_Tapped" />
※ WP 8では、「Tapped」が「Tap」になる。
そのイベント・ハンドラのメソッドで、次のようにしてSampleDataCollectionオブジェクトの内容を変更する。変更したい複合データはページのデータ・コンテキストに保持されているので、それを取り出してSampleCompositeDataにキャストすればよい。例えば先頭のデータを書き換え、さらにコレクションの末尾に新しくデータを追加してみる(次のコード)。
private void TextBlock_Tapped(object sender, TappedRoutedEventArgs e)
{
var composite = this.DataContext as SampleCompositeData;
// 既存のデータを変更してみる
composite.SampleCollection[0].Title += " (変更)"; // (1)
// 新しくデータを追加してみる
composite.SampleCollection.Add(
new SampleData()
{
Title = string.Format("あとから追加されたデータ [{0}]",
DateTimeOffset.Now.ToString("HH:mm:ss")),
}); // (2)
}
Private Sub TextBlock_Tapped(sender As Object, e As TappedRoutedEventArgs)
Dim composite = DirectCast(Me.DataContext, SampleCompositeData)
' 既存のデータを変更してみる
composite.SampleCollection(0).Title += " (変更)" '(1)
' 新しくデータを追加してみる
composite.SampleCollection.Add( _
New SampleData() With
{
.Title = String.Format("あとから追加されたデータ [{0}]", _
DateTimeOffset.Now.ToString("HH:mm:ss"))
}) '(2)
End Sub
これで実行して、イベント・ハンドラを設定したTextBlockコントロールをタップしてみよう。しかし、望んだようには画面は変化しない。上のコードの(1)までを実装した場合には何も起きず、(2)まで実装したときはListViewコントロールを操作しようとすると例外が発生してしまうだろう(WP 8では追加したアイテムは表示されるが、変更は反映されない)。
これは、データの変更がコントロールに伝わっていないからだ。コレクションの個々のデータの変更は無視されるので画面は変化しない。また、コレクションのデータ数が変わっても、コントロール側で管理しているListViewItemオブジェクトの個数には反映されていないので、System.ArgumentException例外が発生してしまう。
これを解決するには、データにINotifyPropertyChangedインターフェイスとINotifyCollectionChangedインターフェイスを実装する。INotifyPropertyChangedインターフェイスは「文字列をコントロールにバインドするには?[Win 8/WP 8]」で紹介したが、クラスのプロパティに変更があったことをバインド先に通知するためのインターフェイスだ。それとは別に、コレクションの要素に追加/削除があったことをバインド先に通知するためのインターフェイスがINotifyCollectionChangedだ。この2つは、次の図に示すように役割が異なる。
INotifyPropertyChangedインターフェイスとINotifyCollectionChangedインターフェイス
※この図では、SampleDataCollectionクラスにINotifyCollectionChangedインターフェイスを継承させている。後述するように、これとは異なる方法もある。
INotifyCollectionChangedインターフェイスは、コレクションにデータの追加/削除があったことを通知する。そして、INotifyPropertyChangedインターフェイスは、個々のデータの内容に変化があったことを通知するのだ。
これらのインターフェイスを実装して、コレクションへの変更が画面に反映されるようにしてみよう。
まず、個々のデータを表すSampleDataクラスに、INotifyPropertyChangedインターフェイスの実装を与える。直接INotifyPropertyChangedインターフェイスを継承させるのではなく、ここでは「WinRT/Metro TIPS: バインドするデータのPropertyChangedを楽に実装するには?[Win 8/WP 8]」で紹介したBindableBaseクラスを継承させることにする(次のコード)。
public class SampleData : Common.BindableBase
{
private string _title;
public string Title
{
get { return _title; }
internal set { SetProperty(ref _title, value); }
}
private string _url;
public string Url
{
get { return _url; }
internal set { SetProperty(ref _url, value); }
}
}
Public Class SampleData
Inherits Common.BindableBase
Private _title As String
Public Property Title As String
Get
Return _title
End Get
Friend Set(value As String)
SetProperty(_title, value)
End Set
End Property
Private _url As String
Public Property Url As String
Get
Return _url
End Get
Friend Set(value As String)
SetProperty(_url, value)
End Set
End Property
End Class
ここで呼び出しているSetPropertyメソッドは、継承したBindableBaseクラスに定義されている。
また、WP 8では自分でBindableBaseクラスを実装する必要もある。これについては上記でも紹介した「バインドするデータのPropertyChangedを楽に実装するには?[Win 8/WP 8]」を参照してほしい。
次に、INotifyCollectionChangedインターフェイスであるが、それを継承するようにSampleDataCollectionクラスを作り変えるのは手間が掛かる。幸いなことに、そのようなことをしなくても済む仕掛けが用意されている。System.Collections.ObjectModel名前空間のObservableCollection<T>クラスがそれだ。
ObservableCollection<T>クラスはINotifyCollectionChangedインターフェイスを実装しているので、ObservableCollection<T>クラスを継承して独自のコレクションクラスを定義すれば、コレクションへの追加/変更が画面に反映されるようになる。
あるいは、既存のコレクションをObservableCollection<T>クラスでラップして利用することも可能だ。今回は、その方法を使ってみよう。それには、SampleCompositeDataクラスのSampleCollectionプロパティを次のコードのように変更する。
【既存コード】(本稿冒頭を参照)
private SampleDataCollection _sampleCollection
= new SampleDataCollection();
public SampleDataCollection SampleCollection
{ get { return _sampleCollection; } }
↓
【変更後】
using System.Collections.ObjectModel;
…… 省略 ……
private ObservableCollection<SampleData> _sampleCollection
= new ObservableCollection<SampleData>(new SampleDataCollection());
public ObservableCollection<SampleData> SampleCollection
{ get { return _sampleCollection; } }
【既存コード】(本稿冒頭を参照)
Private _sampleCollection As SampleDataCollection _
= New SampleDataCollection()
Public ReadOnly Property SampleCollection As SampleDataCollection
Get
Return _sampleCollection
End Get
End Property
↓
【変更後】
Private _sampleCollection As ObservableCollection(Of SampleData) _
= New ObservableCollection(Of SampleData)(New SampleDataCollection())
Public ReadOnly Property SampleCollection _
As ObservableCollection(Of SampleData)
Get
Return _sampleCollection
End Get
End Property
WP 8ではVBコードに「Imports System.Collections.ObjectModel」が必要だ。
これで完成だ。INotifyPropertyChangedインターフェイスとINotifyCollectionChangedインターフェイスを実装したので、データの変更や追加が画面に反映されるようになる(次の画像)。
●まとめ
複合データは、ページのDataContextプロパティにセットし、コントロールからはそこにデータ・バインドすると、見通しよくプログラミングできる。
データ・バインディングを通じてコレクションの変更を通知するには、INotifyPropertyChangedインターフェイスとINotifyCollectionChangedインターフェイスを実装する。ただし、INotifyCollectionChangedインターフェイスの実装にはObservableCollection<T>クラスを利用できる。
なお、ObservableCollection<T>クラスを利用する他の方法については、次のドキュメントを参照してほしい。
- MSDN:クイック スタート: コントロールへのデータ バインド - 「オブジェクトのコレクションへのコントロールのバインド」(ObservableCollection<T>クラスをそのまま使う)
- MSDN:方法 : ObservableCollection を作成およびバインドする(ObservableCollection<T>クラスを継承して独自のコレクションクラスを作る)
Copyright© Digital Advantage Corp. All Rights Reserved.