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
●[実装その1]イベントをそのまま使う
データ・バインドを利用せず、イベントを使って実装してみよう。画面クラスのコンストラクタでイベント・ハンドラを設定し、イベント・ハンドラではClockクラスのNowTimeプロパティを参照して画面を描き変えるのだ。
画面としてMainPage.xamlファイルを作成しよう。そこに、時刻表示に使うテキスト・ブロックを次のコードのように記述する。
   <TextBlock x:Name="textClock1" Text="00:00:00"
    FontSize="120" Foreground="LimeGreen" />
そうしたら、この画面のコードビハインド(=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 上のコードで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
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
これでテキスト・ブロックのデータ・コンテキストに、「デジタル時計」のインスタンスがセットされた。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.