japan.internet.comThe Internet & IT Network
Twitter
RSS
  • ニュース
  • コラム
  • リサーチ
  • ヘッドライン
  • 特集
  • ブログ
  • プレスリリース
  • 専門チャンネル
  • イベント
  • ランキング
  • ニュースメール
2009年11月24日
文字サイズ文字サイズ小文字サイズ中文字サイズ大
事業仕分けによる次世代スーパーコンピューターの開発予算削減について、どうお考えですか?
賛成
反対
どちらとも言えない
投票締切 11/30 12:00
デベロッパー コラム2006年6月27日 10:00
15 seconds
15 seconds japan.internet.com 編集部(japan.internet.com)メールホームrss
米国 WebMediaBrands が運営する、
Microsoft のインターネットソリューションで作業する開発者向けの、フリーのリソースを提供するサイト。

Visual Studio 2005のフォームにおけるBindingNavigatorの拡張

海外海外internet.com発の記事

はじめに

 Visual Studio 2005では、WindowsアプリケーションとSmart Clientアプリケーションを短期間で構築できるように、さまざまな点がすばらしく向上しています。[Data Sources]パネルを使えば、各種コントロールをフォームに配置し、それらをBindingSourceコンポーネントでバインドするという操作が自動で行えます。BindingNavigatorコンポーネントには非常に大きな可能性が隠されており、ほんの少しの拡張を加えるだけで、ナビゲーション機能とデータアクセスに関するCRUD(Create、Read、Update、Delete=作成、更新、取得、削除)機能をほぼ自動的に実装することができます。本稿では、BindingNavigatorコンポーネントの機能を拡張するユーザーコントロールの作成方法を示します。

 以前の記事では、Visual Studio 2005のRAD機能に焦点を当て、ビジネス層とデータアクセス層を自動生成することですばやくN階層のWindowsアプリケーションを作成する方法を説明しました。本稿では、以前の記事で取り上げた概念に基づいて、より迅速なアプリケーション開発を実現するコードテンプレートとユーザーコントロールのシンプルなアーキテクチャを紹介します。

 今回のアーキテクチャでは、ユーザーコントロール内のBindingNavigatorコンポーネントを拡張して、編集済みデータの保存機能を自動生成するようにしています。そのためには、Bining Sourceコントロールを拡張し、インターフェイステンプレートをデータアクセスロジックに追加する必要があります。

Visal Studio 2005のBindingNavigatorコンポーネント

 新しくVisual Studio 2005のRADツールに加わったこの機能を理解するために、実際に使ってみることにしましょう。新しいアプリケーションを作成し、Northwindデータベースの「Customer」テーブルと「Order」テーブルを使用して、新しいデータソース(DataSet)を追加します(詳しい手順は、筆者の前の記事を参照してください)。

 [Data Sources]パネルの[Typded Dataset]からテーブルやフィールドをドラッグしてフォームにドロップすると、すぐに使えるバインド済みのデータアクセスロジックが作成されます。先ほど作成した新しいデータソースの「Customer」テーブルをフォームにドラッグしてください。すると、ウィザードによってそのデータセットのインスタンスが作成され、さらに、そのデータセット内のテーブルにリンクされるBindingSourceコンポーネントと、このバインディングソースにリンクされるBindingNavigatorコンポーネントが作成されます。データセットがプロジェクトにとってローカルである場合は、さらにテーブルアダプタのインスタンス化が行われ、Fillメソッドの呼び出しが追加されます(ただし、オブジェクトデータソースを使っている場合は、このコードを手作業で追加する必要があります)。

 BindingNavigatorコンポーネントによって、ユーザーがレコード間をナビゲートするための一連のツールがフォームの最上部に配置されます。最初のレコード、前のレコード、次のレコード、最後のレコードに移動するためのボタンと、インデックス番号を指定して特定のレコードに直接移動するためのフィールドがあります。

 レコードの追加と削除、そして編集結果の保存を行うためのボタンも追加されます。保存用のボタン(フロッピーディスクのアイコン)は、標準的なナビゲーションバーの一部ではなく、データソースウィザードによって特別に追加されます。本稿を執筆するきっかけとなったのは、この保存ボタンです。

 データセットがプロジェクトにローカルである場合は、保存ボタンがナビゲーションバーに追加されるときに、適切なコードが生成されます(オブジェクトデータソースを使用する場合は、この限りではありません)。「Customer」テーブル用には、次のメソッドが追加されます。

Private Sub CustomersBindingNavigatorSaveItem_Click( _
    ByVal sender As System.Object, ByVal e As System.EventArgs) _
    Handles CustomersBindingNavigatorSaveItem.Click
    Me.Validate()
    Me.CustomersBindingSource.EndEdit()
    Me.CustomersTableAdapter.Update(Me.NorthwindDataSet.Customers)
