WisdomSoft - for your serial experiences.

7.1 バインディング

WPF には UI とデータを接続し、常にデータの変更を反映させる仕組みを提供しています。これによって、ユーザーの入力に応じて内部データを書き換えるコードを書く必要がなくなり、最小限のコードでデータ処理が可能になります。

7.1.1 ビューとモデルの接続

WPF が既定で提供する UIElement の実装は FrameworkElement クラスから派生しています。FrameworkElement クラスでは、WPF のコントロールや図形などが共通して保有する高度な機能をまとめています。よって、このクラスが公開しているメソッドやプロパティの使い方を知ることで、WPF コントロールをさらに柔軟に、便利に使うことができるようになります。

WPF のコントロールで特徴的な機能の 1 つに、データバインディングの存在があります。データバインディングとは、元となるデータと、データを表示したり、操作するためのユーザーインターフェイスを結合するためのオブジェクトです。一般的な設計では、コントロールが表示する値と、アプリケーションが管理するべきデータは分離して扱います。MVC アーキテクチャでは、源泉となるデータをモデル、データを視覚的に表示するコントロールのことをビューと呼んでいます。

図1 モデルとビュー
図1 モデルとビュー

図1は、例えばユーザー情報を格納するデータベースから取得したデータと UI の対応を表します。ユーザー情報を表すデータに対応するオブジェクトは、純粋なデータであることからモデルであると考えることができます。このオブジェクトは情報を保持していますが、それをグラフィカルに表示する機能は持ちません。一方、WPF などの GUI ライブラリは、TextBox クラスや RadioButton クラスなど、グラフィカルに情報を表示する手段を持っています。しかし、これらのコントロールは情報そのものではありません。

モデルとビューは、それぞれが独立したオブジェクトですが、意味的には関連付けられています。もし、プログラムの動作によってモデルの値が書き換えられた場合、ビューも合わせて更新されるべきです。ビューが表示するデータが更新されたにもかかわらず、ビューが変更されなければ、画面上に表示されているデータは古いままとなってしまい、整合性が失われます。

この問題を回避するために、源泉となるデータと、データを表示するコントロールの値の変更を監視して、同期させる仕組みがバインディングです。

図2 バインディング
図2 バインディング

バインディングの概念は古くからありましたが、WPF のオブジェクトはより柔軟にバインディングを実現させることができます。バインディングは、独立したオブジェクトであるモデルとビューを監視し、一方のプロパティが変更されたときに、もう一方のプロパティを更新することで整合性を保たせます。

バインディングは、バインディングターゲットとバインディングソースの 2 つのオブジェクトを関連付けるバインディングオブジェクトを作成します。このとき、バインディングターゲットとなれるのは依存プロパティだけです。バインディングソースには、任意のオブジェクトのプロパティを設定することができます。

図3 ソースとターゲット
図3 ソースとターゲット

バインディングターゲットには依存プロパティしか設定できませんが、通常は WPF の何らかのコントロールがターゲットとなるので問題はありません。FrameworkElement クラスは依存プロパティを保有することができる DependencyObject クラスから派生しています。

バインディングを行うには System.Windows.Data.Binding クラスを利用します。このクラスのオブジェクトが、ソースとターゲットを結ぶバインディングオブジェクトとなります。

System.Windows.Data.Binding クラス
System.Object 
   System.Windows.Markup.MarkupExtension 
     System.Windows.Data.BindingBase 
      System.Windows.Data.Binding
public class Binding : BindingBase

このクラスのコンストラクタは、オーバーロードされています。

Binding クラスのコンストラクタ
public Binding ()
public Binding (string path)

文字列型の path パラメータには、データソースが保有しているプロパティの名前を指定します。ここで指定したプロパティが、データソースのバインドされるプロパティとなります。この設定は Path プロパティからも行えます。Path プロパティは、バインディングソースとなるオブジェクトのプロパティの名前を設定します。

Binding クラス Path プロパティ
public PropertyPath Path { get; set; }

Path プロパティには、プロパティを参照するためのプロパティ名などを提供する System.Windows.PropertyPath クラスのオブジェクトを設定します。プロパティ名だけでは参照できないインデクサを指定したい場合は、このクラスのオブジェクトを明示的にインスタンス化する必要があります。

