デベロッパー2006年5月16日 10:00
文字サイズ文字サイズ小文字サイズ中文字サイズ大

オブジェクトバインディングに関するヒントとテクニック

この記事のURLhttp://japan.internet.com/developer/20060516/25.html
著者:Deborah Kurata
海外internet.com発の記事

はじめに

 筆者は、Visual Basic 2.0以来、データバインディングの概念を嫌ってきた多くの開発者の1人でした。データバインディングの仕組みは拡張性が低く、有益なプログラミング手法を活用していないし、期待通りに動作しないこともしょっちゅうでした。しかし、今はオブジェクトバインディングの愛好者であることを認めざるを得ません。

 本稿では、まず、オブジェクトバインディングの基本について説明します。次に、オブジェクトバインディングをよりよく活用するためのヒントとテクニックを示します。オブジェクトバインディングで実行できることをすべて理解したならば、読者もこれが気に入ることでしょう。

オブジェクトバインディングの基本

 Windows Formsにおけるオブジェクトバインディングの大前提は、TextプロパティなどのWindows Formsコントロールプロパティを、ビジネスオブジェクトプロパティに結び付けることです。ビジネスオブジェクトを作成し、プロパティに値を代入すると、プロパティ値をコントロールにコピーするコードを記述しなくても、その値がアプリケーションのユーザーインターフェイスに反映されます。そしてユーザーがコントロールの内容を変更すると、更新された値がビジネスオブジェクトのプロパティに自動的に代入されます。

 例えば、顧客情報を表示するフォームと、LastNameプロパティおよびFirstNameプロパティを持つCustomerビジネスオブジェクトがあるものとします。オブジェクトバインディングを使用すると、LastNameプロパティをLastNameテキストボックスのTextプロパティに割り当てるコードや、FirstNameプロパティ値をFirstNameテキストボックスのTextプロパティに割り当てるコードを記述する必要がありません。ユーザーがコントロール内のテキストを変更する場合は、逆が当てはまります。つまり、このすべての処理が自動的に行われるのです。

 データベースへのバインディングでは、あらかじめ生成されている大量のコードにバインドする必要がありますが、それに対してオブジェクトバインディングでは、自分で記述したコードにバインドします。そのため、自分で多くを管理することが可能であり、アプリケーションのメンテナンスが非常に簡単です。

 オブジェクトバインディングを試すために、新しいWindows Formsプロジェクトを作成し、クラスを1つ追加してみましょう(実際のアプリケーションでは、このクラスを別のプロジェクトに含める場合もあります。このサンプルでは、話を簡単にするために、すべてのクラスをWindows Formsプロジェクトに追加します)。次に、そのクラスにさまざまなプロパティを持たせます。例えば、Customerクラスには、LastNameFirstNameCustomerIDの各プロパティがあります。LastNameプロパティのコードを以下に示します。

VBの場合
Private _LastName As String
Public Property LastName() As String
   Get
      Return _LastName
   End Get
   Set(ByVal value As String)
      _LastName = value
   End Set
End Property
C#の場合
private string _LastName;
public string LastName
{
   get { return _LastName;}
   set { _LastName = value;}
}

 プロパティプロシージャを作成するには、Propertyスニペットを使用するのが最も簡単です。VBコードウィンドウで「property」と入力し、[Tab]キーを押して、VB Propertyプロシージャを作成します。C#コードウィンドウでは、「prop」と入力し、[Tab]キーを2回押してプロパティを作成します。スニペットの使用または独自のスニペットの作成の詳細については、『CoDe Magazine』2006年1月/2月号の「Having Fun with Code Snippets」を参照してください。

 ビジネスオブジェクト上で目的のプロパティを定義したら、プロジェクトをビルドします。ビルドプロセスによって、[Data Sources]ウィンドウからビジネスオブジェクトにアクセスできるようになります。ビジネスオブジェクトにプロパティを追加する場合はいつでも、[Data Sources]ウィンドウにプロパティが表示される前に、プロジェクトをリビルドする必要があります。

 次に、データソースをビルドします。[Data Sources]ウィンドウが表示されていない場合は、[Data]ウィンドウの[Show Data Sources]を選択して表示し、[Add New Data Source]をクリックします。データソース構成ウィザードが起動します。

 オブジェクトバインディングデータソースを作成するには、ウィザードの先頭のページで[Object]を選択し、2番目のページで目的のビジネスオブジェクトを選択します。[Finish]ボタンをクリックすると、[Data Sources]ウィンドウに新しいオブジェクトデータソースが表示されます。オブジェクトの各プロパティが、ウィンドウ内のオブジェクト名の下に表示されることに注意してください。

 さて、これからが本番です。プロジェクトを作成するときに作成された既定のフォームを開きます。[Data Sources]ウィンドウ内のビジネスオブジェクトに関連するコンボボックスをドロップダウンし、[DataGridView]を選択して、すべてのビジネスオブジェクトをグリッドで表示するか、[Details]を選択して、個々のビジネスオブジェクトプロパティをフォーム上の個別のコントロールとして表示します。最後に、[Data Sources]ウィンドウからフォームへ、ビジネスオブジェクトの名前をドラッグします。図1は、[Details]セクションを使った場合の結果を示しています。[Data Sources]ウィンドウによるフォーム作成の詳細については、『CoDe Magazine』2004年9月/10月号の「Drag-Once Databinding」を参照してください。

