はじめに
.NETプラットフォームのデータバインディング機能は強力な開発ツールです。データバインディングとは、簡単に説明すれば、「アプリケーション要素のプロパティを、グラフィカルな表示要素の中の特定要素にリンクすること」です。たとえば、カスタムオブジェクトのプロパティ(従業員オブジェクトの名前プロパティなど)を、Windowsフォームのテキストボックスのtext
プロパティにリンクするような場合に使用します。
データバインディングは開発者にとって新しい概念でありません。アプリケーションの基本的な部分であり、アプリケーションのエンドユーザーと処理ロジックの間で情報をやりとりするための、最も基本的な方法です。多くの場合、これは「力仕事」で行われてきました。手作業でコードを書いて、オブジェクトのプロパティとGUI要素(テキストボックス、データグリッド、チェックボックスなど)の間でデータをマーシャリングしてきました。しかし、.NET Frameworkには、これまでにない豊富な機能が備わっています。特に、アプリケーションでデータバインディングを使うことには、大きな魅力があります。
- 一度設定するだけで、GUIコントロールとオブジェクトのプロパティとの同期を維持できる。
- 型変換機能により、たとえば、オブジェクトの
decimal
型のプロパティを、テキストボックスのstring
型のtext
プロパティにマッピングすることができる。 - デリゲートとイベントに基づいた方法で、オブジェクトのプロパティの変更を「監視」できる。
- オブジェクトにGUIコントロールのプロパティを管理させることで、わずかな手間で表現豊かなUIが構築できる(たとえば、値が変更されない限り[Save]ボタンを有効にしないなど)。
- 数多くのイベントを利用して、デフォルトの構文解析や値の書式設定よりも、指定したコントロールのプロパティを優先させることができる。
- コントロールのデータ型検証機能により、たとえば、オブジェクトの日時プロパティにバインドされたテキストボックスには、有効な日付だけが入力されることを保証できる。
私の見る限り、データバインディング機能は充分に活用されていません。GUIとアプリケーションオブジェクトとのデータのやりとりを実現するために、膨大なコードを書き続けている開発者をまだよく見かけます。おそらく、.NETで何ができるかを知らないか、データバインディングが水面下でデータをどう管理しているのかを理解していないかの、どちらかでしょう。この記事を読むことで、こうしたハードルを乗り越え、アプリケーションでデータバインディングを使うきっかけにしてほしいというのが、私のねらいです。
データバインディングは広範なテーマなので、この記事で扱うことと扱わないことを、あらかじめ区別しておきます。この記事では、独自のシングルインスタンスオブジェクトをWindowsフォームコントロールにバインディングする方法だけを扱います。Webフォームのバインディングは扱いません。また、独自のコレクションをデータグリッドやドロップダウンボックスといった要素にバインディングする方法も扱いません。ASP.NETのWebフォームに関する開発の大部分が完成しつつあるのは知っていますが、.NET開発者の関心と支持を急速に集めているスマートクライアント開発への準備になるという理由で、Windowsフォームに的を絞ることにしました。
前提
この記事の説明とコード例では、全体を通して同じクラスとWindowsフォームを使用します。コード例を作成するにあたっては、話をわかりやすくするためにシンプルなもの、かつ、皆さんに興味を持ってもらえる程度には複雑なものにするよう心がけました。そこで、Employee
という1つのクラスと、このオブジェクトのプロパティを読み書き可能な方法で表示するフォームを用意することにしました。
この記事で使用するクラスは、一般的な企業をモデルとして作ったものです。したがって、複雑なビジネスロジックや、別の層の別のクラスに属するデータアクセスは出てきません。説明上、オブジェクトのプロパティにデータが必要な場合は、値をハードコーディングします。オブジェクトを保存する必要がある場合は、単純にファイルにシリアライズします。これも、例を単純化して、データバインディングに専念するためです。
Employee
クラスをUMLで表したのが、図1です。
このクラスのすべてのソースコードを見る場合は、この記事の先頭のダウンロード用リンクを使用してください。ダウンロードファイルには3つのソリューションが含まれており、それぞれこの記事で説明するデータバインディングの種類に対応しています。
具体的なデータバインディングの話に進む前に、ソースコードについていくつか説明しておきます。まず、ダーティフラグ(dirty flag)とその実装方法についてです。ダーティフラグは、オブジェクトのプロパティが違う値に変更されたかどうかを示します。オブジェクトのプロパティに値が設定されるたびに、図2のように、CheckDirty
というプライベート関数が呼び出されます。
呼び出されたCheckDirty
が、プロパティの現在値と提示された値が異なると判定しない限り、プロパティには新しい値が設定されません。
CheckDirty
のロジックは図3のとおりです。後述の「上級レベルのデータバインディング」では、このダーティフラグを使ってユーザーインターフェースを改良します。
この記事で使用するフォームが図4です。
このフォームには、従業員の名前(First Name)、名字(Last Name)、雇用日(Hire Date)、給与(Salary)を表示するテキストボックスがあります。.NET Framework内でデータバインディングがどのようにキャストと変換を行うのかを見てもらうため、decimal
やDateTime
など、さまざまなデータ型を選びました。さらに、Boolean型のisActive
プロパティの値を示すチェックボックスと、[Save]と[Load New Values]の2つのボタンを用意しました。
これから説明するアプリケーションの要素について基本的な説明が終わったところで、データバインディングがどのように動作するのかを見ていきましょう。
力仕事のデータバインディング
最初に説明するデータバインディングの方法は、私が「力仕事」と名づけた方法です。この方法では、オブジェクトと表示の連結部をすべてハードコーディングし、.NET Frameworkのデータバインディング機能はまったく使用しません。これを基準にすると、.NETが実際にどれくらいのことを行ってくれるのかがよくわかります。この例の全ソースコードは、ダウンロードサンプルの「ManualDataBindingSolution」フォルダの中にあります。
フォームの中には、フォームで使用するEmployee
クラスのインスタンスを保存するため、図5のとおり、メンバ変数を作成しました。
フォームのコンストラクタの中では、図6のとおり従業員オブジェクトをインスタンス化し、データを設定します。説明の便宜上、値はハードコーディングしています。実際のアプリケーションでは、こうした値はほとんどの場合、ビジネスロジックやデータクラスによって設定されます。
フォームのLoad
イベントハンドラは、図7のようにしてフォームの値をマーシャリングします。このとき、データバインディングコードをカプセル化したLoadValuesToForm
というプライベートメソッド(図8)を呼び出します。これを別々のメソッドに分けたことには、2つの理由があります。フォームの別の領域からコードを再利用できるようにするためと、フォームのイベントハンドラからできるだけ処理ロジックを切り離すためです。これにより、コードがすっきりし、メンテナンスがしやすくなります。
コードを見てわかるとおり、decimal
型とDateTime
型のフィールドの書式を手作業で設定する必要があります。
オブジェクトの値がどのように設定され、その値がどういった経路でオブジェクトからフォームのコントロールに渡されるかを理解したところで、次は、[Save]ボタンがクリックされたときなどに、フォームコントロールの値がオブジェクトにどのようにマーシャリングされるのかを見てみましょう。フォームの値をオブジェクトにマーシャリングするための実際のロジックは図9のとおりです。
ここでも、データをテキストベースのプロパティからdecimal
型やDateTime
型の値にキャストするのは、開発者の仕事です。なお実際のアプリケーションでは、詳しいエラーメッセージを出力するために、238~241行目(decimal
型、DateTime
型の値をマーシャリングしている部分)で値をチェックするロジックを追加するとよいでしょう。たとえば、[Hire Date]テキストボックスに文字列でない日付を入力すると、この部分のコードにより、図10のようなエラーダイアログが表示されます。
すべての値がフォームからオブジェクトに正常にマーシャリングされ、少なくとも1つのプロパティの値が変更されていた場合は、そのオブジェクトがアプリケーションのカレントディレクトリにあるXMLファイルにシリアライズされます。
[Load New Values]ボタンは、データソースから新しい値を入力するなど、現在のEmployee
オブジェクトの値を別の値に変更する処理を行います。注意してほしいのは、オブジェクトのプロパティが変更されたときに、手作業でLoadValuesToForm
メソッドを呼び出して、オブジェクトの値をフォームに読み込む必要があるという点です。オブジェクトのプロパティが変更されたかどうかを手作業で確認する代わりに、フォームへの変更が自動的に行われればよいとは思いませんか。実はそれが、.NET Frameworkのデータバインディング機能の一面です。
一方向のデータバインディング
このシナリオでは、.NET Framworkの機能を使用して、Employee
クラスに変更を加えずにデータバインディングを実現し、フォームのロジックを実際に削減します。このシナリオのコードは「OneWayDataBindingSolution」の中にあります。
このソリューションでは、Employee
を表示するフォームから、2つのマーシャリング関数(LoadValuesToForm
とLoadValuesFromForm
)を削除します。この2つの関数を、フォームのLoad
イベントハンドラから呼び出されるSetDataBindings
に置き換えます。ここでも、データバインディングを設定するロジックをLoad
イベントハンドラ内に直接記述することもできますが、ロジックを独立した関数にまとめた方が、コードの再利用性が高まり、すっきりしたものになります。SetDataBindings
メソッドのコードは図11のとおりです。
このメソッドでは単純に、GUI要素とローカルオブジェクトのプロパティとのデータバインディングを定義しています。バインディングは、コードの243~244行目のような方法で、それぞれ個別に設定する必要があります。構文は以下のとおりです。
この構文から、各コントロールが自分用のデータバインディングのコレクションを持っていることがわかります。このコレクションの中で、コントロールの各プロパティがデータバインディングを持つことができます。重要なのは、プロパティはデータバインディングを1つしか持てないという点です。複数割り当てることもできますが、システム引数例外になります。このため、バインディング処理の前に、目的のプロパティにバインディングがあるかどうかをチェックし、あれば削除します。このコードでは239~241行目でその処理を行っています。
バインディングを作成する構文(図12の242~244行目)を見てみましょう。コントロール側のプロパティ("Text"
)と、データソース(_oEmployee
)と、データソース側のプロパティ("firstName"
)を指定しています。面白いことに、C#の世界では通常は大文字と小文字が区別されますが、コントロールとデータソースに指定されたプロパティの名前については、大文字と小文字の区別がありません。
バインディングに使用するコントロール側のプロパティは、バインディングの構文の中で指定するので、他のプロパティにバインディングするのも簡単です。例えば271~272行目では、チェックボックスの"Checked"
プロパティにバインディングしています。
もう1つ注目すべきは、データバインディング機能は自動的に変換を行ってくれることです。これで、decimal
型やDateTime
型のデータを、文字列から、あるいは文字列へ変換するコードを書く必要はなくなりました。この機能の利点は、開発者がこうした低水準な細かい連結処理の心配をしなくてよくなったことです。また、入力データをフィルタする機能もあります。プロジェクトを起動し、[Hire Date]テキストボックスにdate
型以外の値を入力した場合、または[Salary]テキストボックスに数値以外の値を入力した場合、入力は無効になり元の値に戻されます。
ここで注意してほしいのは、アプリケーションを起動したときに、デフォルトの書式が使用されるという点です。[Hire Date]テキストボックスの場合は、日付と時間が表示されますが、これは意図とは違います。手作業によるデータバインディングの例では、DateTime
型のToShortDateTime()
メソッドを使って値を書式設定しました。Microsoftは、この問題を解決するために、イベントデリゲートを割り当てておき、データバインディング機能がコントロールに表示するデータを書式設定するときにそのイベントデリゲートが呼び出されるようにするという方法を採用しました。今回の例では、バインディング機能がバインド先コントロールの値の書式設定を準備するときに発生するFormat
イベントにイベントデリゲートを割り当てる必要があります。そこで、データバインディングを定義するためのもう1つのメソッドとして、SetDataBindingsWithFormat
というメソッドをフォーム内に作成しました。このメソッドは、表示の書式設定を制御するのに必要なイベントを追加します。さらに、デリゲートとして機能するDateTimeToShortDateString
メソッドを用意しました。サンプルコードを実行するときに、SetDataBindingsWithFormat
とSetDataBindings
のどちらかを呼び出すことで、違いを確認することができます。イベントデリゲートを追加する新しいコードを図14に示します。
図14が示すとおり、ここでは[Hire Date]テキストボックスのバインディング(イベントハンドラの登録も含む)を定義してから、そのバインディングを実際のコントロールに割り当てています。これは、バインディングがコントロールに割り当てられた時点で、バインディング機能が自動的にデータソースから値を取得するためです。したがって、図15のように、バインディングの定義と割り当てを一気に行ってからハンドラを割り当てた場合は、書式設定が適用されません。
データバインディングの動作は、アプリケーションを実行したり、テキストボックスの値を変更したり、[Save]ボタンをクリックしたりすることで確認できます。[Save]のclick
イベントハンドラか、値を調べたいオブジェクト内にブレークポイントを設定すれば、オブジェクトのプロパティが新しい値に更新されることを確認できます。または、「Employees.xml」ファイルでシリアライズされた出力を見ることができます。
しかし、この方法ではデータフローは一方向(フォームからオブジェクト)のみです。オブジェクトのプロパティの値が変更された場合、フォームには変更が反映されません。このことは[Load New Values]ボタンをクリックすると確認できます。このボタンのclick
イベントハンドラによってオブジェクトの値が変更されますが、フォームの値は変更されません。このとき[Save]ボタンをクリックすると、オブジェクトはフォームに表示された値とは別の値で保存されることになります。
データバインディングが一方向だけの機能であるならば、「なぜフォームに初期値が読み込まれたのか」と疑問に思うでしょう。答えはこうです。データバインディングが割り当てられると、データバインディング機能はプロパティの現在の値を参照しますが、ある方法を使わない限り、コントロールがオブジェクトから値を取得するのはこの1回限りになります。以降も継続的に値が取得されるようにするには、次の「上級レベルのデータバインディング」で説明する変更を加えます。
上級レベルのデータバインディング
ここでは、データバインディングの学習の総仕上げとして、オブジェクトとフォームの間で双方向の通信を可能にし、どちらかを変更するともう一方も変更される方法を説明します。この方法は、オブジェクトとGUIコントロールの間で双方向の通信を行うので、ほとんどの場合で、この方法を使用することになるでしょう。前出の例で見たとおり、一方向(基本的にはフォームからオブジェクト)だけの通信を使用すると、オブジェクトがフォーム以外から変更されたときに矛盾が起こります。双方向の通信を行うためには、データバインディング機能に、オブジェクトのプロパティがいつ変更されたのかを知らせる必要があります。このソリューションのソースコードは、ダウンロードサンプルの「TwoWayDataBindingSolution」フォルダに収録されています。
コントロールのプロパティとデータソースのプロパティとの間でバインディングを定義すると、データバインディング機能は自動的にデータソース上で「プロパティ名+Changed」という名前のイベントを探します。イベントが見つかると、データバインディング機能はこのイベントのハンドラを用意し、該当プロパティの値が変更されたときに通知を受け取るようにします。したがって、Employee
のプロパティに対して、プロパティ変更を検出するイベントを実装すれば、簡単に双方向のデータバインディングを有効にすることができます。
図16のコードでは、必要なイベントを宣言するため、Employee
クラスの変数宣言部を変更しています。図17は、プロパティのセッターでイベントを発生させるコードを示しています。
オブジェクトがフォームに変更を通知できるようになったので、[Save]ボタンのEnabled
プロパティをオブジェクトのisDirty
プロパティに連結することができます。これで、オブジェクトのデータが変更されて保存が必要になったときにだけ、[Save]ボタンを有効にできます。それが図18のコードです。
まとめ
.NET Frameworkのデータバインディングには、多くの機能があります。初期のMicrosoftのツールセットからはずいぶん進歩しました。この記事では、完全な手作業から、コントロールとカスタムオブジェクトのプロパティとの間の双方向通信まで、さまざまなレベルでのデータバインディングを取り上げました。また、.NET Frameworkのデータバインディング機能のイベントモデルを利用して、デフォルトの書式設定処理を変更する方法も示しました。