データ連結 ListView コントロールの作成

Bobby Schmidt
Microsoft Corporation

Arrays in the .NET Framework

Rockford Lhotka
Magenic Technologies

August 8, 2002
日本語版最終更新日 2002 年 9 月 30 日

MSDN Code Center から vbDataListView.exe サンプル ファイルのダウンロード。

概要 : Rocky Lhotka 氏は、この資料で継承を使用した独自のデータ連結 ListView コントロールの作成方法、 データ連結コントロールの作成方法、 およびデータ ソースに関する重要な情報を検索するためのリフレクションの使用方法を説明しています。

Microsoft Visual Basic® 開発者は、 何年も前から独自のコントロールを作成できました。 Visual Basic .NET も例外ではなく、 Visual Basic 5.0 や Visual Basic 6.0 と同様の方法でコントロールを作成できます。

Visual Basic .NET ではそれに加えて、継承を使用したコントロールの作成が可能です。 つまり、既存のコントロールを利用して、 それを基に新しいコントロールを作成できます。 新しいコントロールは最初は元のコントロールと同じですが、 機能の変更や追加を適切に行うことができます。 多くの場合、継承を使用する方が、 以前のようにコントロールを組み合わせるよりも簡単で強力です。

個人的に気に入っているのは、 詳細モードの ListView コントロールです。 このコントロールの外観が特に気に入っています。 これは Microsoft Windows® エクスプローラが使用しているのと同じコントロールなので、 ユーザーにも馴染みがあります。 読み取り専用の表形式のデータを表示するのであれば、 ListView は DataGrid などよりも優れたコントロールだと思います。

とは言え、ListView コントロールはデータ連結をサポートしておらず、 もどかしさを感じます。 ListView にデータを手作業で読み込むコードはそれほど複雑なものではありませんが、 コントロールをデータ ソースに単純に連結しておしまいとはいかないので、 いつも腹が立ちます。

この資料では、 継承を使用してカスタム コントロールを作成する処理をウォークスルーします。 今回取り上げるコントロールはデータ連結 ListView コントロールで、 名前を DataView と呼ぶことにしましょう。 資料の中で、データ連結コントロールの作成方法と、 データ ソースに関する重要な情報を検索するための リフレクション (Reflection) (英語) の使用方法を見ていきましょう。

複合データ連結の理解

Visual Basic .NET では容易にコントロールを作成でき、 "単純データ連結" を使用してコントロールをデータ ソースに連結できます。 単純連結は、コントロールの作成者が余分な作業を行わなくても、 データ ソースのプロパティをコントロールのプロパティに連結できます。

フォーム内部では、1 行のコードで単純連結を行うことができます。 たとえば、オブジェクトの Value プロパティを TextBox コントロールの Text プロパティに連結するには、 以下のように記述します。


TextBox1.DataBindings.Add("Text", theObject, "Value")

"複合連結" はまったく別物です。 複合連結は、全体的なデータ セットをコントロールに連結します。 全体的なデータ セットの例には、DataTable、DataView、Array、コレクション オブジェクトなどがあります。 このデータをすべて表示する方法はコントロールによって異なります。 DataGrid コントロールの場合は、 エンド プログラマは表示する列を選択できる機能を使用して、 データを表形式で表示します。

複合連結を使用することは困難なことではありません。 単に DataSource プロパティを設定するだけでも構いません。 もちろん、他のプロパティを設定して表示するデータを制御することもよくありますが、 重要なのは複合連結を使用するのは複雑ではないということです。

たとえば、DataGrid にテーブルをデータ連結するには、 以下のように記述します。


DataGrid1.DataSource = dsDataSet.Tables("TheTable")

では "複合" という用語はどのあたりで重要になってくるのでしょうか。 それはコントロールを "作成" するときです。 複合連結をサポートするコントロールを作成するには、 相当量の作業が必要になります。

複合連結をサポートするコントロールを作成するには、 DataSource プロパティと DataMember プロパティを実装する必要があります。 実装には手間がかかりますが、非常に難しいというわけではありません。 難解になるのは、データ ソースと対話処理を行い、表示するデータを取得するときです。 考えられるデータ ソースは多数ありますが、 すべてのソースを同じ方法で処理できるわけではありません。

