文字列をコントロールにバインドするには?[Win 8/WP 8]WinRT/Metro TIPS

Windowsストア・アプリでデータを表示するにはデータ・バインドが便利だ。本TIPSでは最もシンプルな形のデータ・バインドを解説する。

» 2013年04月04日 13時37分 公開
[山本康彦BluewaterSoft]
WinRT/Metro TIPS
業務アプリInsider/Insider.NET

powered by Insider.NET

「WinRT/Metro TIPS」のインデックス

連載目次

 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コントロールにバインドするというごくシンプルなケースから始めよう。

「デジタル時計」クラス

 次の画像のような簡単なデジタル時計アプリを作ってみよう。ただし、画面とロジックの分離を考えて、時刻を提供するクラスを画面から独立させて作るものとする。

完成したデジタル時計アプリの画面(Win 8/WP 8)完成したデジタル時計アプリの画面(Win 8/WP 8)

完成したデジタル時計アプリの画面(Win 8/WP 8)


 時分秒の文字列を適度な精度で提供する簡易的な「デジタル時計」クラスは、次のコードの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

「デジタル時計」クラスのコード(上:C#、下:VB)
デジタル時計のアプリは、このクラスのインスタンスでPropertyChangedイベントが発生したときに、NowTimeプロパティの値を使って画面を更新する。

[実装その1]イベントをそのまま使う

 データ・バインドを利用せず、イベントを使って実装してみよう。画面クラスのコンストラクタでイベント・ハンドラを設定し、イベント・ハンドラではClockクラスのNowTimeプロパティを参照して画面を描き変えるのだ。

 画面としてMainPage.xamlファイルを作成しよう。そこに、時刻表示に使うテキスト・ブロックを次のコードのように記述する。

<TextBlock x:Name="textClock1" Text="00:00:00"
    FontSize="120" Foreground="LimeGreen" />

時刻表示のためのテキスト・ブロック(XAML)
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

「デジタル時計」のイベントをイベント・ハンドラで処理するコード(上:C#、下:VB)

 これでビルドして動作することを確かめてほしい。

 このようにイベント・ハンドラを使っても目的を達することはできる。しかし、この方法には次のような問題がある。

  • データを提供するクラス(=データ・ソース)に複数のプロパティがあると、イベント・ハンドラの内部が煩雑になる*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

「デジタル時計」のイベントをデータ・バインドで処理するコード(上:C#、下:VB)
先ほどのコードで_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" />

コードによるデータ・バインドで時刻表示を行うのに使用するテキスト・ブロック(XAML)

 これで先ほどのイベントを使ったコードと同様に動作する。ビルドして確かめてほしい。

[実装その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

「デジタル時計」をデータ・ソースとしてテキスト・ブロックのデータ・コンテキストに設定するコード(上:C#、下:VB)
ここでは、Clockクラスをインスタンス化し、そのオブジェクトがデータを提供するようになるまでの時間はごく短いので、ページのコンストラクタに記述している。実際には、ページが表示されたタイミングで非同期にデータを取得させることが多い。

 これでテキスト・ブロックのデータ・コンテキストに、「デジタル時計」のインスタンスがセットされた。XAML側では、次のようにしてデータ・コンテキストを基準としたデータ・バインドを定義できる。

<TextBlock x:Name="textClock3" Text="{Binding NowTime}"
    FontSize="120" Foreground="DarkRed" />

時刻表示をデータ・バインドで行うテキスト・ブロック(XAML)

 Textプロパティにデータ・バインドが指定されている。C#/VBのコードでBindingオブジェクトを作ったときはSourceプロパティとPathプロパティを設定したが、ここでは「NowTime」という名前だけ、つまりBindingオブジェクトのPathプロパティの値だけを指定している。省略されたSourceプロパティは、テキスト・ブロックのデータ・コンテキストと見なされる。

 そして、このテキスト・ブロックのデータ・コンテキストには、C#/VBのコードでClockクラスのインスタンスを与えてあるから、結局このXAMLコードは「ClockクラスのインスタンスのNowTimeプロパティを、TextBlockコントロールのTextプロパティにバインドする」という意味になる。

 このようにXAMLを使うことでデータ・バインドを簡潔に書ける。データ・コンテキストをセットしてしまえば、あとはPathプロパティを指定するだけでデータ・バインドを定義できる。しかし、内部的にやっていることは冒頭のイベント・ハンドラを使ったコードと同じで、データ・ソースのイベントをトリガにしてそのデータを画面に反映させているのだ。

まとめ

 データ・バインドとは、イベントを使ってデータの変化を画面に伝える仕掛けである*2。イベント・ハンドラを使っても同様な実装を行えるが、データ・バインドを利用した方が簡潔に記述できる。ただし、XAMLで記述する場合は「データ・コンテキストに何が入っているか?」を把握することが重要だ。なお、データ・バインドで使うデータ・ソースはINotifyPropertyChangedインターフェイスを実装していなければならない。

 データ・バインドの基本について詳しくは、次のドキュメントを参照してほしい。

*2 本稿では説明しなかったが、逆向き(画面の変化→データ)もある。


「WinRT/Metro TIPS」のインデックス

WinRT/Metro TIPS

Copyright© Digital Advantage Corp. All Rights Reserved.

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。