はじめに
WindowsフォームでDataGridView
等を用いたデータベースアプリを構築する場合、
データの入れ物として型付きDataSet
を使うのが定石かと思います。
しかしながら、型付きDataSet
はデザイナの出来の悪さなどから、かなり嫌われているようです。
今回、型付きDataSet
の代わりに通常のオブジェクト(POCO)をデータの入れ物として使ってみます。
サードパーティのライブラリとして、BindingListViewとDapperを使います。
完成品はこちらに置いてあります
https://github.com/mono1729/BindingWithPoco
作るもの
- SQL ServerのNorthwindサンプルデータベースのProductsテーブルの内容を表示するアプリを作ります。
- 一覧の表示には
DataGridView
を使い、ヘッダクリックでソート可能にします。 -
BindingSource
を用いて、データの詳細形式表示も行います(図の下段)。一覧の選択行に同期して表示が変わります。 - ProductNameでインクリメンタルサーチを行えるテキストボックスも配置します。
- データ更新が画面に即反映されるか、テストするためのボタンも配置します。("Add row", "Modify first row")
制作の流れ
今回のアプリは以下の流れで制作します。
- Productsテーブルの1レコードのデータを表す
Product
クラスを作る -
Product
クラスにはINotifyPropertyChanged
を実装する - Visual Studioのデータソース機能を用いて、画面デザインを行う
-
DataTable
/DataView
の代わりとなるコレクション、BindingList<Product>
/BindingListView<Product>
を定義する - Dapperで
BindingList<Product>
にデータを読み込み、それを元にBindingListView<Product>
のインスタンスも生成する -
BindingListView<Product>
をBindingSource
のDataSource
にする -
BindingSource
をDataGridView
のDataSource
にする - インクリメンタルサーチとテスト用ボタンの機能を実装する
POCOを作る (Product)
Productsテーブルのデータの入れ物になるクラスを用意します。
データが更新された際、画面に通知しなければいけないので、INotifyPropertyChanged
の実装が必要です。
フィールドが多いと大変なので、T4テキストテンプレートやFodyを使うといいかもしれません。
public class Product : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
private int _ProductID;
public int ProductID
{
get { return _ProductID; }
set
{
_ProductID = value;
OnPropertyChanged("ProductID");
}
}
private string _ProductName;
public string ProductName
{
get { return _ProductName; }
set
{
_ProductName = value;
OnPropertyChanged("ProductName");
}
}
/*以下略*/
データソース機能を使って画面デザイン
一度ビルドを行い、先ほど作成したクラスをVisual Studioのデータソースに登録すると、フォームデザイナにドラッグするだけで一覧と詳細を作成できます。
一緒にBindingSource
とBindingNavigator
コンポーネントが配置されますが、BindingNavigator
は今回使わないので消しましょう。
バインド用のコレクションを準備
フォームのコードビハインドで、バインドするためのコレクションを準備します。
ここではデータ保持用のBindingList<Product>
とバインド用のBindingListView<Product>
、2つのメンバを準備します。
using Equin.ApplicationFramework;
namespace BindingWithPoco
{
public partial class Form1 : Form
{
private BindingList<Product> _products;
private BindingListView<Product> _productsView;
データ読込・バインド
Micro-ORMのDapper
を使ってProductsテーブルのデータを読み込み、BindingList<Product>
に結果を入れます。
これを元データとして、BindingListView<Product>
のオブジェクトを作ります(DataView
と同じ役割です)。
そしてBindingListView<Product>
をBindingSource.DataSource
にセットします。
DataGridView.DataSource
にはBindingSource
をセットして、間接的にBindingListView<Product>
を参照させます。
using Dapper;
public partial class Form1 : Form
{
protected override void OnLoad(EventArgs e)
{
base.OnLoad(e);
LoadData();
}
private void LoadData()
{
var connStr = Properties.Settings.Default.db;
var conn = new SqlConnection(connStr);
var sql = "SELECT * FROM Products;";
_products = new BindingList<Product>(conn.Query<Product>(sql).ToList());
_productsView = new BindingListView<Product>(_products);
bindingSource1.DataSource = _productsView;
dataGridView1.DataSource = bindingSource1;
}
フィルタ機能の実装
テキストボックスを配置して、TextChangedイベントにフィルタ機能を実装します。
BindingListView
はDataTable
と同じフィルタ式が使えません。
代わりに匿名メソッドでフィルタ条件を設定します。
public partial class Form1 : Form
{
private void textBox1_TextChanged(object sender, EventArgs e)
{
var inputLower = ((TextBox)sender).Text.ToLower();
_productsView.ApplyFilter(p => p.ProductName.ToLower().Contains(inputLower));
}
動作検証用ボタンの実装
INotifyPropertyChanged
とBindingList
を用いているので、画面表示後にデータをいじっても変更が画面に反映されるはずです。
"Add row"ボタンと、"Modify first row"ボタンを配置して、検証用コードを実装します。
public partial class Form1 : Form
{
private void button1_Click(object sender, EventArgs e)
{
_products.Add(new Product
{
ProductID = 999,
ProductName = "Added!"
});
}
private void button2_Click(object sender, EventArgs e)
{
_products[0].ProductName = "MODIFIED!";
}
動作確認
ソート・フィルタも動きますし、データの変更も画面に即反映できているようです。
まとめ
今回はINotifyPropertyChanged
, BindingList
, BindingListView
を用いることで、型付きDataSet
と同じような双方向データバインドを実現したWindowsフォームアプリを構築することができました。
色々工夫が必要でしたが、DataSet
/DataTable
以下の機能は実現できていません。
- 入力値のバリデーション(
DataRow.RowError
) - 行の状態・変更の取消(
DataRow.RowState
,DataTable.AcceptChanges
/DataTable.RejectChanges
) - テーブル間のリレーションを使った機能(
DataSet.Relations
)
これらの機能を実装するのは大変そうなので、素直に型付きDataSet
を使った方がやはり幸せかもしれません…。
Entity Frameworkを使えば実現できそうな気はしますが。