Windows FormsのDataGridViewがイマイチすぎるので少しはマシに使える方法を考える

## 読み飛ばすのが理想

ぼっちアドカレ4日目の限界派遣SESです。
書くこと思いつかないなーとか思いつつQiitanのぬいぐるみをもらうために今日もない知識を引き出していこうと思います。

今日までほとんどWindowsFormsの悪口しか言っていない気がしますが本日もWindowsFormsについて書いていきます。

## DataGridViewにデータを設定するのがだるすぎる

`DataGridView`とはFormアプリ上で表形式のデータを表示するために利用できるコントロールです。
よく`DataTable`などを`DataSource`に指定することで名称を使ってアクセスできるようになることはよく知られていると思います。

とりあえずフォームにてきとうに`DataGirdView`を作成して以下のようにてきとうなデータを生成して与えてやります。
```cs
public partial class Form2 : Form
{
    public Form2()
    {
        InitializeComponent();
    }

    private void Form2_Load(object sender, EventArgs e)
    {
        var dataTable = new DataTable();

        // カラムに名称をつける
        dataTable.Columns.Add("Column1");
        dataTable.Columns.Add("Column2");
        dataTable.Columns.Add("Column3");

        // てきとうなデータをつっこむ
        foreach (var i in Enumerable.Range(0, 10))
        {
            dataTable.Rows.Add(i, i*2, i*3);
        }

        dataGridView1.DataSource = dataTable;
    }
}
```

すると以下のようにカラム名が指定された状態でデータが表示されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1239781/f44685fc-fd6b-ecfb-4bcc-58785740f90c.png)

### データを取り出す操作を取り出すのがめんどくさい

先ほどの方法で`DataTable`から選択行のデータを取り出すという処理を考えてみましょう。

以下の画面のように行を選択するとテキストボックスにデータが入るといった処理を実現してみます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1239781/90fa5617-2081-5f83-a7cf-e8be4388f8c0.png)

コードは以下のように選択行を取り出して`DataRow`オブジェクトを頑張って獲得することで取得できます。


```cs
private void dataGridView1_SelectionChanged(object sender, EventArgs e)
{
    var dataGridView = (DataGridView)sender;

    // 選択行が0個だったら処理終了
    if (dataGridView.SelectedRows.Count == 0)
        return;

    // データRowを取り出す
    var row = (dataGridView.SelectedRows[0].DataBoundItem as DataRowView)?.Row;
    if (row == null)
        return;

    textBox1.Text = $"{row["Column1"]}, {row["Column2"]}, {row["Column3"]}";
}
```

ね？ひどいでしょう？
`row`を取り出すための処理がまるで呪文ですし、結局row型はDataTable型なのでカラム名を指定しなければ取り出すことができません。

また、`DataTable`では各値は`object`型で渡されるので、キャストするなりの操作が必要だったりと、かなり面倒なことが多いです。

エンティティフレームワークで生成された`DataTable`を継承した型であれば、その型にキャストすることでプロパティを使ってアクセスできるようになりますが、GUIで作成された自動生成コードにうんざりしているので使いたくありません。

### MODELを定義してデータとして割り当てる

そもそもデータを与える時はMODELを配列形式のデータで渡せるほうが嬉しいですよね。

てきとうな以下のようなModelクラスを定義してあげます。
```cs
public class DataModel
{
    public required string Column1 { get; set; }
    public required int Column2 { get; set; }
    public required double Column3 { get; set; }
}
```

そしてモデルを使って以下のように先ほどのフォームのクラスを書き直します。
ここでは`DataSource`として渡すデータが`List`になっています。

```cs
public partial class Form2 : Form
{
    public Form2()
    {
        InitializeComponent();
    }

    private void Form2_Load(object sender, EventArgs e)
    {
        // てきとーなデータを10個つくる
        var dataList = Enumerable.Range(0, 10)
            .Select(x => new DataModel {
                Column1 = $"{x}歳",
                Column2 = x,
                Column3 = x * 1.5,
            })
            .ToList();

        dataGridView1.DataSource = dataList;
    }

    private void dataGridView1_SelectionChanged(object sender, EventArgs e)
    {
        var dataGridView = (DataGridView)sender;

        // 選択行が0個だったら処理終了
        if (dataGridView.SelectedRows.Count == 0)
            return;

        // データをModelとして取り出す
        var row = dataGridView.SelectedRows[0].DataBoundItem as DataModel;
        if (row == null)
            return;

        textBox1.Text = $"{row.Column1}, {row.Column2}, {row.Column3}";
    }
}
```

