第5回 WPFの「データ・バインディング」を理解する:連載:WPF入門(1/3 ページ)
データとなるモデルと、表示を行うビューを結び付ける「データ・バインディング」と、データの表示をカスタマイズできる「データ・テンプレート」について解説。
powered by Insider.NET
今回および次回の2回に渡り、ビューとモデルの疎結合を実現するための仕組みとして、データ・バインディングとコマンドという2つの機能について説明する。まず今回は、これらの機能の背景にあるGUIアプリケーションに対する要件と、データ・バインディングについて説明を行っていく。
■GUIアプリケーションに対する要件
WPFのデータ・バインディングやコマンドといった仕組みを説明する前に、そもそもGUIアプリケーションに対して、どのような要件があるのかを整理してみよう。ここでは、実装上で満たすべき要件として「ビューとモデルの疎結合」と、GUIアプリケーションに求められる機能(の中で、今回はデータ・バインディングに関係する部分)を紹介する。
●ビューとモデルの疎結合
GUIアプリケーション開発においてよくいわれる言葉として、「ビュー(view)とモデル(model)を疎結合にしろ」というものがある。
すなわち、アプリケーションの見た目にかかわる部分(=ビュー)と、見た目と関係なく成り立つロジックや表示したいデータ(=モデル)を分けるのが良いとされる。オブジェクト指向言語の場合には、ビューとモデルを別クラスとして表現し、互いのクラスに参照関係を作らないことを指す(そうすることで、再利用性やテスト可能性が向上する)。
単に「モデル」というと意味が広すぎるが、要は、業務を分析し、モデル化して得られたデータ(=ドメイン・モデル)を指す言葉だ。アプリケーションの種類によっては、モデルという言葉がしっくりこないかもしれないが、(GUIであろうとCUI(character-based user interface)であろうと)表示や操作の方法にかかわらず変わらない部分を独立させ、その部分を指して「モデル」と呼んでいると思えばいいだろう。
●GUIアプリケーションに求められる機能
データ・バインディングと関連してGUIアプリケーションに求められる機能としては、以下のようなものがある。
○同じデータを複数のウィンドウやコントロールから参照
通常、モデルの持っているデータをビュー上に表示する方法は1通りではない。例えば、同じデータを、テーブルに表示したい場合もあれば、折れ線グラフなどで表示したい場合もあるだろう。また、テーブルとグラフを同時に表示したうえで、テーブル上での値の変更が即座にグラフ側に反映されてほしい場合も少なくないだろう。
Visual Studioを例にとって1つ具体例を挙げるなら、XAMLエディタがまさにこのような挙動になっている。
Figure 1に示すように、このXAMLエディタは、マウスを使って視覚的にデザインするための「デザイン」画面(画像上部)と、XAMLコードを文字ベースで入力する「XAML」画面(画像下部)を持っている。そして、どちらか片方への変更があった場合、もう一方に即座に変更結果が反映される。
このような機能を実現するためには、データの変更を通知するような仕組みが必要で、このためにWPFは「データ・バインディング」という仕組みを持っている(また、次回説明する「コマンド」も、ビューとモデルの疎結合を保つために有用である)。次節以降では、このデータ・バインディングについて説明していく。
■データ・バインディング
WPFでは、データ・ソース(=モデルなどの、データの提供元)をビュー(=WPFの場合はXAMLコード)上のUI要素と簡単に結び付けるために、データ・バインディングという仕組みを提供している。「バインディング」(binding:結合)や「結び付ける」という言い方をしているが、これは、値を一度だけ代入するのではなく、「1カ所で値が変化するたびに、ほかの個所にもその変化が即座に伝搬される」ということを指している。
具体的には、以下のような手順でデータ・ソースとUI要素を結び付ける(Figure 2)。
- XAMLコード中に「このプロパティの値をここに表示する」という印だけを入れておく
- 表示したいデータ・ソースは、DataContextプロパティを通じてビューに渡す
アプリケーションの要件によっては、モデルのインスタンスをそのままDataContextプロパティに渡すだけでよい。例えば、モデルが最初からWPF向けに作られている場合や、後述するINotifyPropertyChangedインターフェイスなどの仕組みを必要としない単純な要件の場合などである。ただし実際には、このような場合に該当することはあまりなく、WPF向けにモデルをラッピングしたもの(=ビュー・モデルと呼ぶ)を作って、これをDataContextプロパティに渡すことになる。
●データ・バインディングの使い方
それでは、データ・バインディングの具体例を見てみよう。まず、List 1に示すように、XAMLコード中にBindingマークアップ拡張を使って「このプロパティの値をここに表示する」という印を入れておく。
<Window x:Class="atmarkit05.MinimalSampleWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Width="100" SizeToContent="Height" Title="ウィンドウ名">
<StackPanel>
<TextBlock Text="{Binding X}" />
<TextBlock Text="{Binding Y}" />
</StackPanel>
</Window>
<TextBlock>要素のTextプロパティに対して「{Binding X}」と書くことで、「Textプロパティの値とXプロパティの値を結び付ける」という意味になる。
次に、FrameworkElementクラス(System.Windows名前空間)(Windowクラスもこのクラスを継承している)のDataContextプロパティにデータ・ソースを渡す。
今回はList 2に示すように、分離コード中のビュー・クラス(Windowクラスを継承)のコンストラクタでDataContextプロパティに匿名クラスを使って渡す(ただし、実際には、データの差し替えやテストのしやすさを考えると、ビュー・クラスの外でデータ・ソースを渡す方が好ましい)。実行結果はFigure 3のようになる。
using System.Windows;
namespace atmarkit05
{
public partial class MinimalSampleWindow : Window
{
public MinimalSampleWindow()
{
InitializeComponent();
this.DataContext = new { X = 10, Y = 20 };
}
}
}
Namespace atmarkit05
Class MinimalSampleWindow
Sub New()
InitializeComponent()
Me.DataContext = New With {.X = 10, .Y = 20}
End Sub
End Class
End Namespace
このように、単にデータを表示するだけなら何も特殊な実装は必要ない(後述するようなINotifyPropertyChangedインターフェイスの実装は不要)。また、同名のプロパティさえ持っていればよく、具体的な型が何かは問われない。従って、この例のように匿名クラスであっても構わない(ただし、匿名クラスが利用できるのはアプリケーションが完全信頼で動作している場合のみ)。
ここで、DataContextプロパティの値は包含継承(親UI要素から値が引き継がれる)されていて、ビュー・クラスのDataContextプロパティに渡したデータ・ソースは、<TextBlock>要素のDataContextプロパティにも引き継がれている。「{Binding X}」というマークアップ拡張記述の実体としては、そのUI要素のDataContextプロパティに格納されているオブジェクトのXプロパティを参照することになる。
●バインディングのソースとターゲット
データ・バインディングを行ううえで、データの提供元(=ソース)となるものと、反映先(=ターゲット)となるものができるが、混乱を避けるため、Figure 4に示すように用語を定める。
- バインディング・ソース: データの提供元のオブジェクト。
- ソース・プロパティ: データの提供元となる、バインディング・ソースのプロパティ。
- バインディング・ターゲット: データの反映先のオブジェクト。
- ターゲット・プロパティ: データの反映先となる、バインディング・ターゲットのプロパティ。
まず、WPFのデータ・バインディングにおいて、ターゲット・プロパティは依存関係プロパティでなければならない。幸い、WPFでXAMLコード中に記述する要素のプロパティは、大部分が依存関係プロパティになっていて、ターゲット・プロパティにすることが可能である(ただし、InputBindingクラス(System.Window.Input名前空間)のCommandプロパティのように、.NET Framework 3.5までは依存関係プロパティになっておらず、.NET Framework 4で初めてターゲット・プロパティにできるようになったものもある)。
一方で、バインディング・ソースには以下のようなものが利用可能である。
- CLRオブジェクト: 通常のCLRオブジェクトのプロパティやインデクサをソース・プロパティにできる(アクセス修飾子は「public」である必要がある。また、フィールドは利用できない)。プロパティ値の取得や更新はリフレクションを介して行われる。あるいは、ICustomTypeDescripterインターフェイスを実装する場合や、TypeDescriptionProviderクラスを使って型情報をWPFに対して登録している場合(いずれのクラスもSystem.ComponentModel名前空間)には、これらを介して値の取得・更新が行われる。また、値の変更を通知するためには、INotifyPropertyChangedインターフェイス(System.ComponentModel名前空間)を実装する必要がある。
- 依存関係オブジェクト: ソース・プロパティに依存関係プロパティを用いる場合、パフォーマンスもよく、標準で値の変更を通知する仕組みも備えている。ただし、依存関係プロパティを利用するためには、バインディング・ソースが依存関係オブジェクトである(DependencyObjectを継承している)必要がある(その結果、ほかのクラスを継承できなくなる)という問題もある。
- XMLオブジェクト: Bindingマークアップ拡張の指定の仕方によっては(SourceプロパティとXPathプロパティを利用)、XMLオブジェクトを直接バインディング・ソースにすることができる。
- 動的オブジェクト: .NET Framework 4からは、IDynamicMetaObjectProviderインターフェイスを実装する動的オブジェクトもバインディング・ソースに指定可能になった。すなわち、IronPythonやIronRubyなどの動的言語を用いて作成したオブジェクトを、そのままバインディング・ソースにできる。
実際のところ、CLRオブジェクトをバインディング・ソースとして利用する場合が多いだろう。そこで重要になるのがINotifyPropertyChangedインターフェイスを利用した値の変更通知である。このINotifyPropertyChangedインターフェイスの実装方法については、次回で説明する。
それでは、次のページからはBindingマークアップ拡張の書き方について詳しく説明していこう。
Copyright© Digital Advantage Corp. All Rights Reserved.