![]() ![]() ![]() ![]() Visual Studio 2005のフォームにおけるBindingNavigatorの拡張この記事のURLhttp://japan.internet.com/developer/20060627/26.html
著者:David Catherman
海外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コンポーネントが作成されます。データセットがプロジェクトにとってローカルである場合は、さらにテーブルアダプタのインスタンス化が行われ、 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 この新しいコントロールは大幅な拡張であり、通常はこれで、データバウンドフォームに関するナビゲーションとCRUDのニーズの大部分を満たすことができます。しかし、データバウンドフォームのニーズすべてに対処する完全なツールを作るには、いくつか足りない点があります。克服しなければならない問題としては、次のようなものがあります。
汎用的なテーブル更新インターフェイス 1番目の問題に対処するには、データテーブルのデータ読み込み(
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」テーブルに関する 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呼び出しが使えるようになります。つまり、 BindingNavigatorコントロールの拡張ナビゲーションツールバーの問題を解決して機能を拡張する方法の1つは、BindingNavigatorコントロールをベースとしたユーザーコントロールを作成し、必要なプロパティとメソッドを追加することです。
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 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 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には
DataSourceとDataMenberという2つのプロパティがあり、それによってどのテーブルにバインドされるかが決まります。大抵の場合、DataSourceにはデータセットのインスタンスを指定し、DataMemberにはデータセット内のテーブルの名前を指定します。しかし、親子関係が確立されている状況では、DataSourceにもう1つのBindingSourceを指定し、DataMemberにリレーションの名前を指定することができます。この場合、テーブルを導き出すには、少し計算が必要です。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プロパティからテーブル参照を取得することができます。テーブルへの参照を取得した後は、ポリモーフィズムを利用して、この参照を先ほど作成したインターフェイスにキャストすることができます。正しくキャストされない場合は、そのデータテーブルがインターフェイスを正しく実装していないということであり、例外をスローする必要があります。キャストされる場合は、汎用的な更新ルーチンを呼び出してデータを保存することができます。
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 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 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 このユーザーコントロールをフォームに追加し、 自動保存(AutoSave)機能の実装 2つ目の問題は、1つ目よりも少し厄介です。BindingSourceコンポーネントには「データが修正されたかどうかを示すフラグ」がないため、これが大きな問題となっています。グリッドの場合は、
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 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
AddHandler _BindingSource.BindingComplete, AddressOf BindingComplete
AddHandler _BindingSource.PositionChanged, AddressOf bs_PositionChanged
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つ目の問題は、「変更されたデータを保存するか、変更前の状態に戻すか」という確認メッセージをユーザーに表示しなければならない場合が考えられるということです。もちろん、この確認メッセージの表示はオプション機能にすべきであるため、まずは
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 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 ![]() 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を値として、また、一意のテキストを表示番号として使用します。 ナビゲーションバーにコンボボックスを追加するのは、いくぶん難しくなります。このコンボボックスはバインドできず、しかもこれはただのドロップダウンリストボックスであり、値メンバを持ちません。この不足を補うため、本稿ではハッシュテーブルを使って値メンバを格納します。 ナビゲーションバーにコンボボックスを追加するには、次の手順を実行します。
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セクションに配置されています。このユーザーコントロールをフォームにドラッグし、 親子関係を持つフォームでも、さまざまなナビゲーション機能を使用することができました。これを実際に試してみるには、次のようにします。
これでフォームが実行できるようになり、ナビゲーションも以前と変わらず動作します。 結論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。
|