図1 既定では、ビジネスオブジェクトをフォームにドラッグすると、BindingNavigator(ビデオデッキのボタンに似たコントロールツールバー)もフォームに追加される。これが不要な場合は削除する。
図1 既定では、ビジネスオブジェクトをフォームにドラッグすると、BindingNavigator(ビデオデッキのボタンに似たコントロールツールバー)もフォームに追加される。これが不要な場合は削除する。

 オブジェクトバインディングデータソースは、フォームのコンポーネントトレイに自動的に追加され、ビジネスオブジェクトの名前にちなんだ名前が付けられます。この例の場合は、「CustomerBindingSource」です。生成されるコントロールは、このバインディングソースに自動的にバインドされます。

 Visual Studio IDEは、プロパティの名前を使ってコントロールのラベルを作成しますが、アンダースコアや英字の大文字/小文字に基づいて単語間に空白を追加したりできます。これは、アプリケーションのユーザーインターフェイスの開始点として、非常に優れています。

ビジネスオブジェクトに値を割り当てる

 この時点でアプリケーションを実行しても、何も表示されません。コントロールはプロパティにバインドされていますが、プロパティに値がないのです。そこで、ビジネスオブジェクトに値を割り当てることが、オブジェクトバインディングの次の大きなステップになります。

 データを取得してビジネスオブジェクトに割り当てるには、さまざまな方法があります。例えば、DataSetまたはDataTableを使用する方法や、Webサービスを使って必要なデータを取得する方法があります。また、TableAdaptersを使って、プロパティをデータベースフィールドにバインドする方法もあります。

 話を簡単にするために、ビジネスオブジェクトのプライベートなRetrieveメソッドを使って、必要なデータを取得することにします。実際には、ADO.NETを使ってコード内でDataTableを作成します。この方法の場合、オブジェクトバインディングを試みるのに、データベース、ストアドプロシージャ、またはWebサービスを作成する必要がありません。リスト1は、VBコードのRetrieveメソッドです。リスト2は、C#のRetrieveメソッドです。

リスト1 テスト用の一時的な^^Retrieve^^メソッド(VB)
Private Function Retrieve(ByVal CustomerID As Int32) As DataRow
Dim dt As New DataTable
   ’ Define the columns
   dt.Columns.Add("CustomerID")
   dt.Columns.Add("LastName")
   dt.Columns.Add("FirstName")
   Select Case CustomerID
   Case 1
      dt.Rows.Add(1, "Einstein", "Albert")
   Case 2
      dt.Rows.Add(2, "Curie", "Marie")
   Case 3
      dt.Rows.Add(3, "Brahe", "Tycho")
   Case 4
      dt.Rows.Add(4, "Faraday", "Michael")
   End Select
   Return dt.Rows(0)