End Sub

 Validateコマンドは、コントロールの編集を終わらせます。バインディングソースのEndEditメソッドは、コントロール内の編集済みデータをすべてデータセットのレコードに書き戻します。テーブルアダプタのUpdateコマンドは、更新済みレコードをデータベースに反映させます。

 この新しいコントロールは大幅な拡張であり、通常はこれで、データバウンドフォームに関するナビゲーションとCRUDのニーズの大部分を満たすことができます。しかし、データバウンドフォームのニーズすべてに対処する完全なツールを作るには、いくつか足りない点があります。克服しなければならない問題としては、次のようなものがあります。

  1. オブジェクトデータソースを使う場合は、データテーブルへのデータ読み込みおよびデータテーブルの更新を行うためのコードがウィザードによって自動生成されません。このコードは手作業で追加する必要があります。
  2. データを編集したときは、ユーザーが忘れずに保存ボタンをクリックしなければなりません。データが変更されても、IsDataDirtyフラグはセットされません、そのため、ユーザーがレコードを変更し、保存しないままでいても警告メッセージは表示されません。
  3. レコードを削除する場合も、警告または確認メッセージは表示されません。
  4. ナビゲーションバーには、特定レコードをルックアップする手段も必要です。

汎用的なテーブル更新インターフェイス

 1番目の問題に対処するには、データテーブルのデータ読み込み(Fill)と更新(Update)を実行する、汎用的なコードを作成する必要があります。筆者の以前の記事のパート3では、データアクセスロジックをビジネス層にラップしてしまうコードを紹介しました。その後、この構造に少し手を加え、データテーブルのサブクラスを利用してメソッドの一貫性を高めるように書き換えました。この一貫性を実現するには、個々のテーブルクラスがラッパー内で実装するインターフェイスを作成する必要があります。

  1. ソリューションエクスプローラでプロジェクトを右クリックし、[Add]-[New Item]を選択します(多層アプリケーションの場合は、これはビジネス層で行うべき処理ですが、話を簡単にするために、ここではまずWindowsプロジェクトに格納して、後から移動することにします)。
  2. [Choose a Class]を選び、「_Interface」と名前を付けます(先頭にアンダーバーを付けることで、ソート時にこのクラスが一番上に来るようにしています)。このクラスを、汎用的なメソッドを持ち、プログラム全体を通じてアクセスできるユーティリティ的なクラスにします。
  3. _Interfaceクラス内に、FillメソッドとUpdateメソッドを要求する「ITableUpdate」という名前のインターフェイスを作成します。
  4. Public Class _Interface
        ’’’ <summary>
        ’’’ Interface for Data table in a dataset
        ’’’  to standardize fill and update methods
        ’’’ </summary>
       Public Interface ITableUpdate
            Sub Fill()
            Sub Update()
        End Interface
    End Class
    

 データセットの背後にあるパーシャルクラス(データセットを右クリックして[View Code]をクリックすると表示されます)内で、データセット内の更新可能なテーブル1つ1つに対して次のテンプレートコードを作成します。この例では、「NorthwindDataSet」テーブルに関するCustomerクラスを実装しています。

Partial Class NorthwindDataSet
    Shared taCustomer As New _
        NorthwindDataSetTableAdapters.CustomersTableAdapter

    Partial Class CustomersDataTable
        Implements _Interface.ITableUpdate

        Public Sub Fill() Implements _Interface.ITableUpdate.Fill
        taCustomer.Fill(Me)
        End Sub

        Public Sub Update() Implements _Interface.ITableUpdate.Update
            taCustomer.Update(Me)
        End Sub
    End Class
End Class

 このインターフェイスを実装することにより、どのテーブルでも汎用的なUI呼び出しが使えるようになります。つまり、Saveメソッドは、「Customer」テーブル用の更新メソッドを呼び出す代わりに、汎用的なコードを使って更新を呼び出せるようになります。保存ルーチンを一度書いておけば、個々のテーブル用にいちいちコードを書き直さなくても、同じコードを再利用できるというわけです。

