型付 DataSet の TableAdapter の基本クラスを変更する

概要

型付 DataSet から自動生成される TableAdapter クラスの基本クラスを変更し、複数の TableAdapter を作成した場合でもコードを共有して使えるようにします。

動作環境

.NET Framework
  • .NET Framework 2.0
  • .NET Framework 3.0
  • .NET Framework 3.5
  • .NET Framework 4
  • .NET Framework 4.5

動作確認環境

.NET Framework
  • .NET Framework 3.5
  • .NET Framework 4.5

内容

型付 DataSet の作成と生成されるコード

型付 DataSet を使用すると、SQL を元に各列に型情報を持たせた DataTable が作成できたり、型付 DataTable をもとに DB とのデータのやり取りを簡素化する TableAdapter が使用できるようになります。

ここでは型付 DataSet を作成することが目的ではないので、とりあえずデータベースとテーブルは用意してある状態で DataSet をささっと作ってしまいましょう。テーブルのレイアウトも好きなようにしてかまいません。

まずはプロジェクトに新しい項目として DataSet を追加します。名前はなんでもいいですがここではそのままにしてあります。

DataSet を開きデザイナから TableAdapter を追加します。

ウィザードにしたがって進めると DataTable と TableAdapter が作成されます。

作成した DataTable, TableAdapter を使ってデータベースからレコードを取得し使用する例としては次のような形になるかと思います。(あくまでも一例です)

public Form1()
{
  InitializeComponent();

  using (var adapter = new 金額集計TestTableAdapter())
  {
    var table = adapter.GetData();

    foreach (var row in table)
    {
      Trace.WriteLine(string.Format("{0}:{1}", row.年月, row.金額));
    }
  }
}

通常この DataSet を使うプロジェクトではテーブルが1つということは少なく、テーブルの数、または複数のテーブルを組み合わせた DataSet を多く作ることになります。そうなった場合に思いつくのが処理の共通化だと思います。例えば内部で使用されている SQL を動的に変えてレコードの取得条件を変えるなどです。

しかし、実は DataSet から作成された TableAdapter クラスは自動的にコードが生成されたものになっています。また、この TableAdapter クラスは Component クラスを継承したものになっているため、実質共通的に使えるメソッドやプロパティはありません。メソッドに基底クラスを渡して TableAdapter に関連しそうな処理を呼ぶことができないのです。

さらに内部で使用されている SQL や、データベースにアクセスするために使用されている SqlDataAdapter などは protected で宣言されているため、外から触ることもできません。

ただ、protected で宣言されているメソッドやプロパティについてはその宣言どおり、TableAdapter クラスを継承したクラスを作成すれば触ることもできますし、また、この TableAdapter クラスは partial 修飾子がついているため、別なメソッドやプロパティを宣言して処理を拡張することができます。次のコードのように新たなプロパティを作成して内部プロパティを取得することもできます。

using System.Data.SqlClient;
namespace DataSetBaseAdapter {
    public partial class DataSet1 {
    }
}

namespace DataSetBaseAdapter.DataSet1TableAdapters
{
  public partial class 金額集計TestTableAdapter
  {
    public SqlDataAdapter InnerAdapter
    {
      get { return Adapter; }
    }
  }
}

実際にこの TableAdapter を使うと、拡張したプロパティにアクセスできることがわかります。

しかし、内部プロパティにアクセスできたからといって処理が共通化できるわけではなく、やはり作成した DataSet ごとにコードを書いていかなければなりません。

TableAdapter の基本クラスを変更する

前述のとおり TableAdapter クラスは Component クラスを継承しているため、処理を共通化しにくくなっています。仮にクラスが継承されているコードの部分を変更しても、そのコードは自動生成されているため、再度 DataSet を更新するとコードが戻ってしまいます。できるとすると別なファイルに interface を継承した partial クラスを書くという方法もありますが、結局作成した TableAdapter の数だけ書かないといけないのであまり意味はありません。

実はこの TableAdapter の継承元クラスを変更する仕組みは存在します。その手順について説明したいと思います。

まずは継承したいクラスを作成します。TableAdapter はもともと Component クラスを継承しているため、作成したクラスでも Component クラスは継承しておきましょう。ここに共通化したいコードを書きますが、ここではいったんクラスだけ作成しておきます。名前空間は自分が作成したものに合わせてください。

using System.ComponentModel;

namespace DataSetBaseAdapter
{
  public class BaseAdapter : Component
  {
  }
}

次に DataSet デザイナを開いて作成した TableAdapter を選択します。

右クリックしてプロパティを開きます。BaseClass というプロパティがあり、初期状態では「System.ComponentModel.Component」になっているはずです。ここを先ほど作成したクラスに書き換えてください。その際名前空間も含めてすべて書きます。

保存すると TableAdapter の基本クラスが変わっていることがわかります。これを各 TableAdapter に設定することによって共通的な処理を基本クラスに書くことができるようになります。

例えば内部の SQL や SqlDataAdapter に対してアクセスしたい場合は以下のように記述します。自動的に作成される Adapter や CommandCollection はあくまでも継承先クラスが作成するプロパティなので継承元から直接触ることはできません。以下のコードではリフレクションを使うことによって強制的にアクセスできるようにしています。

using System.ComponentModel;
using System.Data.SqlClient;
using System.Reflection;

namespace DataSetBaseAdapter
{
  public class BaseAdapter : Component
  {
    public SqlDataAdapter InnerAdapter
    {
      get
      {
        return (SqlDataAdapter)GetType().GetProperty("Adapter",
          BindingFlags.NonPublic | BindingFlags.Instance).GetValue(this, null);
      }
    }

    public SqlCommand[] InnerCommandCollection
    {
      get
      {
        return (SqlCommand[])GetType().GetProperty("CommandCollection",
          BindingFlags.NonPublic | BindingFlags.Instance).GetValue(this, null);
      }
    }
  }
}

実際に TableAdapter を使うと継承元のクラスのプロパティにアクセスできるようになっていることがわかります。