複合データ連結を提供している Windows フォーム コントロールは、 IList インターフェイスを実装するすべてのデータ セットやデータ リストをサポートします。 このインターフェイスは、 データのリストを公開するオブジェクトの基本インターフェイスです。 つまり、データやオブジェクトのリストの大部分はデータ ソースとして使用できます。 また、コントロール内で IList インターフェイスを使用して、 リストと対話処理を行うことができます。

一部のオブジェクトは IList を実装していませんが、 IListSource を実装しています。 これは、 IListSource を実装しているオブジェクトとは直接対話処理はできないが、 代わりに使用できる IList オブジェクトがあるかどうかを問い合わせることができることを意味します。 この最も顕著な例が DataTable オブジェクトです。 IListSource を実装するオブジェクトから IList を取得するには、 IListSource インターフェイスから GetList メソッドを呼び出すだけです。

DataSet オブジェクトもサポートする必要があります。 しかし、実は大部分のコントロールは DataSet オブジェクト自体には連結しません (DataGrid は例外です)。 代わりに、DataSet に含まれている DataTable オブジェクトに連結します。 DataTable の名前は、 通常、 コントロールの DataMember プロパティを使用して提供されます。 つまり、データ ソースが DataSet のときは、 作業を余計に行って DataTable 自体に連結する必要があります。

ここまでの話は、比較的単純です。 複雑になるのは、 IList インターフェイスがデータやオブジェクトのリストにアクセスできるようにするだけであるという点です。 オブジェクトの型や、オブジェクトとの対話方法は指示しません。 にもかかわらず、取得するオブジェクトは以下のようにさまざまです。

DataRow オブジェクト DataTable オブジェクトから取得します。
DataRowView オブジェクト DataView オブジェクトから取得します。
プリミティブ ValueType オブジェクト (Integer、Single、およびその他ネイティブ データ型を含む) 配列とコレクションから取得します。
String オブジェクト 配列とコレクションから取得します。
構造体 配列とコレクションから取得します。
任意のオブジェクト 配列とコレクションから取得します。

上記のそれぞれのオブジェクト型から値を取得できる標準的なメカニズムは存在しません。 したがって、それぞれのオブジェクト型を個別に処理する必要があります。 つまり、IList インターフェイスを使用してデータ要素を検索し、 IList が保持しているオブジェクトの型を判断します。 その後は適切な技法を使用して、 表示するデータをオブジェクトの型に応じて取得できます。

メタデータの検索と使用

多くの複合連結コントロールには、 単にデータを連結するだけでなく、 データ ソースからフィールドの数や名前を自動的に取得する機能があります。 つまり、コントロールにデータ ソースを提供すれば、 特に指定しなくてもすべてのフィールド名が検索され、 すべてのデータが表示されます。

エンド プログラマが表示するフィールドを直接制御したい場合は、 この自動検出処理を無効にして、 表示する列名を 1 つずつ手作業でコントロールに指示できます。

データ連結自体と同様に、 データ ソースの IList 内の元になるデータ オブジェクトの型に応じて異なる技法を使用して、 このメタデータを取得する必要があります。 たとえば、作業に使用するデータ ソースが DataTable のときは DataTable オブジェクトのメタデータを使用できますが、 データ ソースがビジネス オブジェクトのコレクションのときはリフレクション (Reflection) (英語) を使用する必要があります。

データの編集

データの編集や操作を行う複合連結コントロールを構築するときに、 別の全体的な問題が発生する場合があります。 DataGrid コントロールにはデータの編集や操作の機能があり、 ユーザーはデータの埋め込み先編集を行うことができます。 埋め込み先編集は、リスト オブジェクトが IBindingList をサポートしているかどうか、 およびリスト内の各データ オブジェクトが IEditableObject をサポートしているかどうかの判断が必要になるので、 複雑になります。

このようなインターフェイスは DataGrid には "必須" ではありません。 DataGrid はこのようなインターフェイスを持たないオブジェクトとも対話処理を行います。 ただし、このようなインターフェイスを実装しているオブジェクトと比べると対話処理の機能が少なくなります。

IBindingList は、いつ元になるデータが変更されたかを示すイベントを保持しているので、 このインターフェイスが存在する場合はこのインターフェイスをサポートする必要があります。 このイベントが発生したときに、 データをコントロールに再連結して、 表示を更新する必要があります。 データの埋め込み先編集はサポートしないので、 このコントロールの IEditableObject については気にする必要はありません。

コントロールの実装

