ルックアップリストの追加
最後の問題は、「問題」というよりは「利便性を高めるための事柄」です。筆者の見解では、データ入力フォームでは大抵の場合、特定レコードをルックアップする手段が必要です。通常、このためにはコンボボックスの形式を使い、IDを値として、また、一意のテキストを表示番号として使用します。SelectedValueChangedイベントでは、BindingSourceの位置を選択したレコードの位置へと変更することができます。
ナビゲーションバーにコンボボックスを追加するのは、いくぶん難しくなります。このコンボボックスはバインドできず、しかもこれはただのドロップダウンリストボックスであり、値メンバを持ちません。この不足を補うため、本稿ではハッシュテーブルを使って値メンバを格納します。
ナビゲーションバーにコンボボックスを追加するには、次の手順を実行します。
- ユーザーコントロールのデザインビューのバーの右端にあるアイコンは、追加可能な項目を表示するドロップダウンリストです。まず[Label]を選ぶと、「ToolStripLabel1」という名前のラベルがバーに追加されます。
- この名前を「Lookup」に変更します(また、この値のプロパティを作成しておき、開発者がデザイン時に変更できるようにしておく必要があります)。
- ドロップダウンリストを再びクリックし、[Combo Box]コントロールを追加して、「ToolStripComboBox1」という項目を新規に追加します。
- このユーザーコントロールにプロパティをもう1つ追加して、開発者がどのフィールドを表示メンバにするかを選択できるようにします。
- このプロパティの既定値を、バウンドテーブル内の最初のテキスト型フィールドにします。そのためには次のコードを
bs_DataSourceChangedメソッドに追加します。 - ハッシュテーブルを定義し、コンボボックスに適切な値を読み込むための
BuildLookupListメソッドを追加します。 - このメソッドは、リストが変更されたときに呼び出される必要があります。
BindingSourceメソッドのハンドラセクションにハンドラを1つ追加し、さらにもう1つ、「イベントを処理して上記のメソッドを呼び出すためのメソッド」を追加します。 - バインディングソースがリレーションに基づいている場合は、リストの内容は親の位置が変わるたびに変更されます。そのため、親バインディングソースを追跡するプロパティと、それを呼び出すメソッドを新たに追加する必要があります(この処理もListChangedハンドラで行いたくなるかもしれませんが、イベントの引数がそれぞれ違うという点に注意してください)。
- 親の位置が変わった場合に対処する、もう1つのイベントハンドラを追加します。
- 開発者は、親バインディングソースのプロパティを設定することができます、そして、次のソースを
bs_DataSourceChangedメソッドに追加することで、データテーブルにプロパティを割り当てるときにキャプチャできるようになります。 - この時点でフォームを実行するとリストは作成されますが、ユーザーが何かを選んでも何も起こりません。ユーザーがコンボボックスリストから項目を選ぶ際のイベントをキャプチャする必要があります。コンボボックスコントロールを選び(場合により、この作業は非常に厄介です。ラベルを選択して[Tab]キーを押してください)、次にプロパティパネルでイベントのセクションに移動(上部の稲妻アイコンをクリック)します。それから、[SelectedIndexChanged]イベントをクリックして、コードスタブを生成します。次のコードを追加してください。
- 最後の問題は、「レコードの選択後に[Next]または[Previous]ボタンを使って位置を移動すると、コンボボックス内のエントリがレコードの表示と同期しなくなる」ということです。次のメソッドを使えば、リストとレコードの同期をとることができます。
- このメソッドが
PositionChangedイベントハンドラから呼び出されるようにします。 - 最後に、コンパイルしてフォームを実行してみましょう。フォームの背後に何もコードを書かなくても、すべてが問題なく機能します。
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
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
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
AddHandler _BindingSource.ListChanged, AddressOf bs_ListChanged Private Sub bs_ListChanged(ByVal sender As Object, _ ByVal e As System.ComponentModel.ListChangedEventArgs) BuildLookupList() End Sub
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
Private Sub parentBS_PositionChanged(ByVal sender As Object, ByVal e As EventArgs) BuildLookupList() End Sub
'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
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
'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
このユーザーコントロールをさらに拡張して、フォーム用の他のCRUD型イベント(例えばユーザーがデータを編集するまでコントロールを無効にするなど)を実現することができます。新しいコントロールを実装するには、ウィザードによって配置されたBindingNavigatorを削除し、[Toolbox]パネルを開きます。すると、今回作成したユーザーコントロールがWinセクションに配置されています。このユーザーコントロールをフォームにドラッグし、BindingSourceプロパティに適切な値を指定します。
親子関係を持つフォームでも、さまざまなナビゲーション機能を使用することができました。これを実際に試してみるには、次のようにします。
- まず、パネルコントロールをフォームの右側にドラッグします。
- [Data Sources]パネルを開いて「Customer」テーブルを展開し、子リレーションシップテーブルの[Orders]を選びます。このテーブルを、パネルにドラッグします。ウィザードによって、もう1つのバインディングソースとグリッドが追加されます。
- 必要に応じて、パネルとグリッドのサイズを変更します。グリッドよりも上の位置にあるパネルでは、少し余白を残してください。
- もう1つのexBindingNavigatorユーザーコントロールをパネルにドラッグします。このユーザーコントロールは最上部にドッキングします。
- ナビゲータの
BindingSourceプロパティを、新しく「OrdersBindingSource」にセットします。
これでフォームが実行できるようになり、ナビゲーションも以前と変わらず動作します。
結論
BindingNavigatorコンポーネントは一度ユーザーコンポーネントにして、そこにプロパティとメソッドを追加するという方法で簡単に拡張できます。このユーザーコントロールをフォームに追加すると、データテーブルへのオートフィルや更新機能(ユーザーへの確認メッセージ付き)が実装されるほか、特定レコードのルックアップができるようになります。拡張したユーザーコントロールをコントロールライブラリプロジェクトに格納すると、任意のソリューションに追加できるようになり、データアクセスアプリケーションの開発速度がアップします。
データアクセスロジックにインターフェイスを実装すると、データセット内の各テーブルで必要となるメソッドのスタブをすばやく作成できますが、それでも一部のコードは書く必要があります。ただ、これは非常に構造化されたコードなので、CodeDomやサードパーティ製のコード生成システムで簡単に生成できます。
この方法論には、いくつかの問題がついて回ります(その大部分は、Visual Studio 2005自身の問題です)。ユーザーコントロールをフォームに追加した後、そのユーザーコントロールに何らかの変更を加えても、Visual Studio内でフォームが自動的にリロードされることはありません。フォームを一度閉じてから開き直し、デザインモードで表示する必要があります。ときには、フォームを正確に表示するためにプロジェクトをリビルドしなければならないこともあります。そのため、ユーザーコントロールをフォームで使用するのは、ユーザーコントロールを完全に作成し終えてからにしてください。
この記事のコードを、ダウンロードサンプルとしてご用意してあります。このダウンロードファイルには、Northwindデータベースを使ったVBとC#の両方のサンプルが収められています。