BindingNavigatorコントロールの拡張

 ナビゲーションツールバーの問題を解決して機能を拡張する方法の1つは、BindingNavigatorコントロールをベースとしたユーザーコントロールを作成し、必要なプロパティとメソッドを追加することです。

  1. Windowsプロジェクトを右クリックし、[Add]-[New Item]をクリックします 項目のリストから[User Control]を選び、「exBindingNavigator.vb」と名前を付けます(この拡張を他のアプリケーションでも使いたい場合は、ユーザーコントロールとカスタムコントロールを独立したプロジェクトに配置する必要があります。今回のサンプルでは、Windowsプロジェクトの中にそのまま残します)。
  2. 空のコンテナを持った空白のコントロールが作成されます。右下の角を、BindingNavigatorツールバーと同じ大きさになるまでドラッグします。
  3. [Customer]フォーム(あるいはデータソースウィザードがナビゲーションコントロールの作成に利用した任意のフォーム)に戻り、ナビゲーションツールバーをコピーします。
  4. ユーザーコントロールに戻り、ナビゲーションバーをコンテナに貼り付けます。
  5. プロパティボックスで、Navigatorの名前を「CustomerBindingNavigator」から「GenericBindingNavigator」などの汎用的な名前に変更します。
  6. 保存ボタン(フロッピーディスクのアイコン)をクリックし、プロパティパネルで名前をより汎用的な「BindingNavigatorSaveItem」に変更します。
  7. フォーム上で右クリックし、[View Code]をクリックしてコードページを開きます。Sub Newコンストラクタを追加し、コンストラクタ内でDockプロパティをフォームの上端にセットします。
  8. Public Class exBindingNavigator
        Public Sub New()
            ’ This call is required by the Windows Form Designer.
            InitializeComponent()
    
            ’ Add any initialization after the InitializeComponent() call.
            Me.Dock = DockStyle.Top
        End Sub
    End Class
    
  9. まず必要なのは、ナビゲーションバーのバインディングソースの参照を追跡するプロパティです。バインディングソースを定義するときに必要な作業の1つは、基となるテーブルへの参照を定義することです。ここで、そのテーブルへの参照を取得するコードを含めることもできます。ただし場合により、バインディングソースが定義されないうちはデータリソースも定義されないか、あるいはデータソースが変更される可能性があります。そのため、DataSourceChangedイベントを処理するイベントハンドラを追加して、テーブル参照をセットするメソッドを呼び出します。テーブルに自動的にデータを読み込むために、格納元のフォームへの参照を取得し、Form Loadイベントをサブスクライブします。
  10. Private WithEvents _BindingSource As BindingSource
    Public Property BindingSource() As BindingSource
        Get
            Return _BindingSource
        End Get
        Set(ByVal value As BindingSource)
            GenericBindingNavigator.BindingSource = value
            _BindingSource = value
            If Not _BindingSource Is Nothing Then
                ’subscribe to the events in case not yet set
                AddHandler _BindingSource.DataSourceChanged, _
                    AddressOf bs_DataSourceChanged
                ’get a reference to the table now
                bs_DataSourceChanged(New Object, New EventArgs)
            End If
        End Set
    End Property
    
  11. 上のDataSourceChangedイベントを処理するメソッドを追加します。
  12. Private Sub bs_DataSourceChanged(ByVal sender As Object, _
                                     ByVal e As EventArgs)
        If Not _BindingSource Is Nothing Then
            _DataTable = GetTableFromBindingSource( _
                GenericBindingNavigator.BindingSource)
            If Not _DataTable Is Nothing Then
                ’if child BS, get reference to parent BS
                Dim testBS As BindingSource = _
                    TryCast(GenericBindingNavigator.BindingSource.DataSource, _
                            BindingSource)
                If Not testBS Is Nothing Then
                   ’call the getter to capture event 
                    ParentBindingSource = testBS
                End If
            End If
        End If
    End Sub
    
    上記のような形でBindingSourceからテーブル参照を取得するには、もう1つのメソッドが必要です。BindingSourceにはDataSourceDataMenberという2つのプロパティがあり、それによってどのテーブルにバインドされるかが決まります。大抵の場合、DataSourceにはデータセットのインスタンスを指定し、DataMemberにはデータセット内のテーブルの名前を指定します。しかし、親子関係が確立されている状況では、DataSourceにもう1つのBindingSourceを指定し、DataMemberにリレーションの名前を指定することができます。この場合、テーブルを導き出すには、少し計算が必要です。
  13. 次に、GetTableFromBindingSourceというメソッドを追加します。このメソッドは、BindingSourceをパラメータとして受け取り、データテーブルへの参照を戻します。
  14. Public Function GetTableFromBindingSource(ByVal bs As BindingSource)
        ’get a reference to the dataset
        Dim ds As DataSet, dt As DataTable
        ’try to cast the data source as a binding source
        Dim bsTest As BindingSource = bs
        Do While Not TryCast(bsTest.DataSource, BindingSource) Is Nothing
            ’if cast was successful, walk up the chain until dataset is reached
            bsTest = CType(bsTest.DataSource, BindingSource)
        Loop
        ’since it is no longer a binding source, it must be a dataset
        If TryCast(bsTest.DataSource, DataSet) Is Nothing Then
        ’Cast as dataset did not work
            Throw New ApplicationException("Invalid Binding Source ")
        End If
        ds = CType(bsTest.DataSource, DataSet)
        ’check to see if the Data Member is the name of a table in the dataset
        If ds.Tables(bs.DataMember) Is Nothing Then
            ’it must be a relationship instead of a table
            Dim rel As System.Data.DataRelation = ds.Relations(bs.DataMember)
            If Not rel Is Nothing Then
                dt = rel.ChildTable
            Else
                Throw New ApplicationException("Invalid Data Member")
            End If
        Else
            dt = ds.Tables(bs.DataMember)
        End If
        If TryCast(dt, ITableUpdate) Is Nothing Then
            Throw New ApplicationException("Table " & dt.TableName & _
                " does not implement ITableUpdate interface")
        End If
        Return dt
    End Function
    
    テーブルを取得するには、まずデータセットへの参照が必要です。DataSourceがBindingSourceである場合は、基になるデータセットを取得できる時点まで道筋をさかのぼっていく必要があります。ひとたびデータセットを取得すれば、DataMemberがデータセット内のテーブルを指しているかどうかを確認できます。テーブルでない場合は、DataMemberはリレーションを指しています。データセット内の一連のリレーションのなかから該当するリレーションを探し、そのリレーションのChildTableプロパティからテーブル参照を取得することができます。
    テーブルへの参照を取得した後は、ポリモーフィズムを利用して、この参照を先ほど作成したインターフェイスにキャストすることができます。正しくキャストされない場合は、そのデータテーブルがインターフェイスを正しく実装していないということであり、例外をスローする必要があります。キャストされる場合は、汎用的な更新ルーチンを呼び出してデータを保存することができます。
  15. 以下のメソッドをexBindingNavigatorクラスに追加して、保存ボタンのクリックイベントを処理します(保存ボタンをダブルクリックして、コードスタブを生成することもできます)。
  16. Private Sub SaveItem_Click(ByVal sender As System.Object, _
                               ByVal e As System.EventArgs)
        Me.Validate()
        _BindingSource.EndEdit()
        ’cast table as ITableUpdate to get the Update method
        CType(_DataTable, _Interface.ITableUpdate).Update()
        IsDataDirty = False
    End Sub
    
  17. Form Loadイベントを処理するメソッドをもう1つ追加すると、フォームが開いたときにテーブルに自動的に値を読み込むことができます。テーブルは大抵の場合、必要な場合にロジックを使って値を読み込むことが多いので、この処理は必須というわけではありません。そのため、この機能を有効にするかどうかを判別するプロパティを追加して、有効/無効を開発者が選択できるようにします。
  18. Private _AutoFillFlag As Boolean = True
    Public Property AutoFillFlag() As Boolean
        Get
            Return _AutoFillFlag
        End Get
        Set(ByVal value As Boolean)
            _AutoFillFlag = value
        End Set
    End Property
    
    Private Sub Form_Load(ByVal sender As Object, ByVal e As EventArgs)
        If _AutoFillFlag Then
            ’cast table as ITableUpdate to get the Fill method
            CType(_DataTable, _Interface.ITableUpdate).Fill()
        End If
    End Sub
    
  19. 最後に、親フォームのLoadイベントをサブスクライブすることによって、これらすべてが実行されるようにします。ユーザーコントロールのLoadイベントが起こると、親フォームへの参照が取得できるようになります。
  20. Private Sub exBindingNavigator_Load(ByVal sender As System.Object, _
        ByVal e As System.EventArgs) Handles MyBase.Load
        ’get the reference to the hosting form
        Dim frm As Object = CType(Me, ContainerControl).ParentForm
        While TryCast(frm, System.Windows.Forms.Form) Is Nothing
            ’if not a form, walk up chain
            If Not TryCast(frm, System.ComponentModel.Container) Is Nothing Then
                frm = CType(frm, ContainerControl).Parent
            Else
                frm = CType(frm, Control).Parent
            End If
        End While
        _Form = CType(frm, System.Windows.Forms.Form)
        ’add the handler for the Form Load to fill the table
        AddHandler _Form.Load, AddressOf Form_Load
    End Sub
    

 このユーザーコントロールをフォームに追加し、BindingSourceプロパティをセットすると、保存ボタンはプログラムで使用するあらゆるデータセットとプロパティセットを扱えるようになり、テーブルに自動的に値が読み込まれるようになります(インターフェイスを実装している限り)。これで、1つ目の問題は解決です。