コントロールの最適な作成方法は、 Windows コントロール ライブラリ プロジェクトにコントロールを配置することです。 Visual Studio .NET でプロジェクトを作成し、 名前を DataListView と付けます。

Dd314007.vbnet08262002_fig1(ja-jp,MSDN.10).gif
図 1. コントロール プロジェクトの作成

既定ではこの種類のプロジェクトは、 1 つのユーザー コントロールを保持して開始されます。 これは Visual Basic 6.0 の集合コントロールの概念と同じです。 このコントロールは UserControl1 です。

ここでは、他のコントロールを集合したコントロールは作成しません。 代わりに、機能を強化した単一のコントロールを作成します。 これを行うには、継承を使用するのが最も簡単な方法です。

UserControl1 を右クリックして削除します。 [プロジェクト] メニューの [クラスの追加] をクリックして、 プロジェクトに新しいクラスを追加します。 名前を DataListView と付けます。

新しいコントロールを ListView から単純に継承して、 既存の ListView と同じにできます。


Public Class DataListView
  Inherits System.Windows.Forms.ListView
    
End Class

新しいコントロールを既存のコントロールから継承することで、 その既存のメソッド、プロパティ、および動作のすべてを取得します。 これを元に、既存の ListView に新機能を追加します。

まず、コントロールをいくつかの既定値で初期化します。 通常の ListView の既定値の一部は、データを表示するには適切ではありません。 コンストラクタ メソッドを作成して、独自の既定値を設定できます。


  Public Sub New()
    View = View.Details
    FullRowSelect = True
    MultiSelect = False
  End Sub

新しいコントロールは既定で詳細表示を行うように設定します。 詳細表示は行をクリックすると行全体が選択されますが、一度に 1 行しか選択できません。 もちろん、ここでの既定値をエンド プログラマが変更することはできますが、 データ連結コントロールには ListView コントロール固有の既定値よりも上記の値が適しています。

後でデータ連結用のコードを配置する Sub メソッドを作成しておきましょう。


  Private Sub DataBind()
    ' TODO: データ連結コードを記述します
  End Sub

この部分で、コントロールに適切なデータを読み込むための困難な作業を行います。

DataSource と DataMember

すべての複合連結コントロールは DataSource プロパティと DataMember プロパティを公開しています。 ここで重要なのは、エンド プログラマがコントロールにデータへの参照を提供できる DataSource プロパティです。

どちらのプロパティも単純なプロパティではありません。 DataGrid や ListBox がどのように機能するかを見ると、 DataSource プロパティは [プロパティ] ウィンドウに利用可能なデータ ソースの一覧を表示することがわかります。 同様に、DataMember プロパティは DataSet 内のテーブルの選択に役立つグラフィック ツールを持っています。 さらに、これらのプロパティは [プロパティ] ウィンドウの Data カテゴリに表示されます。

以上の特別な動作はすべて、 それぞれのプロパティに格納されている属性によるものです。 DataSource プロパティでは、 <Category()> 属性を使用して Data カテゴリにプロパティを表示する必要があります。 <TypeConverter()> 属性は、 データ ソースを一覧するドロップダウン メニューの処理方法を Visual Studio® .NET に指示します。 最後に <RefreshProperties()> 属性を使用して、 新しいデータが選択されたときに Visual Studio .NET が他のすべてのプロパティを更新できるようにします。 新しいデータ ソースに基づいて DataMember プロパティを更新したいので、 この属性が重要になります。

以上の属性をすべて指定すると、 クラスに基本的な DataSource プロパティを追加できます。 まず、このプロパティに適用した属性が含まれている System.ComponentModel 名前空間をインポートします。


Imports System.ComponentModel

続いて値を保持する変数を宣言します。


Public Class DataListView
  Inherits System.Windows.Forms.ListView

  Private mDataSource as Object

最後にプロパティそのものを記述します。


  <Category("Data"), _
   RefreshProperties(RefreshProperties.Repaint), _
   TypeConverter("System.Windows.Forms.Design.DataSourceConverter," & _
   "System.Design")> _
  Public Property DataSource() As Object
    Get
      Return mDataSource
    End Get
    Set(ByVal Value As Object)
      mDataSource = Value
      DataBind()
    End Set
  End Property