System.Windows.PropertyPath クラス
[TypeConverterAttribute(typeof(PropertyPathConverter))] 
public sealed class PropertyPath

Binding オブジェクトが参照しているプロパティの情報を知りたい場合も Path プロパティから受け取った PropertyPath から取得することができます。この場では、Binding クラスのコンストラクタらプロパティ名を指定する方法を使うので、PropertyPath は使いません。

バインディングを行うには、データの源泉となるオブジェクトを設定しなければなりません。データソースを設定するには Source プロパティを利用します。この Source プロパティに設定したオブジェクトの、コンストラクタから設定したプロパティ名、または Path プロパティが表すプロパティが使われます。

Binding クラス Source プロパティ
public Object Source { get; set; }

データソースには任意のオブジェクトを設定することができるので、このプロパティの型は Object 型です。

ソースとなるオブジェクトとプロパティ名が適切に設定することができれば、あとはバインディングターゲットを設定するだけです。バインディングターゲットを設定するには、ターゲットとなる FrameworkElement オブジェクトの SetBinding() メソッドを使います。

FrameworkElement クラス SetBinding() メソッド
public BindingExpressionBase SetBinding (
    DependencyProperty dp,
    BindingBase binding
)

dp には、バインディングの対象となる依存プロパティを、binding には、バインディングソースが正しく設定された Binding オブジェクトを設定します。ターゲットとなる DependencyProperty オブジェクトは、通常はコントロールを提供するクラスの static なフィールドで公開されています。例えば、TextBox クラスの Text プロパティの実体は TextProperty フィールドで公開されている依存プロパティです。

TextBox クラス TextProperty フィールド
public static readonly DependencyProperty TextProperty

SetBinding() メソッドは、戻り値に設定したバインディングの情報を保存する System.Windows.Data.BindingExpressionBase クラスのオブジェクトを返します。通常は使いませんが、バインディングを手動で管理したい場合などで使うことがあります。

System.Windows.Data.BindingExpressionBase クラス
System.Object 
   System.Windows.Expression 
    System.Windows.Data.BindingExpressionBase
public abstract class BindingExpressionBase : Expression, IWeakEventListener

BindingExpressionBase クラスのオブジェクトは、このオブジェクトを生成した BindingBase オブジェクトを提供したり、バインディングソースやターゲットを更新するメソッドなどを提供しています。この場では、SetBinding() の戻り値を使うことはありません。

バインディングのソースには、あらゆるオブジェクトを設定することができますが、この場では TextBox オブジェクトをソースとして、Label オブジェクトをターゲットにバインディングの効果を試したいと思います。TextBox コントロールにテキストが入力されると Text プロパティの値が変更されます。そこで、Text プロパティが変更された場合、Label クラスが表示するテキストも同期するようにプログラムします。

コード1
using System;
using System.Windows;
using System.Windows.Data;
using System.Windows.Controls;

class Test{
	[STAThread]
	public static void Main() {
		TextBox textBox = new TextBox();
		textBox.Text = "テキストを入力してください";
		textBox.Margin = new Thickness(10);

		Binding bind = new Binding();
		bind.Path = new PropertyPath("Text");
		bind.Source = textBox;

		Label label = new Label();
		label.SetBinding(ContentControl.ContentProperty, bind);

		StackPanel panel = new StackPanel();
		panel.Children.Add(label);
		panel.Children.Add(textBox);

		Window wnd = new Window();
		wnd.Content = panel

		Application app = new Application();
		app.Run(wnd);
	}
}
実行結果
コード1 実行結果

コード1は、データソースに TextBox オブジェクトの Text プロパティを選択し、ターゲットに Label オブジェクトの ContentProperty 依存プロパティを選択しています。図4は、コード1のバインディング関係を表したものです。

図4  コード1のラベルとテキストボックスの関係
図4 コード1のラベルとテキストボックスの関係

そのため、ソースである TextBox オブジェクトの Text プロパティの値が変更されると、自動的に Label オブジェクトの ContentProperty も同じ値に自動的に更新されます。