自動保存(AutoSave)機能の実装

 2つ目の問題は、1つ目よりも少し厄介です。BindingSourceコンポーネントには「データが修正されたかどうかを示すフラグ」がないため、これが大きな問題となっています。グリッドの場合は、RowLeaveイベントの際にIsRowDirtyフラグを確認することができますが、バインドの詳細に関するフォーマットコントロールには、それに相当するフラグがありません(このことをバグとして報告したところ、Microsoftからは、将来のバージョンで修正予定であるという返事が来ました)。というわけで、今回作成するコントロールでは、データが編集されたときにセットできるフラグを追加する必要がありました。

  1. 画面上で右クリックし、[View Code]をクリックしてコードビハインドページ(分離コードページ)を開きます。次のコードを追加して、このコントロールに必要となるIsDataDirtyプロパティを作成します。
  2. Private _IsDataDirty As Boolean = False
    Public Property IsDataDirty() As Boolean
        Get
            Return _IsDataDirty
        End Get
        Set(ByVal value As Boolean)
            _IsDataDirty = value
            If BindingNavigatorSaveItem.Enabled <> _IsDataDirty Then
                BindingNavigatorSaveItem.Enabled = _IsDataDirty
            End If
        End Set
    End Property
    
  3. このフラグをセットするには、BindingSourceのBindingCompleteイベントをインターセプトする必要があります。実際のところ、これは不経済なイベントです。というのは、レコードを変更するたびに、バインドされているコントロール1つ1つが呼び出されるためです。しかし、データが更新されてコントロールからデータセットに戻される際にもこのイベントが呼び出されます。つまり、本稿で必要となる情報を提供してくれるわけです。次のメソッドではBindingCompleteイベントを処理し、データが編集されている場合はプロパティをセットします。
  4. Private Sub BindingComplete(ByVal sender As Object, _
        ByVal e As BindingCompleteEventArgs)
        ’is this a return from the control to the dataset?
        If e.BindingCompleteContext = BindingCompleteContext.DataSourceUpdate Then
            ’was it successful and not for a read only control?
            If e.BindingCompleteState = BindingCompleteState.Success And _
                    Not e.Binding.Control.BindingContext.IsReadOnly Then
                IsDataDirty = True
            End If
        End If
    End Sub
    
  5. ここでHandles句を使ってイベントをサブスクライブすることも可能ですが、今回のサンプルではVisual Basic内でエラーが発生するのを回避するために、BindingSourceが定義されるのを待ってからハンドラを割り当てるようにします。BindingSourceのsetメソッドのハンドラセクションに、次のコードを追加してください。
  6.     AddHandler _BindingSource.BindingComplete, AddressOf BindingComplete
        AddHandler _BindingSource.PositionChanged, AddressOf bs_PositionChanged
    
  7. 2行目で追加するハンドラによって、ユーザーが新しいレコードに移動したときに前のレコードを保存できるようになります。レコードがいつ変更されたかを知るために使うことのできるイベントは、いくつかあります。例えば、CurrentChanged、CurrentItemChanged、PositionChangedなどです。ここで最良の選択は、PositionChangedです。これらのイベントに関する問題は、どれもが「過去形」で実行されることです。つまり、「レコードが既に変更されてしまってからイベントが呼び出される」ということです。そのため、レコードを変更せずに残しておく手段がありません。最善の措置は、前のレコードのデータを保存することです。
  8. 次のメソッドでは、バインディングソースを対象にPositionChangedイベントを処理し、フラグをチェックして、必要に応じて保存メソッドを呼び出します。
  9. Private Sub bs_PositionChanged(ByVal sender As Object, ByVal e As EventArgs)
        If (_IsDataDirty And Not _DataTable Is Nothing) Then
            SaveItem_Click(New Object(), New EventArgs())
        End If
    End Sub
    