<TypeConverter()> 属性が System.Windows.Forms.Design.DataSourceConverterSystem.Design を参照している点に注目してください。 DataSourceConverter は [プロパティ] ウィンドウにデータ ソースの一覧を提供します。 このクラスは System.Design アセンブリにあるので、 そのアセンブリも参照しています。

データ ソースとして新しい値が提供されたら、 DataBind メソッドを呼び出すことで、 表示されているデータがコントロールにより更新されます。

同様の方法で DataMember プロパティを作成できます。 このプロパティも <Category()> 属性を使用します。 [プロパティ] ウィンドウを正しく動作させるには、 <Editor()> 属性を使用します。 まず、値を保持する変数を宣言します。


  Private mDataMember As String

続いてプロパティを追加します。


  <Category("Data"), _
   Editor("System.Windows.Forms.Design.DataMemberListEditor," & _
   "System.Design", GetType(System.Drawing.Design.UITypeEditor))> _
  Public Property DataMember() As String
    Get
      Return mDataMember
    End Get
    Set(ByVal Value As String)
      mDataMember = Value
      DataBind()
    End Set
  End Property

このプロパティの <Editor()> 属性は、 DataSource での <TypeConverter()> と同じ基本的な役割を果たします。 いずれのプロパティも、 Visual Studio .NET が特定のオブジェクトを呼び出して、 機能拡張された [プロパティ] ウィンドウへの表示機能を管理するよう指定しています。

この時点で、 このコントロールは DataSource プロパティと DataMember プロパティを公開するので、 エンド プログラマがこのコントロールを使用すると、 [プロパティ] ウィンドウでプロパティが適切に機能します。 もちろん、まだ値の処理は必要であり、 コントロールの複雑な部分をビルドする必要があります。

変更したデータの処理

先ほど IBindingList インターフェイスと、 このインターフェイスが元になるデータが変更されたときに起動するイベントをどのように保持しているかについて触れました。 データ ソースがこのインターフェイスを実装する場合は、 この機能をサポートする必要があります。

Visual Basic .NET にはイベントを処理するいくつかの方法がありますが、 WithEvents キーワードを使用するのが最も簡単です。 以下のように IBindingList インターフェイス用の変数を宣言します。


  Private WithEvents mBindingList As IbindingList

その後、ListChanged イベントのイベント ハンドラを記述できます。


  Private Sub mBindingList_ListChanged(ByVal sender As Object, _
      ByVal e As System.ComponentModel.ListChangedEventArgs) _
      Handles mBindingList.ListChanged

    DataBind()

  End Sub

元になるデータが変更された場合、 コントロールをデータ ソースに再連結する必要があります。

最後に、DataSource または DataMember が変更されるたびに、 新しいデータ ソースが IBindingList を実装しているかどうかを調べる必要があります。 実装している場合、 mBindingList 変数にはそのデータ ソースへの参照が必要になります。 この参照を行うには "元になる" データ ソースを取得するために多少の作業が必要なので、 やや手間がかかります。

たとえば指定したデータ ソースが DataSet の場合、 元になるデータ ソースは DataTable になります。 しかし、実際には DataTable に連結するのではなく、 常に DataTable が提供する IList 参照に連結します。 IList 参照とは、テーブルの既定のデータ ビューを表す DataView オブジェクトです。

以上の目的を果たすため、 元になるデータ ソースまたは内部データ ソースを検索するメソッドを記述しましょう。


  Private Function InnerDataSource() As IList
    If TypeOf mDataSource Is DataSet Then
      If Len(mDataMember) > 0 Then
        Return CType(CType(mDataSource, DataSet).Tables(mDataMember), _
          IListSource).GetList

      Else
        Return CType(CType(mDataSource, DataSet).Tables(0), _
          IListSource).GetList
      End If

    ElseIf TypeOf mDataSource Is IListSource Then
      Return CType(mDataSource, IListSource).GetList

    Else
      Return CType(mDataSource, IList)
    End If
  End Function

データ ソースが DataSet の場合、 DataMember プロパティを使用して適切な DataTable オブジェクトを取得します。 DataTable が実装する IListSource は、 連結可能な IList オブジェクトを返すGetList メソッドを保持しています。

データ ソースが DataTable の場合、 または IListSource を実装するその他のオブジェクトの場合、 IListSource インターフェイスを使用して IList オブジェクトを返します。