7.1.2 独自のデータをソースにする

バインディングのソースには Object 型の任意のプロパティを設定することができるため、独自のクラスをソースとすることも可能です。通常は、O/R マッピングによって作られた、データベースから取得したデータのオブジェクト表現を担当するクラスがソースとなるでしょう。

例えば、認証用 ID や名前、生年月日などのユーザー情報を提供する User という名前のクラスを自作したとします。コントロールやプログラムから、ユーザー情報に変更が加えられたとき、バインディングターゲットとなるコントロールも合わせて更新しなければなりません。

図5 独自オブジェクトをソースにする
図5 独自オブジェクトをソースにする

図5は、テキストボックスなどのユーザー入力によって User オブジェクトの Name プロパティが変更された時、Label オブジェクトの表示を合わせて更新しなければならない場合の関係を表しています。

コード2
using System;
using System.Windows;
using System.Windows.Data;
using System.Windows.Controls;

class User {
	private string name;

	public string Name {
		set { name = value; }
		get { return name; }
	}
}

class Test : Window {
	[STAThread]
	public static void Main() {
		Window wnd = new Test();
		Application app = new Application();
		app.Run(wnd);
	}

	private User user;
	private Label label;
	private TextBox textBox;

	public Test() {
		user = new User();
		user.Name = "ル・ブラン・ド・ラ・ヴァリエール";

		Binding bind = new Binding("Name");
		bind.Source = user;

		label = new Label();
		label.SetBinding(ContentControl.ContentProperty, bind);

		textBox = new TextBox();
		textBox.Margin = new Thickness(10);
		textBox.TextChanged += textBoxTextChanged;

		StackPanel panel = new StackPanel();
		panel.Children.Add(label);
		panel.Children.Add(textBox);

		Content = panel;
	}

	private void textBoxTextChanged(Object sender, TextChangedEventArgs e) {
		user.Name = textBox.Text;
	}
}
実行結果
コード2 実行結果

コード2は、ユーザー情報を提供するクラスを擬態する User クラスを作成しています。この場では、簡易的にユーザー名を表現するための Name プロパティが存在するとして、このプロパティが変更されたときに、合わせてビューとなるコントロールの表示も更新しなければなりません。

当然、バインディングソースとなるのは User クラスのオブジェクトの Name プロパティです。そして、ターゲットにはウィンドウの上部に表示される Label オブジェクトの Content 依存プロパティです。

プログラムを実行すると、確かに User オブジェクトの Name プロパティにしたがって Content が初期化されていることが確認できます。しかし、テキストボックスからテキストを入力してソースの Name プロパティの値を変更しても、ターゲットが更新されることはありません。TextChanged イベントで、テキストボックスのテキストが変更されると Name プロパティの値を更新するコードは正しく実行されているにもかからわずです。

これは、ソースオブジェクトがプロパティの変更を通知する手段を持たないためです。Binding オブジェクトは、プロパティが変更されたときに何らかの方法で外部のコードに通知する方法を提供しなければなりません。

TextBox クラスのオブジェクトの場合は正しくプロパティの変更が通知されていました。あれは TextBox クラスが TextChanged イベントを公開していたためです。.NET 1.x のデータバインディング方式に従った ~Changed イベントが公開されている場合 Binding はそれを認識してくれます。例えば、Name プロパティをバインディングソースとするクラスが NameChanged イベントを公開していれば、Binding オブジェクトは自動的に Name プロパティの変更を監視してくれます。

コード3
using System;
using System.Windows;
using System.Windows.Data;
using System.Windows.Controls;

class User {
	private string name;

	public event EventHandler NameChanged;

	public string Name {
		set {
			name = value;
			if (NameChanged != null)
				NameChanged(this, new EventArgs());
		}
		get { return name; }
	}
}

class Test : Window {
	[STAThread]
	public static void Main() {
		Window wnd = new Test();
		Application app = new Application();
		app.Run(wnd);
	}

	private User user;
	private Label label;
	private TextBox textBox;