保存するかどうかをユーザーに確認する

 3つ目の問題は、「変更されたデータを保存するか、変更前の状態に戻すか」という確認メッセージをユーザーに表示しなければならない場合が考えられるということです。もちろん、この確認メッセージの表示はオプション機能にすべきであるため、まずはAutoSaveというプロパティを追加する必要があります。これがfalseの場合は、保存するかどうかの確認メッセージがユーザーに表示されます。

  1. コードページのプロパティ定義の部分に次のコードを追加します。
  2. Private _AutoSaveFlag As Boolean
    Public Property AutoSaveFlag() As Boolean
        Get
            Return _AutoSaveFlag
        End Get
        Set(ByVal value As Boolean)
            _AutoSaveFlag = value
        End Set
    End Property
    
  3. 前述のPositionChangedイベント用のハンドラを修正して、このフラグをチェックし、フラグが有効である場合は保存確認メッセージを表示するようにします。
  4. Private Sub bs_PositionChanged(ByVal sender As Object, ByVal e As EventArgs) _
        Handles _BindingSource.PositionChanged
    
        If (_IsDataDirty And Not _DataTable Is Nothing) Then
            Dim msg As String = "Do you want to save edits to the previous record?"
            If _AutoSaveFlag Or MessageBox.Show(msg, "Confirm Save", _
                MessageBoxButtons.YesNo) = DialogResult.Yes Then
    
                SaveItem_Click(New Object(), New EventArgs())
            Else
                _DataTable.RejectChanges()
                MessageBox.Show("All unsaved edits have been rolled back.")
                _IsDataDirty=False
            End If
        End If
    End Sub
    
  5. これで保存確認ができるようになりましたが、レコードの削除の際にも、削除を確認する必要があります。そのためには、まず標準の削除メソッドを無効にする必要があります。ユーザーコントロールのデザイナビューで、BindingNavigatorのツールストリップを選びます。プロパティパネルの[Items]セクションにある[DeleteItem]でドロップダウンリストを表示して、[(none)]を選びます。
  6. さらに、ユーザーに確認メッセージを表示した後にのみレコードを削除するメソッドが必要です。ツールバーにある[Delete]アイコン(赤い×)をダブルクリックしてコードスタブを作成し、次のコードを追加します。
  7. Private Sub BindingNavigatorDeleteItem_Click(ByVal sender As System.Object, _
        ByVal e As System.EventArgs) Handles BindingNavigatorDeleteItem.Click
    
        Dim msg As String = "Are you sure you want to delete the current record? "
        If _AutoSaveFlag Or MessageBox.Show(msg, "Confirm Delete", _
            MessageBoxButtons.YesNo) = DialogResult.Yes Then
    
            ’Delete the current record
            _BindingSource.RemoveCurrent()
            CType(_DataTable, Win._Interface.ITableUpdate).Update()
        End If
    End Sub
    