それ以外の場合は、 データ ソース オブジェクトが直接 IList を実装していると想定します。 実装していない場合にはエラーが発生します。 Windows フォームの複合データ連結は常にデータ ソースが IList を実装している必要があるので、 エラーを発生するのが適しています。

ここで、内部データ ソースが IBindingList を実装しているかどうかを判断するメソッドを実装します。


  Private Sub SetSource()
    Dim InnerSource As IList = InnerDataSource()

    If TypeOf InnerSource Is IBindingList Then
      mBindingList = CType(InnerSource, IBindingList)

    Else
      mBindingList = Nothing
    End If
  End Sub

IBindingList インターフェイスを実装している場合、 mBindingList 変数にデータ ソースへの参照を渡します。 この参照が適切であれば、 元になるデータが変更されたことを示すデータ ソースからのイベントを取得できます。

IBindingList インターフェイスを実装していない場合、 mBindingList に Nothing を設定し、 以前連結していたデータ ソースからデータが変更されたことを示すイベントを取得しないようにします。

最後に、 DataSource プロパティと DataMember プロパティを更新して SetSource メソッドを呼び出す必要があります。 DataSource の Set ブロックに、以下のように記述します。


    Set(ByVal Value As Object)
      mDataSource = Value
      SetSource()
      DataBind()
    End Set

DataMember の Set ブロックに、以下のように記述します。
    Set(ByVal Value As String)
      mDataMember = Value
      SetSource()
      DataBind()
    End Set

これで、データの変更が通知されるようになります。

列とフィールドの関連付け

ListView コントロールは Columns コレクションを持っています。 Columns コレクションを使用して各行に表示するデータを制御できます。 ListView コントロールはもともとデータ連結をサポートしていません。 したがって各列のフィールド名を指定できず、 表示上の列とデータ ソースのフィールドをリンクできないので、 この機能を追加する必要があります。

Columns コレクションは、 ColumnHeader オブジェクトのコレクションです。 継承を使用して、ColumnHeader と等価な新しい型のオブジェクトを作成します。 このオブジェクトに Field プロパティを追加します。 これを行うには、 プロジェクトに新しいクラスを追加し、 名前を DataColumnHeader と付けます。 以下のコードを追加します。


Public Class DataColumnHeader
  Inherits ColumnHeader

  Private mField As String

  Public Property Field() As String
    Get
      Return mField
    End Get
    Set(ByVal Value As String)
      mField = Value
    End Set
  End Property

End Class

また、新しいコレクション オブジェクトも必要になります。 既存のコレクション オブジェクトは ColumnHeader オブジェクトを使用して機能しますが、 必要なのは DataColumnHeader オブジェクトを使用して機能するオブジェクトです。 さいわい、Visual Basic .NET では簡単にカスタム コレクション オブジェクトを作成できます (詳細については「Doing Collections with Inheritance」 (英語) を参照してください)。

プロジェクトに DataColumnHeaderCollection という新しいクラスを追加します。 コレクションのすべてのコードはここに記載しませんが、 この資料の先頭のリンクから入手できるソース コードの一部として含まれています。 このクラスでオーバーロードされる一連の Add メソッドを定義しているので、 列をコントロールに柔軟に追加できます。 しかし、おそらく最も重要な機能は Invalidate イベントです。 このイベントはコレクション内で要素の追加、削除、変更があるたびに発生します。 コントロールはこのイベントを使用して列のリストがいつ変更されたかを認識するので、 自動的にデータを最新状態にして表示を更新できます。

これで列ヘッダーとコレクション用の独自のオブジェクトが準備できたので、 コントロール自体の Columns プロパティをオーバーライドできます。

今回の例は、Shadows キーワードの使用が適しているケースの 1 つです。 ListView の元の作成者は Columns プロパティをオーバーライドして独自のプロパティが使用されることを想定しなかったので、 このプロパティを Overridable としてマークしませんでした。 つまり、単純にはこのプロパティをオーバーライドできません。 しかし、Shadows キーワードを使用すると、 オーバーライドを想定してデザインされなかったメソッドもオーバーライドできます。

まず、コレクションを保持する変数を宣言しましょう。


  Private WithEvents mColumns As DataColumnHeaderCollection

続いて、コンストラクタを使用してコレクションのインスタンスを作成します。


  Public Sub New()
    mColumns = New DataColumnHeaderCollection()
    View = View.Details
    FullRowSelect = True
    MultiSelect = False
  End Sub

