文字列をコントロールにバインドするには?[Win 8/WP 8]:WinRT/Metro TIPS
Windowsストア・アプリでデータを表示するにはデータ・バインドが便利だ。本TIPSでは最もシンプルな形のデータ・バインドを解説する。
powered by Insider.NET
Windowsストア・アプリでデータを表示するにはデータ・バインドを使うのがよいといわれる。しかしドキュメントやサンプル・コードを読んでみても、何だか難しそうなうえに、とても範囲が広そうだ。どこから手を付けたらよいのだろうか?
そこで本稿では、最もシンプルな形のデータ・バインドを解説する。なお、掲載しているコードはWindowsストア・アプリのものだが、記述するコードはWindows Phone 8でも全く同じである。本稿のサンプルは「Windows Store app samples:MetroTips #31(Windows 8版)」と「Windows Store app samples:MetroTips #31(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を使用している。
Windows Phone 8向けのアプリを開発するには、SLAT対応CPUを搭載したPC上の64bit版Win 8 Pro以上とWindows Phone SDK 8.0(無償)が必要となる。
●データ・バインドはWindowsストア・アプリ開発の肝
Windowsストア・アプリでは、データ・アクセスは非同期に行うのが一般的だし、画面とは別にバックグラウンド・タスクでデータ・アクセスを行うパターンもよくある。同期アクセスが主体だった従来のデスクトップ・アプリの感覚で「メソッドを呼び出してデータを取得し、そのデータを使って画面を更新する」という明示的なやり方は、Windowsストア・アプリらしくないのだ。
時間の経過や何らかの処理によって動的に変化するデータを自動的に画面に反映させるには、データ・バインドが最適だ。データ・バインドの理解は、Windowsストア・アプリ開発には必須だといえるだろう。その第一歩として、時間の経過とともに変化する文字列をTextBlockコントロールにバインドするというごくシンプルなケースから始めよう。
●「デジタル時計」クラス
次の画像のような簡単なデジタル時計アプリを作ってみよう。ただし、画面とロジックの分離を考えて、時刻を提供するクラスを画面から独立させて作るものとする。
時分秒の文字列を適度な精度で提供する簡易的な「デジタル時計」クラスは、次のコードのClockクラスのように実装できる。秒が変わるのを一定間隔(約10ミリ秒間隔)で監視し、変化したところでイベントを発生させるのだ。なお、INotifyPropertyChangedインターフェイス(System.ComponentModel名前空間)を継承し、発生させるイベントとしてPropertyChangedEventHandlerデリゲート(System.ComponentModel名前空間)を使っているのは、データ・バインドにも使えるようにするためだ。データ・バインドしないのならば、独自のイベント定義でも構わない。
public class Clock : INotifyPropertyChanged
{
// 現在時刻を表す文字列のプロパティ "HH:mm:ss"
public string NowTime { get; private set; }
// NowTimeプロパティが変化したときに発生させるイベントの定義
// なお、このプロパティはINotifyPropertyChangedインターフェイスの実装である
public event PropertyChangedEventHandler PropertyChanged;
public Clock()
{
Run(); // 時刻監視の無限ループを動かす
}
private async void Run()
{
DateTimeOffset lastTime;
while (true)
{
await Task.Delay(10); // おおよそ10ミリ秒ごとにシステム時計をチェックする
var nowTime = DateTimeOffset.Now;
if (lastTime.Second != nowTime.Second)
{
// 秒が変わったら、プロパティに時刻をセットし、イベントを発火させる
this.NowTime = nowTime.ToString("HH:mm:ss");
if (this.PropertyChanged != null)
this.PropertyChanged(this, new PropertyChangedEventArgs("NowTime"));
lastTime = nowTime;
}
}
}
}
Public Class Clock
Implements INotifyPropertyChanged
' 現在時刻を表す文字列のプロパティ "HH:mm:ss"
Private _nowTime As String
Public Property NowTime As String
Get
Return _nowTime
End Get
Private Set(value As String)
_nowTime = value
End Set
End Property
' NowTimeプロパティが変化したときに発生させるイベントの定義
Public Event PropertyChanged(sender As Object, e As PropertyChangedEventArgs) _
Implements INotifyPropertyChanged.PropertyChanged
Public Sub New()
Run() ' 時刻監視の無限ループを動かす
End Sub
Private Async Sub Run()
Dim lastTime As DateTimeOffset
While (True)
Await Task.Delay(10) ' おおよそ10ミリ秒ごとにシステム時計をチェックする
Dim nowTime = DateTimeOffset.Now
If (lastTime.Second <> nowTime.Second) Then
' 秒が変わったら、プロパティに時刻をセットし、イベントを発火させる
Me.NowTime = nowTime.ToString("HH:mm:ss")
RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs("NowTime"))
lastTime = nowTime
End If
End While
End Sub
End Class
デジタル時計のアプリは、このクラスのインスタンスでPropertyChangedイベントが発生したときに、NowTimeプロパティの値を使って画面を更新する。
●[実装その1]イベントをそのまま使う
データ・バインドを利用せず、イベントを使って実装してみよう。画面クラスのコンストラクタでイベント・ハンドラを設定し、イベント・ハンドラではClockクラスのNowTimeプロパティを参照して画面を描き変えるのだ。
画面としてMainPage.xamlファイルを作成しよう。そこに、時刻表示に使うテキスト・ブロックを次のコードのように記述する。
<TextBlock x:Name="textClock1" Text="00:00:00"
FontSize="120" Foreground="LimeGreen" />
Textプロパティに値が設定してあるのは、XAMLエディタ上でデザインを確認するため。FontSizeプロパティとForegroundプロパティは適当でよい。
そうしたら、この画面のコードビハインド(=MainPage.xaml.csファイルまたはMainPage.xaml.vbファイル)に、Clockクラスのインスタンスとそのイベント・ハンドラを追加する(次のコードの太字部分)。
// 「デジタル時計」クラスのインスタンス
private Clock _clock1 = new Clock();
public MainPage()
{
this.InitializeComponent();
// 【1】「デジタル時計」クラスのイベント・ハンドラを設定する
_clock1.PropertyChanged += clock1_PropertyChanged;
}
// 【1】「デジタル時計」クラスのプロパティが変化したときに呼び出されるハンドラ
void clock1_PropertyChanged(object sender,
System.ComponentModel.PropertyChangedEventArgs e)
{
textClock1.Text = _clock1.NowTime;
}
' 「デジタル時計」クラスのインスタンス
Private _clock1 As Clock = New Clock()
Public Sub New()
' この呼び出しはデザイナーで必要です。
InitializeComponent()
' InitializeComponent() 呼び出しの後で初期化を追加します。
' 【1】「デジタル時計」クラスのイベント・ハンドラを設定する
AddHandler _clock1.PropertyChanged, AddressOf clock1_PropertyChanged
End Sub
' 【1】「デジタル時計」クラスのプロパティが変化したときに呼び出されるハンドラ
Private Sub clock1_PropertyChanged(sender As Object, e As PropertyChangedEventArgs)
textClock1.Text = _clock1.NowTime
End Sub
これでビルドして動作することを確かめてほしい。
このようにイベント・ハンドラを使っても目的を達することはできる。しかし、この方法には次のような問題がある。
- データを提供するクラス(=データ・ソース)に複数のプロパティがあると、イベント・ハンドラの内部が煩雑になる*1。
- データ・ソースが増えるとイベント・ハンドラも増えていき、コード全体が煩雑になる。
*1 上のコードでclock1_PropertyChangedメソッドの引数e(PropertyChangedEventArgsクラス)には、変更されたプロパティの名前が入ってくる。複数のプロパティがある場合は、そのプロパティの名前によって分岐するコードを書くことになる。
●[実装その2]コードだけでバインドするには?
上記のイベントを使ったコードは、次のようにデータ・バインドを使って書き直せる。実はデータ・バインドとは、データの変化をイベントによって画面に反映させるコードを隠ぺいする仕掛けなのだ。
// 「デジタル時計」クラスのインスタンス
private Clock _clock1 = new Clock();
public MainPage()
{
this.InitializeComponent();
…… 省略 ……
// 【2】テキスト・ブロックへのバインディング
var tbBind = new Binding()
{
Source = _clock1,
Path = new PropertyPath("NowTime"),
};
textClock2.SetBinding(TextBlock.TextProperty, tbBind);
}
' 「デジタル時計」クラスのインスタンス
Private _clock1 As Clock = New Clock()
Public Sub New()
' この呼び出しはデザイナーで必要です。
InitializeComponent()
' InitializeComponent() 呼び出しの後で初期化を追加します。
…… 省略 ……
' 【2】テキスト・ブロックへのバインディング
Dim tbBind = New Binding()
With tbBind
.Source = _clock1
.Path = New PropertyPath("NowTime")
End With
textClock2.SetBinding(TextBlock.TextProperty, tbBind)
End Sub
先ほどのコードで_clock1.PropertyChangedイベントにイベント・ハンドラをセットしていた部分が、Bindingオブジェクトの生成とテキスト・ブロックのSetBindingメソッド呼び出しに変わっている。イベント・ハンドラ内で行っていたNowTimeプロパティの値の画面への反映は、SetBindingメソッドで結び付けられたBindingオブジェクトが行ってくれる。
Bindingクラスのオブジェクトには、上のコードのように、SourceプロパティとPathプロパティを最低限指定する必要がある。ここでは、「_clock1クラスのオブジェクトをデータ・ソースとして、その『NowTime』プロパティの値をバインドする」という意味になる。このとき、Bindingオブジェクトはデータ・ソースのPropertyChangedイベントにBindingオブジェクト自体が持っているイベント・ハンドラを設定する。
SetBindingメソッドで、どのコントロールのどのプロパティにデータをバインドさせるか指定する。ここでは、「textClock2コントロールのTextProperty依存関係プロパティ(=Textプロパティ)にバインドさせる」という意味だ。
この方法では、イベント・ハンドラを使った場合の問題点は解消されている。データ・ソースやバインドするプロパティの数が増えても、分岐やイベント・ハンドラの管理は増えず、Bindingオブジェクトを作ってSetBindingメソッドを呼び出すコードを並べていくだけで済む。
また、コードによるデータ・バインドを使って時刻を表示するためのテキスト・ブロックをMainPage.xamlファイルに追加しておく(Gridコントロールに、textClock1やtextClock2をそのまま追加するときにはコントロールをStackPanelなどに格納するとよい)。
<TextBlock x:Name="textClock2" Text="00:00:00"
FontSize="120" Foreground="DarkGoldenrod" />
これで先ほどのイベントを使ったコードと同様に動作する。ビルドして確かめてほしい。
●[実装その3]XAMLでバインドするには?
しかし、Bindingオブジェクトの作成とSetBindingメソッドの呼び出しのコードを記述するのは面倒だ。実はXAMLで同じことをさらに簡潔に記述できる。コントロールの「データ・コンテキスト」という場所(=DataContextプロパティ)にデータ・ソースをセットしてやると、あとはXAMLでバインドを記述することが可能だ。
まず、コードビハインドを次のように書き変える。
// 「デジタル時計」クラスのインスタンス
private Clock _clock1 = new Clock();
public MainPage()
{
this.InitializeComponent();
…… 省略 ……
// 【3】テキスト・ブロックのデータ・コンテキストに設定
// ※バインドはXAMLで定義する
textClock3.DataContext = _clock1;
}
' 「デジタル時計」クラスのインスタンス
Private _clock1 As Clock = New Clock()
Public Sub New()
' この呼び出しはデザイナーで必要です。
InitializeComponent()
' InitializeComponent() 呼び出しの後で初期化を追加します。
…… 省略 ……
'【3】テキスト・ブロックのデータ・コンテキストに設定
' ※バインドはXAMLで定義
textClock3.DataContext = _clock1
End Sub
ここでは、Clockクラスをインスタンス化し、そのオブジェクトがデータを提供するようになるまでの時間はごく短いので、ページのコンストラクタに記述している。実際には、ページが表示されたタイミングで非同期にデータを取得させることが多い。
これでテキスト・ブロックのデータ・コンテキストに、「デジタル時計」のインスタンスがセットされた。XAML側では、次のようにしてデータ・コンテキストを基準としたデータ・バインドを定義できる。
<TextBlock x:Name="textClock3" Text="{Binding NowTime}"
FontSize="120" Foreground="DarkRed" />
Textプロパティにデータ・バインドが指定されている。C#/VBのコードでBindingオブジェクトを作ったときはSourceプロパティとPathプロパティを設定したが、ここでは「NowTime」という名前だけ、つまりBindingオブジェクトのPathプロパティの値だけを指定している。省略されたSourceプロパティは、テキスト・ブロックのデータ・コンテキストと見なされる。
そして、このテキスト・ブロックのデータ・コンテキストには、C#/VBのコードでClockクラスのインスタンスを与えてあるから、結局このXAMLコードは「ClockクラスのインスタンスのNowTimeプロパティを、TextBlockコントロールのTextプロパティにバインドする」という意味になる。
このようにXAMLを使うことでデータ・バインドを簡潔に書ける。データ・コンテキストをセットしてしまえば、あとはPathプロパティを指定するだけでデータ・バインドを定義できる。しかし、内部的にやっていることは冒頭のイベント・ハンドラを使ったコードと同じで、データ・ソースのイベントをトリガにしてそのデータを画面に反映させているのだ。
●まとめ
データ・バインドとは、イベントを使ってデータの変化を画面に伝える仕掛けである*2。イベント・ハンドラを使っても同様な実装を行えるが、データ・バインドを利用した方が簡潔に記述できる。ただし、XAMLで記述する場合は「データ・コンテキストに何が入っているか?」を把握することが重要だ。なお、データ・バインドで使うデータ・ソースはINotifyPropertyChangedインターフェイスを実装していなければならない。
データ・バインドの基本について詳しくは、次のドキュメントを参照してほしい。
*2 本稿では説明しなかったが、逆向き(画面の変化→データ)もある。
Copyright© Digital Advantage Corp. All Rights Reserved.