ルックアップリストの追加

 最後の問題は、「問題」というよりは「利便性を高めるための事柄」です。筆者の見解では、データ入力フォームでは大抵の場合、特定レコードをルックアップする手段が必要です。通常、このためにはコンボボックスの形式を使い、IDを値として、また、一意のテキストを表示番号として使用します。SelectedValueChangedイベントでは、BindingSourceの位置を選択したレコードの位置へと変更することができます。

 ナビゲーションバーにコンボボックスを追加するのは、いくぶん難しくなります。このコンボボックスはバインドできず、しかもこれはただのドロップダウンリストボックスであり、値メンバを持ちません。この不足を補うため、本稿ではハッシュテーブルを使って値メンバを格納します。

 ナビゲーションバーにコンボボックスを追加するには、次の手順を実行します。

  1. ユーザーコントロールのデザインビューのバーの右端にあるアイコンは、追加可能な項目を表示するドロップダウンリストです。まず[Label]を選ぶと、「ToolStripLabel1」という名前のラベルがバーに追加されます。
  2. この名前を「Lookup」に変更します(また、この値のプロパティを作成しておき、開発者がデザイン時に変更できるようにしておく必要があります)。
  3. ドロップダウンリストを再びクリックし、[Combo Box]コントロールを追加して、「ToolStripComboBox1」という項目を新規に追加します。
  4. このユーザーコントロールにプロパティをもう1つ追加して、開発者がどのフィールドを表示メンバにするかを選択できるようにします。
  5. Private _DisplayMember As String
    Public Property DisplayMember() As String
        Get
            Return _DisplayMember
        End Get
        Set(ByVal value As String)
            _DisplayMember = value
        End Set
    End Property
    
  6. このプロパティの既定値を、バウンドテーブル内の最初のテキスト型フィールドにします。そのためには次のコードをbs_DataSourceChangedメソッドに追加します。
  7. If Not _DataTable Is Nothing Then
        ’find the first text column
        For Each col As DataColumn In _DataTable.Columns
            If col.GetType().Equals(Type.GetType("System.String")) Then
                _DisplayMember = col.ColumnName
                Exit For
            End If
        Next
    End If
    
  8. ハッシュテーブルを定義し、コンボボックスに適切な値を読み込むためのBuildLookupListメソッドを追加します。
  9. Private ht As New System.Collections.Hashtable
    Public Sub BuildLookupList()
        If Not _DisplayMember Is Nothing Then
            ’fill both lookup box and hash table with values
            ToolStripComboBox1.Items.Clear()
            ht.Clear()
            If (BindingSource.List.Count > 0) Then
                ’get the primary key as value member (assumes single field key)
                Dim _valueMember As String = _DataTable.PrimaryKey(0).ColumnName
                ’temp change the sort of the binding source
                Dim tempSort As String = _BindingSource.Sort
                BindingSource.Sort = _DisplayMember + " ASC"
                ’step through the records in the binding source as filtered
                For Each drv As DataRowView In _BindingSource
                    If Not drv(_DisplayMember) Is Nothing Then
                        ToolStripComboBox1.Items.Add(drv(_DisplayMember))
                        Try
                            ht.Add(drv(_DisplayMember), drv(_valueMember))
                        Catch ’ignore dups
                        End Try
                    End If
                Next
                ’restore sort field
                BindingSource.Sort = tempSort
            End If
        End If
    End Sub
    
  10. このメソッドは、リストが変更されたときに呼び出される必要があります。BindingSourceメソッドのハンドラセクションにハンドラを1つ追加し、さらにもう1つ、「イベントを処理して上記のメソッドを呼び出すためのメソッド」を追加します。
  11. AddHandler _BindingSource.ListChanged, AddressOf bs_ListChanged
    
    Private Sub bs_ListChanged(ByVal sender As Object, _
        ByVal e As System.ComponentModel.ListChangedEventArgs)
    
        BuildLookupList()
    End Sub
    
  12. バインディングソースがリレーションに基づいている場合は、リストの内容は親の位置が変わるたびに変更されます。そのため、親バインディングソースを追跡するプロパティと、それを呼び出すメソッドを新たに追加する必要があります(この処理もListChangedハンドラで行いたくなるかもしれませんが、イベントの引数がそれぞれ違うという点に注意してください)。
  13. Private WithEvents _ParentBindingSource As BindingSource
    Public Property ParentBindingSource() As BindingSource
        Get
            Return _ParentBindingSource
        End Get
        Set(ByVal value As BindingSource)
            _ParentBindingSource = value
            If Not _ParentBindingSource Is Nothing Then
                AddHandler _BindingSource.PositionChanged, _
                    AddressOf parentBS_PositionChanged
            End If
        End Set
    End Property
    
    Private Sub parentBS_PositionChanged(ByVal sender As Object, _
        ByVal e As EventArgs)
    
        BuildLookupList()
    End Sub
    
  14. 親の位置が変わった場合に対処する、もう1つのイベントハンドラを追加します。
  15. Private Sub parentBS_PositionChanged(ByVal sender As Object, ByVal e As EventArgs)
        BuildLookupList()
    End Sub
    
  16. 開発者は、親バインディングソースのプロパティを設定することができます、そして、次のソースをbs_DataSourceChangedメソッドに追加することで、データテーブルにプロパティを割り当てるときにキャプチャできるようになります。
  17. ’if child BS, get reference to parent BS
    Dim testBS As BindingSource = TryCast(_BindingSource.DataSource, BindingSource)
    If Not testBS Is Nothing Then
        ParentBindingSource = testBS ’call the getter to subcribe to events
    End If
    
  18. この時点でフォームを実行するとリストは作成されますが、ユーザーが何かを選んでも何も起こりません。ユーザーがコンボボックスリストから項目を選ぶ際のイベントをキャプチャする必要があります。コンボボックスコントロールを選び(場合により、この作業は非常に厄介です。ラベルを選択して[Tab]キーを押してください)、次にプロパティパネルでイベントのセクションに移動(上部の稲妻アイコンをクリック)します。それから、[SelectedIndexChanged]イベントをクリックして、コードスタブを生成します。次のコードを追加してください。
  19. Private Sub ToolStripComboBox1_SelectedIndexChanged(ByVal sender As System.Object, _
        ByVal e As System.EventArgs) Handles ToolStripComboBox1.SelectedIndexChanged
    
        If (ToolStripComboBox1.Focused And ToolStripComboBox1.Text <> "") Then
            ’get the primary key field
            Dim _valueMember As String = _DataTable.PrimaryKey(0).ColumnName
            ’set the position by finding the primary key in the binding source
            _BindingSource.Position = BindingSource.Find(_valueMember, _
                ht(ToolStripComboBox1.Text))
        End If
    End Sub
    
  20. 最後の問題は、「レコードの選択後に[Next]または[Previous]ボタンを使って位置を移動すると、コンボボックス内のエントリがレコードの表示と同期しなくなる」ということです。次のメソッドを使えば、リストとレコードの同期をとることができます。
  21. ’make sure the same record shows in the combo box as is in the record
    Private Sub syncLookupCombobox()
        If (ToolStripComboBox1.Items.Count > 0 And _BindingSource.Position >= 0) Then
            ’get the display string for the current record in the binding source
            Dim lookup As String = CType(_BindingSource.Current, _
                DataRowView).Row(_DisplayMember).ToString()
            If (lookup.Length > 0) Then
                ToolStripComboBox1.SelectedIndex = _
                    ToolStripComboBox1.FindStringExact(lookup)
            End If
        End If
    End Sub
    
  22. このメソッドがPositionChangedイベントハンドラから呼び出されるようにします。
  23. 最後に、コンパイルしてフォームを実行してみましょう。フォームの背後に何もコードを書かなくても、すべてが問題なく機能します。

 このユーザーコントロールをさらに拡張して、フォーム用の他のCRUD型イベント(例えばユーザーがデータを編集するまでコントロールを無効にするなど)を実現することができます。新しいコントロールを実装するには、ウィザードによって配置されたBindingNavigatorを削除し、[Toolbox]パネルを開きます。すると、今回作成したユーザーコントロールがWinセクションに配置されています。このユーザーコントロールをフォームにドラッグし、BindingSourceプロパティに適切な値を指定します。

 親子関係を持つフォームでも、さまざまなナビゲーション機能を使用することができました。これを実際に試してみるには、次のようにします。

  1. まず、パネルコントロールをフォームの右側にドラッグします。
  2. [Data Sources]パネルを開いて「Customer」テーブルを展開し、子リレーションシップテーブルの[Orders]を選びます。このテーブルを、パネルにドラッグします。ウィザードによって、もう1つのバインディングソースとグリッドが追加されます。
  3. 必要に応じて、パネルとグリッドのサイズを変更します。グリッドよりも上の位置にあるパネルでは、少し余白を残してください。
  4. もう1つのexBindingNavigatorユーザーコントロールをパネルにドラッグします。このユーザーコントロールは最上部にドッキングします。
  5. ナビゲータのBindingSourceプロパティを、新しく「OrdersBindingSource」にセットします。

 これでフォームが実行できるようになり、ナビゲーションも以前と変わらず動作します。