最後に、Shadows にするプロパティを追加します。


  Public Shadows ReadOnly Property Columns() As DataColumnHeaderCollection
    Get
      Return mColumns
    End Get
  End Property

エンド プログラマはコントロールと対話処理を行うときは常に、 カスタムの DataColumnHeader オブジェクトとカスタムの Columns プロパティを併用することになります。 つまり、このコントロールは列のタイトル、幅などに関するすべての初期情報を指定するだけでなく、 列を連結するデータ フィールドも指定します。

また、 このコントロールはコレクションの Invalidate イベントも処理する必要があるので、 いつ列が変更されたかを判断できます。


  Private Sub mItems_Invalidate() Handles mColumns.Invalidate
    DataBind()
  End Sub

DataSource プロパティや DataMember プロパティと同様、 列が変更されたときに DataBind メソッドを呼び出してコントロールのデータ連結を更新するようにします。

メタデータの自動探索

エンド プログラマが表示する列と、その列に連結するデータ フィールドを手作業で指定できるのも優れた方法ですが、 より自動的に処理できる別の方法があります。 理想的には、コントロールがデータ ソースを調べ、 メタデータを使用して利用可能なすべてのデータ フィールドを判断できるべきです。 列ヘッダーはフィールドごとに作成できるので、 コントロールは利用可能なすべてのフィールドを連結できます。

ここからコントロールの作成が興味深いものになってきます。 先ほど説明したとおり、 さまざまな型のオブジェクトがデータ ソースとして機能できるので、 さまざまな技法を使用して各データ ソースからのメタデータを取得する必要があります。

まず、自動探索を行うかどうかを制御するプロパティを追加します。 コントロールに次の変数を追加します。


  Private mAutoDiscover As Boolean = True

実際の探索作業を行うメソッドをサブルーチンにします。


  Private Sub DoAutoDiscover()
    ' TODO: ここで探索を行います
  End Sub

プロパティを作成できたので、 エンド プログラマは自動探索を行うかどうかを選択できるようになります。


  <Category("Data")> _
  Public Property AutoDiscover() As Boolean
    Get
      Return mAutoDiscover
    End Get
    Set(ByVal Value As Boolean)
      If mAutoDiscover = False AndAlso Value = True Then
        mAutoDiscover = Value
        DoAutoDiscover()
      Else
        mAutoDiscover = Value
      End If
    End Set
  End Property

[プロパティ] ウィンドウの Data 部分にこのプロパティを配置する <Category()> 属性の使用法に注目してください。 プロパティが False から True に変更されるタイミングを検出しているので、 DoAutoDiscover メソッドを呼び出すことができるようになった点にも注目してください。

同様に、 DataSourceDataMember が変更されたときに自動探索が必要かどうかを確認します。 それぞれのプロパティの Set ブロックに、次のコードを追加します。


      SetSource()
      If mAutoDiscover Then
        DoAutoDiscover()
      End If
      DataBind()
    End Set

残っているのは重要な部分だけです。 次の 2 種類の一般的な型のデータ ソースをサポートする必要があります。

  • DataView
  • IList インターフェイス

DataView をサポートするので、 DataTable オブジェクトがサポートされます。 したがって、常に DataTable オブジェクトの元になる DataView に連結します。 また、DataSet をサポートする必要がありますが、 これは DataTable をサポートすることにより処理されます。 先ほど実装した InnerDataSource メソッドは常に DataSet や DataTable を元になる DataView に変換します。

データ ソースの型ごとに、 メタデータを読み取るメソッドを作成します。 まず、DataView のメタデータを読み取ります。


  Private Sub DoAutoDiscover(ByVal ds As DataView)
    Dim Field As Integer
    Dim Col As DataColumnHeader

    For Field = 0 To ds.Table.Columns.Count - 1
      Col = New DataColumnHeader()
      Col.Text = ds.Table.Columns(Field).Caption
      Col.Field = ds.Table.Columns(Field).ColumnName
      mColumns.Add(Col)
    Next
  End Sub

これはきわめて標準的な ADO.NET コードです。 単に DataView の Columns コレクションをループ検索し、 Caption を列のタイトルとして、 ColumnName を列のフィールドとして取得しています。 このデータを使用して、 DataView の各列の DataColumnHeader オブジェクトを作成し、 これらのオブジェクトを Columns コレクションに追加します。