以下のように同様の操作が可能になります。

でも、カラム名は直接プロパティ名が使われてしまうため、それもイマイチですよね。
できれば論理名と物理名はわけておきたいです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1239781/e813db3f-5f46-5b52-6d23-f19403e5e255.png)

### カラムに論理名をつける

これが結構面倒です。

`DataTable`を利用した例でもそうですが、DataGridViewではカラム名を指定しない限り、プロパティ名がカラム名として利用されます。

また、先ほどの手法ではプロパティから自動でカラムが生成されるため、非表示パラメーターといったものが設定できません。

以下のようにカラムを生成することで、自動割り当てではないカラムを生成することができます。
```cs
private void InitDataGirdViewColumns()
{
    // カラムが自動生成されないようにする
    dataGridView1.AutoGenerateColumns = false;

    // カラムを生成する
    dataGridView1.Columns.AddRange([
        new DataGridViewTextBoxColumn
        {
            Name = "ねんれい",
            DataPropertyName = "Column1",
        },
        new DataGridViewTextBoxColumn
        {
            Name = "しんちょう(m)",
            DataPropertyName = "Column2",
        },
        new DataGridViewTextBoxColumn
        {
            Name = "まめを食べる数",
            DataPropertyName = "Column3",
        }
    ]);
}
```

でも結局これだとカラム名が必要になるし、Modelのプロパティ名を変更した際など`DataPropertyName`なども変更しなくてはならないので全然DRYじゃなくていやな気分になります。

### Modelの中でDataGridViewの情報を完結させたい

Modelの中だけでDataGridViewで表示される情報をすべて表現できればいい感じですよね。

なので以下のようなAttributeを定義します。

```cs
// プロパティのみに付与できる用にするAttribute
[AttributeUsage(AttributeTargets.Property)]
public class DataGirdViewPropAttribute: Attribute
{
    public string LogicName { get; } // 論理名
    public bool Visible { get; } // 可視性
    
    public DataGirdViewPropAttribute(string logicName, bool visible = true)
    {
        LogicName = logicName;
        Visible = visible;
    }
}
```

これをモデルのプロパティへとセットします。
論理目と物理名がセットになっていていい感じに見えます。

```cs
public class DataModel
{
    [DataGirdViewProp("ねんれい", visible:false)] // 年齢は非表示項目とした
    public required string Column1 { get; set; }
    [DataGirdViewProp("しんちょう(m)")]
    public required int Column2 { get; set; }
    [DataGirdViewProp("まめを食べる数")]
    public required double Column3 { get; set; }
}
```

`DataGridView`には拡張メソッドを定義して、型情報からプロパティ情報とカスタム属性を取得し、それらをカラム情報をして追加します。

```cs
public static void SetColumns(this DataGridView dataGridView, Type modelType)
{
    // プロパティとカスタム属性を取得
    var propAndAttrs = modelType.GetProperties()
        .Select(prop => (prop, attr: prop.GetCustomAttribute<DataGirdViewPropAttribute>()))
        .Where(x => x.attr != null);

    foreach(var (prop, attr) in propAndAttrs)
    {
        dataGridView.Columns.Add(
            new DataGridViewTextBoxColumn
            {
                DataPropertyName = prop.Name, // プロパティ名を設定
                Name = attr!.LogicName, // 論理名を設定
                Visible = attr!.Visible, // 可視性を設定
            }    
        );
    }
}
```

最後に`SetColumns`メソッドを呼び出すことで完成です。

```cs
private void InitDataGirdViewColumns()
{
    // カラムが自動生成されないようにする
    dataGridView1.AutoGenerateColumns = false;

    // カラムを生成する
    dataGridView1.SetColumns(typeof(DataModel));
}
```

これで論理名のカラムを表示しながら、年齢という重要な個人情報を隠すことができました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1239781/fddafc16-ad6e-ff4a-cecb-24d5ca04ec41.png)


## まとめ

やっぱりレガシーですよねー。アドカレが始まってからずっと文句しか書いていない気がします。
それでも少しでも扱いやすくするために色々考察をするのは楽しいです。
また、今回の方法では行のソートができないという問題がありますが、ソート可能な`BindingList`を作ることでソート可能にしている方も見受けられます。

https://nenono.hatenablog.com/entry/2014/10/05/193441

また、「こんなんライブラリ使えば余裕やで余裕」など教えていただけると幸いです。（絶対に車輪の再開発してる気がするなーと思いながら作ってました）