結論

 BindingNavigatorコンポーネントは一度ユーザーコンポーネントにして、そこにプロパティとメソッドを追加するという方法で簡単に拡張できます。このユーザーコントロールをフォームに追加すると、データテーブルへのオートフィルや更新機能(ユーザーへの確認メッセージ付き)が実装されるほか、特定レコードのルックアップができるようになります。拡張したユーザーコントロールをコントロールライブラリプロジェクトに格納すると、任意のソリューションに追加できるようになり、データアクセスアプリケーションの開発速度がアップします。

 データアクセスロジックにインターフェイスを実装すると、データセット内の各テーブルで必要となるメソッドのスタブをすばやく作成できますが、それでも一部のコードは書く必要があります。ただ、これは非常に構造化されたコードなので、CodeDomやサードパーティ製のコード生成システムで簡単に生成できます。

 この方法論には、いくつかの問題がついて回ります(その大部分は、Visual Studio 2005自身の問題です)。ユーザーコントロールをフォームに追加した後、そのユーザーコントロールに何らかの変更を加えても、Visual Studio内でフォームが自動的にリロードされることはありません。フォームを一度閉じてから開き直し、デザインモードで表示する必要があります。ときには、フォームを正確に表示するためにプロジェクトをリビルドしなければならないこともあります。そのため、ユーザーコントロールをフォームで使用するのは、ユーザーコントロールを完全に作成し終えてからにしてください。

 この記事のコードを、ダウンロードサンプルとしてご用意してあります。このダウンロードファイルには、Northwindデータベースを使ったVBとC#の両方のサンプルが収められています。