汎用の IList の場合の処理は、それほど単純ではありません。 IList はさまざまな型のデータやオブジェクトのリストを保持できます。 データおよびオブジェクトを以下の分類でグループ化できます。

  • ValueType (プリミティブ)
  • 文字列
  • オブジェクトまたは構造体

IList にはすべて同じ型のデータを含める必要があるという技術的な規定は存在しません。 しかし、Windows フォームの複合データ連結は一般的に、 IList 内のすべての項目は最初の要素と同じ型であると想定して記述されています。 ここではその手法に従い、 IList の最初の要素を使用してリスト全体のメタデータを判断します。

データがオブジェクトまたは構造体の場合はリフレクションを使用するので、 次の名前空間をインポートします。


Imports System.Reflection

これで汎用の IList の自動探索用コードを構築できます。


  Private Sub DoAutoDiscover(ByVal ds As IList)
    If ds.Count > 0 Then
      ' リストの先頭項目を取得します
      Dim obj As Object = ds.Item(0)

      If TypeOf obj Is ValueType AndAlso obj.GetType.IsPrimitive Then
        ' 値がプリミティブ値型の場合
        Dim col As DataColumnHeader
        col = New DataColumnHeader()
        col.Text = "Value"
        mColumns.Add(col)

      ElseIf TypeOf obj Is String Then
        ' 値が単純な文字列の場合
        Dim col As DataColumnHeader
        col = New DataColumnHeader()
        col.Text = "Text"
        mColumns.Add(col)

      Else
        ' 値がオブジェクトか構造体の場合
        Dim SourceType As Type = obj.GetType
        Dim column As Integer

        ' すべてのパブリック プロパティのリストを取得します
        Dim props As PropertyInfo() = SourceType.GetProperties()
        If UBound(props) >= 0 Then
          For column = 0 To UBound(props)
            mColumns.Add(props(column).Name)
          Next
        End If

        ' すべてのパブリック フィールドのリストを取得します
        Dim fields As FieldInfo() = SourceType.GetFields()
        If UBound(fields) >= 0 Then
          For column = 0 To UBound(fields)
            mColumns.Add(fields(column).Name)
          Next
        End If
      End If
    End If
  End Sub

単純な値型や文字列を処理する場合、 既定のタイトルを持つ行を 1 つ作成します。 興味深いのはデータ要素がオブジェクトか構造体のときで、 リフレクションを使用して要素のすべてのパブリック プロパティ メソッドのリストを取得し、 各要素の列を作成している点です。 同様に、要素のすべてのパブリック フィールド (変数) のリストを取得し、各フィールドの列を作成します。

最後に、DataSource プロパティ、DataMember プロパティ、 および AutoDiscover プロパティから呼び出される DoAutoDiscover メソッドを記述する必要があります。


  Private Sub DoAutoDiscover()
    Dim InnerSource As IList = InnerDataSource()
    mColumns.Clear()

    If InnerSource Is Nothing Then Exit Sub

    If TypeOf InnerSource Is DataView Then
      DoAutoDiscover(CType(InnerSource, DataView))

    Else
      DoAutoDiscover(InnerSource)
    End If
  End Sub

このメソッドは内部データ ソースを取得します。 内部ソースが Nothing の場合、自動探索は不可能です。 Nothing ではない場合、 データ ソースが DataView かどうかを確認し、 そのデータ ソースの処理に適したオーバーロード メソッドを呼び出します。

これでほぼ終了です。 この時点でエンド プログラマは [プロパティ] ウィンドウかコードを使用して、 データ ソースをコントロールに連結できます。 自動探索を使用して列を設定したり、 手作業で Columns コレクションに列を追加できます。

残りは、コントロール内のデータを実際に表示する作業だけです。

データの連結

概念上は、データ ソースへの連結は、単に IList のすべての要素をループ検索し、 各要素のデータをコントロールの行に読み込むだけの作業です。 実際には IList にさまざまな型のデータやオブジェクトが含まれているので、やや複雑になります。 自動探索コードで IList を機能させる方法は既に見たので、 データ連結コードに関する同じ基本的な問題を見てみましょう。

ソリューションの中核となるのは、 各フィールドの値を IList の要素から取得する関数です。 この GetField メソッドは、 要素が DataView オブジェクトの DataRowView なのか、 プリミティブ値型なのか、文字列なのか、あるいはオブジェクトまたは構造体なのかを判断します。