End Function
リスト2 テスト用の一時的な^^Retrieve^^メソッド(C#)
private DataRow Retrieve(Int32 CustomerID)
{
   DataTable dt = new DataTable();
   // Define the columns
   dt.Columns.Add("CustomerID");
   dt.Columns.Add("LastName");
   dt.Columns.Add("FirstName");
   switch (CustomerID)
   {
      case 1:
         dt.Rows.Add(1, "Einstein", "Albert");
         break;
      case 2:
         dt.Rows.Add(2, "Curie", "Marie");
         break;
      case 3:
         dt.Rows.Add(3, "Brahe", "Tycho");
         break;
      case 4:
         dt.Rows.Add(4, "Faraday", "Michael");
         break;
   }
   return dt.Rows[0];
}

 ベストプラクティスでは、Factoryパターンを使ってビジネスオブジェクトを作成し、これに取得データを割り当てます。Factoryパターンを実装するには、さまざまな方法があります。今回の例では、次に示すように、Factoryパターンの実装によってコンストラクタをプライベートにし(これで他のコードはオブジェクトを作成できなくなります)、インスタンスの作成と値の割り当てを行う静的な(共有の)Createメソッドを定義します。

VBの場合
Private Sub New()
’ Don’t allow creation of this object
End Sub
Public Shared Function Create(ByVal CustomerID _
   As Int32) As Customer
   ’ Create the customer
   Dim oCustomer As New Customer
   ’ Retrieve the data for this customer
   Dim dr As DataRow
   dr = oCustomer.Retrieve(CustomerID)
   ’ Populate the business object
   oCustomer.Populate(dr)
   Return oCustomer
End Function
C#の場合
private Customer()
{
   // Don’t allow creation of this object
}
public static Customer Create(Int32 CustomerID)
{
   // Create the customer
   Customer oCustomer = new Customer();
   // Retrieve the data for this customer
   DataRow dr =oCustomer.Retrieve(CustomerID);
   // Populate the business object
   oCustomer.Populate(dr);
   return oCustomer;
}

 実際にビジネスオブジェクトへの値の割り当てを行うメソッドは、次のようになります。

VBの場合
Private Sub Populate(ByVal CustomerDataRow As _
   DataRow)
   With CustomerDataRow
      Me.LastName = .Item("LastName").ToString
      Me.FirstName = .Item("FirstName").ToString
      Me.CustomerID = .Item("CustomerID").ToString
   End With
End Sub
C#の場合
private void Populate(DataRow CustomerDataRow)
{
   this.LastName =
      CustomerDataRow["LastName"].ToString();
   this.FirstName =
      CustomerDataRow["FirstName"].ToString();
   this.CustomerID =
   Convert.ToInt32(CustomerDataRow["CustomerID"]);
}

 ビジネスオブジェクトにプロパティを追加するたびに、Populateメソッドのコードを拡張する必要があります。型指定されていないDataSet、DataReader、または他の方法を使用するのではなく、TableAdapterまたは型指定されたDataSetを使ってデータを取得する場合は、引用符で囲まれた文字列の列名の代わりに、自動的に生成された厳密に型指定された名前を使って、データ列を参照します。

 オブジェクトバインディングを完成させるには、フォームのLoadイベントに、オブジェクトの作成とバインドを行うコードを追加します。具体的には、次のようにCreateメソッドを呼び出してオブジェクトのインスタンスを作成し、そのインスタンスにCustomerBindingSourceを代入してバインドします。

VBの場合
Private Sub CustomerWin_Load(ByVal sender As _
   Object, ByVal e As System.EventArgs) _
   Handles Me.Load
   Dim CustomerInstance As Customer
      CustomerInstance = Customer.Create(1)
      CustomerBindingSource.DataSource =
         CustomerInstance
End Sub
C#の場合
private void CustomerWin_Load(object sender,
   EventArgs e)
{
   Customer CustomerInstance =
      Customer.Create(1);
   customerBindingSource.DataSource =
      CustomerInstance;
}

 この時点で、アプリケーションを実行できます。問題がなければ、フォームが表示され、そのフォーム上のコントロールにプロパティ値がテキストとして表示されます。

データ管理

 フォーム上の値の更新をユーザーに許可する場合は、その値を検証する必要が生じます。すべての値が有効なときは、変更を保存するオプションを提供するようにします。オブジェクトバインディングは、検証およびデータ管理プロセスを支援するいくつかの機能を備えています。

 オブジェクトバインディングを使って検証を実装するには、最初に、ErrorProviderコントロールをフォームに追加し、そのバインディングソースを、本稿で作成したCustomerBindingSourceに設定します。これにより、ErrorProviderはビジネスオブジェクトからのエラーを認識できるようになります。

 ErrorProviderを設定したら、ビジネスオブジェクトのプロパティに直接、フィールドレベルの検証ルールを実装します。プロパティが無効な場合は、例外をスローします。ユーザーが入力した値が無効の場合は、ErrorProviderアイコンに例外のテキストが自動的に表示されます。

 簡単な例として、Customerビジネスオブジェクトに、ユーザーに姓の入力を求めるビジネスルールがあるものとします。これは、次のように実装できます。

VBの場合
Public Property LastName() As String
   Get
      Return _LastName
   End Get
   Set(ByVal value As String)
      If String.IsNullOrEmpty(value) Then
         Throw New Exception( _
            "The Last Name cannot be empty")
      End If
      _LastName = value
   End Set
End Property
C#の場合
public string LastName
{
   get { return _LastName;}
   set
   {
      if (String.IsNullOrEmpty(value))
      {
         throw new Exception(
            "The Last Name cannot be empty");
      }
      _LastName = value;
   }
}
注意
 プロパティプロシージャ内のコード、またはプロパティプロシージャによって呼び出されるコードが原因で生じる予期せぬ例外は、オブジェクトバインディングによってキャッチされ、ErrorProviderに表示されます。このため、例えばキャストエラーがあっても、このエラーを目にすることはありません。オブジェクトバインディングコードがこのエラーをキャッチして、ErrorProviderアイコンに表示します。

 適切な検証コードをビジネスオブジェクトプロパティに直接追加することができ、必要であれば例外をスローできます。もっと柔軟で再利用性に優れた検証処理が必要な場合は、独立したValidationクラスを作成し、このクラスで必須フィールドのチェック、文字列長のチェック、数値チェック、日付範囲のチェックなど、さまざまな種類の検証を行うことができます。Validationクラスは、例外をスローする代わりに、検証エラーのコレクションを保持できます。その場合、ビジネスオブジェクトはIDataErrorInfoインターフェイスを実装して、このコレクションをErrorProviderに公開する必要があります。この方法の詳細については割愛しますが、興味を抱く読者が多いようであれば、このトピックに関する記事を書くつもりです。

 ユーザーの入力値を検証したら、ビジネスオブジェクトは、値の変更を追跡する必要があります。これによって、何らかの変更があった場合のみ、終了時に変更を保存するかどうかをユーザーに確認するなどの機能を実装できるようになります。また、オブジェクトの更新、作成、または削除を追跡し、元のデータソースにさかのぼって適切な処理を実行することができます。

 このデータ管理では、次の4つの基本ステップが必要です。

  1. データ状態を表す定数を定義します。
  2. ビジネスオブジェクトにプロパティを追加して、状態を保持します。
  3. INotifyPropertyChangedインターフェイス(System.ComponentModel.INotifyPropertyChanged)を実装します。
  4. PropertyChangedイベントを生成します。

 この4ステップを実装するにあたり、最も直接的なのは、必要なコードの大部分をすべてのビジネスオブジェクトクラスで繰り返すという方法です。もっと洗練された実装にするには、4ステップのコードを実装した基本のビジネスオブジェクトクラスを作成し、この基本クラスをすべてのビジネスオブジェクトクラスで継承するという方法もあります。

 ここでは、例を簡単にするために、上記の4ステップをCustomerビジネスオブジェクトクラスに直接実装することにします。

 ステップ1:データ状態を表す定数を定義します。

VBの場合
Public Enum EntityStateEnum
   Unchanged
   Added
   Deleted
   Modified
End Enum
C#の場合
public enum EntityStateEnum
{
   Unchanged,
   Added,
   Deleted,
   Modified,
}

 ステップ2:状態を保持するプロパティを定義します。

VBの場合
Private _EntityState As EntityStateEnum
Public Property EntityState() As EntityStateEnum
   Get
      Return _EntityState
   End Get
   Private Set(ByVal value As EntityStateEnum)
      _EntityState = value
   End Set
End Property
C#の場合
private EntityStateEnum _EntityState;
public EntityStateEnum EntityState
{
   get { return _EntityState; }
   private set { _EntityState = value; }
}

 setアクセサの実装はPrivateであることに注意してください。これによって、他のコードによる状態の変更が回避されます。

 ステップ3:INotifyPropertyChangedインターフェイスを実装します。このインターフェイスを実装し、プロパティ値が変更されたときにPropertyChangedイベントを起動することによって、オブジェクトバインディングは変更を自動的に検出します。プロパティに加えられた変更は、ユーザーインターフェイスの中で確実に更新されます。

VBの場合
Public Class Customer
   Implements INotifyPropertyChanged
   Public Event PropertyChanged(ByVal sender As _
     Object, ByVal e As PropertyChangedEventArgs) _
     Implements INotifyPropertyChanged.PropertyChanged
C#の場合
class Customer : INotifyPropertyChanged
public event PropertyChangedEventHandler
   PropertyChanged;

 ステップ4:PropertyChangedイベントを起動します。

VBの場合
Private Sub DataStateChanged(ByVal dataState As _
   EntityStateEnum, ByVal propertyName As String)
   ’ Raise the event
   If Not String.IsNullOrEmpty(propertyName) Then
      RaiseEvent PropertyChanged(Me, _
         New PropertyChangedEventArgs(propertyName))
   End If
   ’ If the state is deleted, mark it as deleted
   If dataState = EntityStateEnum.Deleted Then
      Me.EntityState = dataState
   End If
   If Me.EntityState = EntityStateEnum.Unchanged Then
      Me.EntityState = dataState
   End If
End Sub
C#の場合
private void DataStateChanged(EntityStateEnum
   dataState, string propertyName)
{
   // Raise the event
   if (PropertyChanged != null &&
      propertyName != null)
   {
      PropertyChanged(this, new
         PropertyChangedEventArgs(propertyName));
   }
   // If the state is deleted, mark it as deleted
   if (dataState == EntityStateEnum.Deleted)
   {
      this.EntityState = dataState;
   }
   if (this.EntityState ==
      EntityStateEnum.Unchanged)
   {
      this.EntityState = dataState;
   }
}

 このDataStateChangedメソッドは、PropertyChangedイベントを起動し、適切なデータ状態を設定します。DataStateChangedメソッドは、次に示すように、ビジネスオブジェクト内の各プロパティのプロパティプロシージャで呼び出されます。

VBの場合
Public Property LastName() As String
   Get
      Return _LastName
   End Get
   Set (ByVal value as String)
      If String.IsNullOrEmpty(value) Then
         Throw New Exception( _
            "The Last Name cannot be empty")
      End If
      If value <> _LastName Then
         Me.DataStateChanged( _
            EntityStateEnum.Modified, _
            "LastName")
      End If
      _LastName = value
   End Set
End Property
C#の場合
public string LastName
{
   get { return _LastName;}
   set
   {
      if (String.IsNullOrEmpty(value))
      {
         throw new Exception(
            "The Last Name cannot be empty");
      }
      if (value != _LastName)
      {
         this.DataStateChanged(
            EntityStateEnum.Modified,
            "LastName");
      }
      _LastName = value;
   }
}

 基本のデータ管理コードを実装した後で、必要に応じて、追加の機能を実装することができます。例えば、次のような単純な読み取り専用のIsDirtyプロパティを追加できます。

VBの場合
Public ReadOnly Property IsDirty() As Boolean
   Get
      Return Me.EntityState <> _
         EntityStateEnum.Unchanged
   End Get
End Property
C#の場合
public Boolean IsDirty
{
   get{ return
      this.EntityState!=EntityStateEnum.Unchanged;}
}

 このIsDirtyプロパティをユーザーインターフェイスのClosingイベントの中で呼び出せば、ユーザーに変更の保存確認を求めるべきどうかを判別できます。

 前述の通り、この一連のデータ管理コード(最後のIsDirtyプロパティも含む)を1つの基本クラスに追加し、この基本クラスをすべてのビジネスオブジェクトに継承させるという実装方法も考えられます。そうすれば、すべてのビジネスオブジェクトにかかわる一連の基本機能を1つのコードセットで提供することができます。

リストの管理

 アプリケーションのユーザーインターフェイスに、項目をリストしなければならないことはよくあります。例えば、すべての顧客をコンボボックスに表示して、その中から編集する顧客を選択する場合が考えられます。顧客の種類、状態、支払い予定のリストを表示することもあるでしょう。顧客が購入したすべての製品のリストをグリッドで表示する場合もあるかもしれません。

 最初に、適度なサイズの項目リストを実装する方法について見ていきましょう。このリストでは、項目のいくつかのプロパティを表示し、場合によっては編集する必要があります。ここでは、顧客が購入した項目のリストをグリッドで表示することにします。

 まずプロジェクトにPurchasedItemクラスを追加し、ItemNameDescriptionItemIDなどの適当なプロパティを設定します。CustomerクラスのCreateメソッドに似たCreateメソッドを実装しますが、今回のメソッドでは、IDではなくDataRowを渡します(このクラスの実装については、ダウンロードサンプルを参照してください)。

 プロジェクトにPurchasedItemCollectionクラスを追加し、このクラスをBindingList(Of T)から継承します。BindingList汎用コレクションは、System.ComponentModelにあることに注意してください。

VBの場合
Public Class PurchasedItemCollection
Inherits BindingList(Of PurchasedItem)
C#の場合
class PurchasedItemCollection :
   BindingList<PurchasedItem>

 Factoryパターンに従い、コレクションエントリを作成するCreateメソッドを実装します。

VBの場合
Public Shared Function Create(ByVal CustomerID _
   As Int32) As PurchasedItemCollection
   Dim oCollection As New PurchasedItemCollection
   Dim dt As DataTable
   ’ Perform the appropriate retrieve
   dt = oCollection.Retrieve(CustomerID)
   ’ For each row, create an object in the list
   For Each dr As DataRow In dt.Rows
      Dim oItem As PurchasedItem
      oItem = PurchasedItem.Create(dr)
      oCollection.Add(oItem)
   Next
   Return oCollection
End Function
C#の場合
public static PurchasedItemCollection Create(
   Int32 CustomerID)
{
   PurchasedItemCollection oCollection =
      new PurchasedItemCollection();
   // Perform the appropriate retrieve
   DataTable dt =
      oCollection.Retrieve(CustomerID);
   // For each row, create an object in the list
   foreach (DataRow dr in dt.Rows)
   {
      PurchasedItem oItem =
         PurchasedItem.Create(dr);
      oCollection.Add(oItem);
   }
   return oCollection;
}

 CustomerクラスのRetrieveメソッドに似たRetrieveメソッドを追加します(このメソッドの実装については、ダウンロードサンプルを参照してください)。

 両クラスの実装が完成したら、プロジェクトをビルドして、[Data Sources]ウィンドウから新しいクラスにアクセスできるようにします。次に、Customerクラスの場合と同じステップで、PurchasedItemCollectionクラスのデータソースを作成します。必ずDataGridViewを既定として設定し、PurchasedItemCollectionデータソースをフォームの下部にドラッグ&ドロップします。これにより、PurchasedItemクラスで定義した各プロパティを列として持つグリッドが表示されます。

 フォームのLoadイベントの中で、PurchasedItemCollectionBindingSourceのデータソースを、PurchasedItemCollectionクラスのインスタンスに設定します。

VBの場合
Private Sub CustomerWin_Load(ByVal sender As _
   Object, ByVal e As System.EventArgs) _
   Handles Me.Load
   Dim CustomerInstance As Customer
   CustomerInstance = Customer.Create(1)
   CustomerBindingSource.DataSource = _
      CustomerInstance
   Dim ItemCollection As PurchasedItemCollection
   ItemCollection = _
      PurchasedItemCollection.Create(1)
   PurchasedItemCollectionBindingSource. _
      DataSource = ItemCollection
End Sub
C#の場合
private void CustomerWin_Load(object sender,
   EventArgs e)
{
   Customer CustomerInstance =
      Customer.Create(1);
   customerBindingSource.DataSource =
      CustomerInstance;
   PurchasedItemCollection ItemCollection =
      PurchasedItemCollection.Create(1);
   purchasedItemCollectionBindingSource.
      DataSource = ItemCollection;
}

 BindingList(Of T)を継承するクラスへのオブジェクトバインディングは、項目のリストを表示および管理するのに非常に適しています。ただし、リスト内のすべての項目に対して、インスタンスが作成および設定されることに注意してください。この例のように、適度な数の項目であれば問題ありません。しかし、リスト内の項目数が数十万にも及ぶ場合は、適切に動作しない可能性があります。

 例えば編集対象の顧客を選択するための顧客名リストのように、大量の読み取り専用のキー値が必要な場合には、値ごとにインスタンスを作成するのは非効率的に感じられます。この場合は、キー値のDataTableに直接バインドしたいと思うかもしれません。しかし、この場合でも、ちょっとしたテクニックによって、一種のオブジェクトバインディングを引き続き使用することができます。

 そのためには、CustomerListクラスをプロジェクトに追加し、CustomerIDFullNameなど、キー値の名前に一致するプロパティを用意します。また、キー値のDataTableを返すCustomerDataTableプロパティも用意します。最後に、Retrieveメソッドを実装して、DataTableを取得します。リスト3およびリスト4は、結果のコードを示しています。

リスト3 オブジェクトバインディングを利用してDataTableとのバインドを実現するクラス(VB)
Public Class CustomerList
   Public ReadOnly Property FullName() As String
      Get
         Return ""
      End Get
   End Property
   Public ReadOnly Property CustomerID() As Int32
      Get
         Return 0
      End Get
   End Property
   Public ReadOnly Property CustomerDataTable() As DataTable
      Get
         Return Retrieve()
      End Get
   End Property
   Private Function Retrieve() As DataTable
      Dim dt As New DataTable
      dt.Columns.Add("FullName")
      dt.Columns.Add("CustomerID")
      dt.Rows.Add("Einstein, Albert", 1)
      dt.Rows.Add("Curie, Marie", 2)
      dt.Rows.Add("Brahe, Tycho", 3)
      dt.Rows.Add("Faraday, Michael", 4)
      Return dt
   End Function
End Class
リスト4 オブジェクトバインディングを利用してDataTableとのバインドを実現するクラス(C#)
class CustomerList
{
   public string FullName
   {
      get { return "";}
   }
   public Int32 CustomerID
   {
      get { return 0;}
   }
   public DataTable CustomerDataTable
   {
      get { return Retrieve();}
   }
   private DataTable Retrieve()
   {
      DataTable dt = new DataTable();
      dt.Columns.Add("FullName");
      dt.Columns.Add("CustomerID");
      dt.Rows.Add("Einstein, Albert", 1);
      dt.Rows.Add("Curie, Marie", 2);
      dt.Rows.Add("Brahe, Tycho", 3);
      dt.Rows.Add("Faraday, Michael", 4);
      return dt;
   }
}

 プロジェクトをビルドし、CustomerListクラスのデータソースを作成します。[Data Sources]ウィンドウ内で、FullNameプロパティをコンボボックスとして表示するように設定します。次に、FullNameプロパティをフォームにドラッグします。図2のように表示されます。

図2 コンボボックスを使って、より複雑なデータバインディングを実現する。
図2 コンボボックスを使って、より複雑なデータバインディングを実現する。

 コンボボックスのスマートタグをクリックして、コンボボックスのプロパティを設定します。DisplayMemberプロパティをFullNameに、ValueMemberプロパティをCustomerIDに設定します。

 最後に、ユーザーインターフェイスを修正して、コンボボックスに値を読み込むためのコードと、ユーザーがコンボボックスから顧客を選択したときに他のコントロールの内容を更新するためのコードを追加します(以前に作成したLoadイベントをコメントアウトするか、削除するのを忘れないようにしてください)。

VBの場合
Private Sub CustomerWin_Load(ByVal sender As _
   Object, ByVal e As System.EventArgs) _
   Handles Me.Load
   ’ Bind the list
   Dim oList As New CustomerList
   CustomerListBindingSource.DataSource = _
      oList.CustomerDataTable
End Sub
Private Sub _
   FullNameComboBox_SelectionChangeCommitted( _
   ByVal sender As Object, _
   ByVal e As System.EventArgs) _
   Handles FullNameComboBox.SelectionChangeCommitted
   Dim oCustomer As Customer
   oCustomer = Customer.Create(
      FullNameComboBox.SelectedValue)
   CustomerBindingSource.DataSource = oCustomer
   Dim oCollection As PurchasedItemCollection
   oCollection = PurchasedItemCollection.Create( _
      FullNameComboBox.SelectedValue)
   PurchasedItemCollectionBindingSource. _
      DataSource = oCollection
End Sub
C#の場合
private void CustomerWin_Load(object sender,
   EventArgs e)
{
   CustomerList oList = new CustomerList();
   customerListBindingSource.DataSource =
      oList.CustomerDataTable;
}
private void
   fullNameComboBox_SelectionChangeCommitted(
      object sender, EventArgs e)
{
   Int32 selectedID = Convert.ToInt32(
      fullNameComboBox.SelectedValue);
   Customer oCustomer =
      Customer.Create(selectedID);
   customerBindingSource.DataSource =
      oCustomer;
   PurchasedItemCollection oCollection =
      PurchasedItemCollection.Create(selectedID);
   purchasedItemCollectionBindingSource.
      DataSource = oCollection;
}

 これは基本的に見せかけのオブジェクトバインディングで、オブジェクトにバインドしていると思わせていますが、実際のバインド先はDataTableです。この方法は、クラスのインスタンスは不要だが、どのデータソースでもオブジェクトバインディングを使用するように統一したい場合に、非常に効果的な方法です(データソースの混在を気にしない場合は、ここに示す「見せかけの」オブジェクトバインディングではなく、データベースバインディングを使ってTableAdapterに直接バインドすることもできます)。

 この例の場合、顧客が多数に及ぶ可能性があるため、DataTableへのバインドには意味があります。また、ワークフローの観点からは、ユーザーが一度に編集する顧客数はわずかであることが予想されます。従って、数十万もの個別のインスタンスを作成し、それぞれのインスタンスにバインドするのは無駄が多すぎます。

 この「見せかけのオブジェクトバインディング」の手法は、顧客の種類、状態、支払い予定などのリストが必要なときにも使えます。このようなケースでは、リストは常に読み取り専用であり、インスタンスを本当に作成して管理する必要はありません。

 例として、CustomerクラスにStateプロパティを追加してみましょう。プロジェクトをビルドすると、[Data Sources]ウィンドウにStateが追加されます。次に、本稿で作成したCustomerListクラスに似たStateListクラスをビルドします。このStateListクラスは、状態のリストを管理します。

 このようなリストを使用する場合は、コンボボックスの3つのプロパティを操作する必要があります。このプロパティはすべて、コンボボックスのスマートタグからアクセスできます。

プロパティ名説明
DisplayMemberコンボボックスに表示する値のソースとなるプロパティ。StateListクラスのプロパティを指定する必要があります("StateName"など)。
ValueMemberコンボボックスの選択値を表すときに使用するプロパティ。StateListクラスのプロパティを指定する必要があります("StateID"など)。
SelectedValueユーザーの選択値の代入先となるバインディングソースおよびプロパティ。Customerクラスのプロパティ(ひいてはCustomerBindingListのプロパティ)を指定する必要があります("State"など)。

 このように、すべての型コードに対してクラスとインスタンスを作成しなくても、完全な機能を備えたオブジェクトバンディングを実現できます。

ユーザーインターフェイスに関する注意

 オブジェクトバインディングに関連して、ユーザーインターフェイスにはいくつか注意を要することがあります。例えば、ラジオボタンにバインドしたり、親プロパティを子データのグリッドに追加する簡単な方法はありません。最後に、この点に触れておきたいと思います。

 ラジオボタンにバインドするには、「RadioButtonList」というようなコントロールを使用するのが最も簡単です。しかし、Windows Formsにはこのようなコントロールがありません。一連のラジオボタンにバインドするには、ラジオボタンごとに個々のビジネスオブジェクトプロパティを作成しなければなりません。

 例えば、顧客が請求書を受け取る方法(電子メール、FAX、郵送のいずれか)を定義する一連のラジオボタンがあるものとします。これらにバインドするには、3つの個別のブール型のビジネスオブジェクトプロパティ(E-mail、FAX、Postal)を作成し、各プロパティを個々のラジオボタンにバインドする必要があります。各プロパティのプロパティプロシージャでは、このプロパティがtrueの場合は他のプロパティ値をfalseに設定するコードを追加します(この実装については、ダウンロードサンプルを参照してください)。

 ユーザーインターフェイスに関する2番目の注意事項は、親情報を子データのグリッドに簡単に表示できないということです。例えば、すべての請求書をリストするユーザーインターフェイスを作成するときに、図2のCustomerフォームと同様のグリッドを配置する一方で、他の詳細情報はフォームにいっさい表示しないというデザインにしたとします。このグリッドをユーザーが実用的に使うためには、関連するCustomerインスタンスからCustomerの名前を表示する必要があります。

 そのためには、PurchasedItemビジネスオブジェクトから親オブジェクトCustomerを参照して、Customerインスタンスを作成するのが一番分かりやすいでしょう。この参照は可能であり、[Data Sources]ウィンドウにもCustomerインスタンスが表示されていますが、グリッドからそのCustomerインスタンスにアクセスすることはできません。

 これに対処するには、PurchasedItemビジネスオブジェクトにCustomerNameプロパティを追加するしかありません。このプロパティのプロパティプロシージャで、関連するCustomerの顧客名を取得します。具体的な例については、ダウンロードサンプルを参照してください。

 オブジェクトバインディングは、ユーザーインターフェイス上のコントロールをビジネスオブジェクトプロパティに結び付ける、非常に論理的なメカニズムを提供します。ここで紹介したようなヒントとテクニックを実装することによって、オブジェクトバインディングを大いに活用できます。

 オブジェクトバインディングは完全ではありませんが、この世に完全なものなどないのですから、どんどん使ってみることをお勧めします。

著者紹介

Deborah Kurata(Deborah Kurata)
ビジネスの構想をMicrosoft .NETテクノロジで実現することに力を注ぐコンサルティング会社InStep Technologies Inc.の共同設立者。優れた.NETアプリケーションの設計、デザイン、開発に15年以上従事。『Doing Objects in Visual Basic 6.0』(SAMS)や『Doing Web Development: Client-Side Techniques』(APress)など複数の著作がある。INETA Speaker’s Bureauのメンバーであり、数多くの講演を行っている。

Copyright 2008 Jupitermedia Corporation All Rights Reserved.http://www.internet.com/