著者紹介

David Catherman(David Catherman)
CMI Solutions所属。データベースアプリケーションのデザイン/開発に20年以上の経験を持ち、ここ4、5年は特にMicrosoft .NETとSQL Serverに仕事が集中している。現在はCMI Solutionsのアプリケーション設計者および上級開発者であり、Visual StudioとSQL Server 2005を使用している。.NETのMCPを取得し、現在MCSDを取得中。メールの宛先はDCatherman@CMiSolutions.com。
Graphic Design Forum
【Graphic Design Forum】
流動的媒体と静的媒体に関する見解(11月18日)
スマートにソーシャルウェブを構築しよう
スマートにソーシャルウェブを構築しよう
オバマ大統領も絶賛。メイヨークリニックのソーシャルメディアポリシー(11月24日)
アイレップの SEM フロンティア
アイレップの SEM フロンティア
検索技術の進化で広がる SEO 領域―2010年以降に要求される事は?(11月24日)
百式のネットビジネス研究
百式のネットビジネス研究
外国で見かけた標識を写真に撮ると翻訳してくれる iPhone アプリ「PicTranslator」(11月24日)
DevX
DevX
HTML 5のフォーム要素(11月24日)
エンジニア転職ノウハウ開発室
エンジニア転職ノウハウ開発室
エンジニア的「合わない」と思う瞬間/理系の人々(11月24日)
「IT の耳」
「IT の耳」
【書評】『Hyper-V スタートアップバイブル』――仮想化についてのすぐれた解説書(11月20日)
週刊-サイト別アクセス状況データ
週刊-サイト別アクセス状況データ
ビデオリサーチインタラクティブ調査(月間インターネットオーディエンスデータ)(11月19日)
海外ソーシャルウェブに学ぶ成功の秘訣
海外ソーシャルウェブに学ぶ成功の秘訣
ゲーム業界を襲う世界的な激震。ソーシャルゲーム急成長のインパクト(11月19日)
今さら聞けない初歩からのアクセス解析
今さら聞けない初歩からのアクセス解析
サイトリニューアル前のアクセス解析活用法(11月19日)
成約率、反応率を上げる Web 文章術
成約率、反応率を上げる Web 文章術
文章力を磨き、キャッシュを生み出す Web サイト に(11月19日)
Copyright 2009 Japan Internet.com K.K. All Rights Reserved.http://www.internet.com/