	public Test() {
		user = new User();
		user.Name = "ル・ブランド・ラ・ヴァリエール";

		Binding bind = new Binding("Name");
		bind.Source = user;

		label = new Label();
		label.SetBinding(ContentControl.ContentProperty, bind);

		textBox = new TextBox();
		textBox.Margin = new Thickness(10);
		textBox.TextChanged += textBoxTextChanged;

		StackPanel panel = new StackPanel();
		panel.Children.Add(label);
		panel.Children.Add(textBox);

		Content = panel;
	}

	private void textBoxTextChanged(Object sender, TextChangedEventArgs e) {
		user.Name = textBox.Text;
	}
}
実行結果
コード3 実行結果

コード3は、User オブジェクトの Name プロパティが、TextBox のイベントで呼び出された textBoxTextChanged() によって変更されると、上部のラベルの Content プロパティを正しく更新します。これは User クラスが NameChanged イベントを公開し、Name プロパティの値が変更されたタイミングで正しく NameChanged イベントを発生させているためです。

この方法は比較的簡単ですが、明示的な通知方法ではありません。より推奨される通知方法は System.ComponentModel.INotifyPropertyChanged インターフェイスをバインディングソースに実装することです。

System.ComponentModel.INotifyPropertyChanged インターフェイス
public interface INotifyPropertyChanged

INotifyPropertyChanged は、オブジェクトの何らかのプロパティが変更されたことを通知する PropertyChanged イベントを宣言しています。

INotifyPropertyChanged インターフェイス PropertyChanged イベント
event PropertyChangedEventHandler PropertyChanged

このイベントは、プロパティの変更を通知する System.ComponentModel.ProgressChangedEventHandler デリゲートを登録することができます。

System.ComponentModel.ProgressChangedEventHandler デリゲート
public delegate void ProgressChangedEventHandler (
    Object sender,
    ProgressChangedEventArgs e
)

ProgressChangedEventHandler デリゲートは、第1パラメータ sender にイベントを発生させたオブジェクトを、第2パラメータ e に変更されたプロパティの情報を格納する System.ComponentModel.PropertyChangedEventArgs クラスのオブジェクトを受け取ります。

System.ComponentModel.PropertyChangedEventArgs クラス
System.Object 
   System.EventArgs 
    System.ComponentModel.PropertyChangedEventArgs
public class PropertyChangedEventArgs : EventArgs

PropertyChangedEventArgs クラスのコンストラクタには、変更されたプロパティの名前を表す文字列を設定します。

PropertyChangedEventArgs クラスのコンストラクタ
public PropertyChangedEventArgs (string propertyName)

バインディングソースで、INotifyPropertyChanged インターフェイスを実装し、プロパティが変更された時点で PropertyChanged イベントを実行すれば、Binding オブジェクトはソースが変更されたことを認識することができます。

コード4
using System;
using System.ComponentModel;
using System.Windows;
using System.Windows.Data;
using System.Windows.Controls;

class User : INotifyPropertyChanged {
	private string name;

	public event PropertyChangedEventHandler PropertyChanged;

	public string Name {
		set {
			name = value;
			if (PropertyChanged != null)
				PropertyChanged(this, new PropertyChangedEventArgs("Name"));
		}
		get { return name; }
	}
}

class Test : Window {
	[STAThread]
	public static void Main() {
		Window wnd = new Test();
		Application app = new Application();
		app.Run(wnd);
	}

	private User user;
	private Label label;
	private TextBox textBox;

	public Test() {
		user = new User();
		user.Name = "ル・ブランド・ラ・ヴァリエール";

		Binding bind = new Binding("Name");
		bind.Source = user;

		label = new Label();
		label.SetBinding(ContentControl.ContentProperty, bind);

		textBox = new TextBox();
		textBox.Margin = new Thickness(10);
		textBox.TextChanged += textBoxTextChanged;

		StackPanel panel = new StackPanel();
		panel.Children.Add(label);
		panel.Children.Add(textBox);

		Content = panel;
	}

	private void textBoxTextChanged(Object sender, TextChangedEventArgs e) {
		user.Name = textBox.Text;
	}
}

コード4の実行結果はコード3と同じものですが、イベントの通知方法が違います。