DataRowView の場合、 標準的な ADO.NET コードを使用して値を取得できます。 プリミティブ値型か文字列の場合、単に値そのものを返すことができます。 オブジェクトか構造体を処理している場合、 リフレクションを使用してパブリックのプロパティやフィールドの値を読み込んで値を返す必要があります。

以下に GetField メソッドを示します。


  Private Function GetField(ByVal obj As Object, _
    ByVal FieldName As String) As String
    If TypeOf obj Is DataRowView Then
      ' DataView の DataRowView の場合
      Return CType(obj, DataRowView).Item(FieldName).ToString

    ElseIf TypeOf obj Is ValueType AndAlso obj.GetType.IsPrimitive Then
      ' プリミティブ値型の場合
      Return obj.ToString

    ElseIf TypeOf obj Is String Then
      ' 単純な文字列の場合
      Return CStr(obj)

    Else
      ' オブジェクトまたは構造体の場合
      Try
        Dim sourcetype As Type = obj.GetType

        ' フィールドがプロパティかどうかを確認します
        Dim prop As PropertyInfo = sourcetype.GetProperty(FieldName)

        If prop Is Nothing OrElse Not prop.CanRead Then
          ' そのような名前の読み取り可能なプロパティはありません - フィールドを確認します
          Dim field As FieldInfo = sourcetype.GetField(FieldName)

          If field Is Nothing Then
            ' フィールドもありません
            ' デバッグ用の目印にフィールド名を返します
            Return "値 " & FieldName & " は存在しません"

          Else
            ' フィールドを取得し、値を返します
            Return field.GetValue(obj).ToString
          End If

        Else
          ' プロパティを検出し、値を返します
          Return prop.GetValue(obj, Nothing).ToString
        End If

      Catch ex As Exception
        Return ex.Message
      End Try
    End If
  End Function

データの各要素から特定のフィールドを取得できるようになったので、 DataBind メソッドを実装してデータ ソースをループ検索できます。


  Private Sub DataBind()
    Clear()

    ' データ ソースがない場合は処理しません
    If mDataSource Is Nothing Then Exit Sub
    ' 列がない場合は処理しません
    If mColumns.Count = 0 Then Exit Sub

    Dim InnerSource As IList = InnerDataSource()
    Dim Row As Integer
    Dim Field As Integer
    Dim Item As ListViewItem

    ' 列ヘッダーを読み込みます
    For Field = 0 To mColumns.Count - 1
      MyBase.Columns.Add(mColumns(Field))
    Next

    ' コントロールにデータを読み込みます
    For Row = 0 To InnerSource.Count - 1
      Item = New ListViewItem()

      ' 最初のフィールドを読み込みます
      Item.Text = GetField(InnerSource.Item(Row), _
        mColumns(0).Field).ToString

      ' すべてのサブフィールドを読み込みます
      For Field = 1 To mColumns.Count - 1
        Item.SubItems.Add(GetField(InnerSource.Item(Row), _
          mColumns(Field).Field).ToString)
      Next
      Items.Add(Item)
    Next
  End Sub

まず列をセットアップしてから、各列にヘッダー テキストを追加します。 DataListView は直接 ListView を元に作成されていることを思い出してください。 ListView は独自の Columns コレクションを持っています。 MyBase キーワードにより、 ListView の元になる Columns コレクションにアクセスできるので、 基本的には列オブジェクトのリストを表示用のコントロールにコピーできます。

次に、IList データ ソースの要素ごとに、 コントロールに ListViewItem を追加します。 最初の列が ListViewItem の最初のフィールドであると想定されるのに対し、 他のすべての列はサブ項目として処理されます。 各データ要素のフィールドが ListViewItem に読み込まれたら、 ListViewItem が表示用のコントロールに追加されます。

結論

複合連結コントロールを作成するには、 相当量の作業を行い、 リフレクションを使用する必要があります。 しかし、全体的なデータ セットに連結するコントロールを作成して、 そのデータを期待どおりに表示できるので、 最終的には強力なものができあがります。

この例では、 DataSet オブジェクト、DataTable オブジェクト、 および DataView オブジェクト以外に、 IList インターフェイスを実装する配列やコレクションへの連結方法を紹介しました。 この機能は、Visual Basic .NET に付属の DataGrid コントロールや ListBox コントロールが提供するサポートと互換性があります。