Windowsフォームで型付きDataSetの代わりにPOCOを使ってみる

# はじめに
Windowsフォームで`DataGridView`等を用いたデータベースアプリを構築する場合、
データの入れ物として型付き`DataSet`を使うのが定石かと思います。
しかしながら、型付き`DataSet`はデザイナの出来の悪さなどから、かなり嫌われているようです。
今回、型付き`DataSet`の代わりに通常のオブジェクト(POCO)をデータの入れ物として使ってみます。
サードパーティのライブラリとして、[BindingListView](http://blw.sourceforge.net/)と[Dapper](https://github.com/StackExchange/Dapper)を使います。

完成品はこちらに置いてあります
https://github.com/mono1729/BindingWithPoco

# 作るもの
- SQL ServerのNorthwindサンプルデータベースのProductsテーブルの内容を表示するアプリを作ります。
- 一覧の表示には`DataGridView`を使い、ヘッダクリックでソート可能にします。
- `BindingSource`を用いて、データの詳細形式表示も行います(図の下段)。一覧の選択行に同期して表示が変わります。
- ProductNameでインクリメンタルサーチを行えるテキストボックスも配置します。
- データ更新が画面に即反映されるか、テストするためのボタンも配置します。("Add row", "Modify first row")
![image.png](https://qiita-image-store.s3.amazonaws.com/0/212876/95c2eff7-3871-1a27-5f93-bec7a90276b2.png)

# 制作の流れ
今回のアプリは以下の流れで制作します。

1. Productsテーブルの１レコードのデータを表す`Product`クラスを作る
2. `Product`クラスには`INotifyPropertyChanged`を実装する
3. Visual Studioのデータソース機能を用いて、画面デザインを行う
4. `DataTable`/`DataView`の代わりとなるコレクション、`BindingList<Product>`/`BindingListView<Product>`を定義する
5. Dapperで`BindingList<Product>`にデータを読み込み、それを元に`BindingListView<Product>`のインスタンスも生成する
6. `BindingListView<Product>`を`BindingSource`の`DataSource`にする
7. `BindingSource`を`DataGridView`の`DataSource`にする
8. インクリメンタルサーチとテスト用ボタンの機能を実装する

# POCOを作る (Product)
Productsテーブルのデータの入れ物になるクラスを用意します。
データが更新された際、画面に通知しなければいけないので、`INotifyPropertyChanged`の実装が必要です。
フィールドが多いと大変なので、T4テキストテンプレートやFodyを使うといいかもしれません。

```cs:Product.cs
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`は今回使わないので消しましょう。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/212876/3996a364-e7a0-c643-194a-0a9c2269c0a5.png)

# バインド用のコレクションを準備
フォームのコードビハインドで、バインドするためのコレクションを準備します。
ここではデータ保持用の`BindingList<Product>`とバインド用の`BindingListView<Product>`、２つのメンバを準備します。

```cs:Form1.cs
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>`を参照させます。

```cs:Form1.cs
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`と同じフィルタ式が使えません。
代わりに匿名メソッドでフィルタ条件を設定します。

```cs:Form1.cs
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"ボタンを配置して、検証用コードを実装します。

```cs:Form1.cs
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!";
    }
```

# 動作確認
ソート・フィルタも動きますし、データの変更も画面に即反映できているようです。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/212876/9290268d-cacb-6330-0622-dd2cd57b1151.png)

# まとめ
今回は`INotifyPropertyChanged`, `BindingList`, `BindingListView`を用いることで、型付き`DataSet`と同じような双方向データバインドを実現したWindowsフォームアプリを構築することができました。

色々工夫が必要でしたが、`DataSet`/`DataTable`以下の機能は実現できていません。

- 入力値のバリデーション(`DataRow.RowError`)
- 行の状態・変更の取消(`DataRow.RowState`,`DataTable.AcceptChanges`/`DataTable.RejectChanges`)
- テーブル間のリレーションを使った機能(`DataSet.Relations`)

これらの機能を実装するのは大変そうなので、素直に型付き`DataSet`を使った方がやはり幸せかもしれません…。
Entity Frameworkを使えば実現できそうな気はしますが。