Windows Forms C#定石 - DataGridView - EditMode, DropDown

## はじめに

C# ソフト開発時に、決まり事として実施していた内容を記載します。

**DataGridView については下記記事もあります**
- [Windows Forms C#定石 - DataGridView - ReadOnly, Disable相当](https://qiita.com/chai0917/items/525634fb35cf4efc0b40)
- [Windows Forms C#定石 - DataGridView - 値依存イメージ表示, ちらつき防止](https://qiita.com/chai0917/items/608f511b6609a8713526)
- [Windows Forms C#定石 - DataGridView - DataTable](https://qiita.com/chai0917/items/dbf8e6480d6383480f8e)

### テスト環境

ここに記載した情報/ソースコードは、Visual Studio Community 2022 を利用した下記プロジェクトで生成したモジュールを Windows 11 24H2 で動作確認しています。
- Windows Forms - .NET Framework 4.8
- Windows Forms - .NET 8

::: note
記載したソースコードは .NET 8 ベースとしています。
.NET Framework 4.8 の場合は、コメントで記載している null 許容参照型の明示 `?` を削除してください。
:::

::: note
Visual Studio 2022 - .NET Framework 4.8 は、C# 7.3 が既定です。
このため、サンプルコードは、C# 7.3 機能範囲で記述しています。
:::

## EditMode

https://learn.microsoft.com/ja-jp/dotnet/api/system.windows.forms.datagridview.editmode

DataGridView 利用時、DataGridViewEditMode.EditOnEnter がお勧めです。

### 既定状態

DataGridView.EditMode の既定値 DataGridViewEditMode.EditOnKeystrokeOrF2 の場合、セルに対するマウスクリックの挙動は以下の通りです。

- DataGridViewTextBoxCell
  - １回目のクリックで選択、２回目クリックで編集モードに遷移
- DataGridViewCheckBoxCell
  - チェックエリアをクリックで即時選択/非選択となる
    - 上記は見た目の変化だけで、実際に DataGridViewCheckBoxCell の値が更新されるのは、セル移動時となります。詳細は末尾「おまけ - DataGridViewCheckBoxCell」参照
- DataGridViewComboBoxCell
  - １回目のクリックで編集モードに遷移。編集モードになった後、DropDown 表示ボタンが有効となる
  - 対象セルが選択されていない状態で、DropDown 表示ボタンをクリックしても、DropDown 表示されない（編集モードに遷移するだけ）

![DataGrodVoew-01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3963267/52d3a84b-e1c1-4eb1-9765-7b8f810b385c.png)

![DataGrodVoew-02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3963267/cb6bb106-6f1a-407e-9fb7-d54eac39e91e.png)


編集モードというワンクッションで、操作に手間がかかるので、後述 EditOnEnter がお勧めです。

::: note
DataGridViewTextBox 以外のセルを併用する場合、仕様の整合性についても引っかかる部分がありました。

- DataGridViewCheckBoxCell、DataGridViewButtonCell が即時操作であれば、DataGridViewComboBoxCell が選択されてない状態でも、DropDown 表示ボタンクリックで、DropDown 表示ができても良い気がする
- DataGridViewCheckBoxCell が即時操作なのに、DataGridViewTextBoxCell、DataGridViewComboBoxCell は編集モード遷移が必要というのは不整合な気がする
:::

### EditOnEnter

DataGridViewEditMode.EditOnEnter は、セルが選択されると同時に編集モードに入る動作となります。

```CSharp
// デザイナで DataGridView dataGridView1 を配置
dataGridView1.EditMode = DataGridViewEditMode.EditOnEnter;
```

タブ移動で問題なく編集モードとなり、マウスクリックで下記動作となります。

- DataGridViewTextBoxCell
  - １回目のクリックで編集モードに遷移
- DataGridViewCheckBoxCell
  - チェック部分をクリックで即時選択/非選択可能
- DataGridViewComboBoxCell
  - 対象セルが選択されていない状態でも、DropDown 表示ボタンをクリックで、DropDown 表示される
  - 対象セルが選択された状態で、セルをクリックするとDropDown 表示される

::: note
DataGridViewComboBoxCell で  対象セルが選択されていない状態でも、DropDown 表示ボタンクリックが有効になることが、非常に意味があります。
EditMode 既定値のままで、CellEnter イベントで BeginEdit(true) という手法だと、上記挙動にはなりません。
```CSharp
// .NET Framework 時 object? の ? 不要
private void DataGridView_CellEnter(object? sender, DataGridViewCellEventArgs e)
{
  if (e.ColumnIndex < 0 || e.RowIndex < 0)
  {
    return;
  }
  if (sender is DataGridView dgv)
  {
    dgv.BeginEdit(true);   // 編集モードに移行
  }
}
```
:::


## DropDown

DataGridViewComboBoxCell 選択時、自動的に DropDown 表示を行うこともできますが、基本的に、そのような設定はしませんでした。（理由は後述）

### 自動表示

DataGridViewComboBoxCell 選択時に、DropDown 自動表示は、下記で対処可能です。

```CSharp
// デザイナで DataGridView dataGridView1 を配置
dataGridView1.CellEnter += DataGridView_CellEnter;
```
```CSharp
// .NET Framework 時 object? の ? 不要
private void DataGridView_CellEnter(object? sender, DataGridViewCellEventArgs e)
{
  if (e.ColumnIndex < 0 || e.RowIndex < 0)
  {
    return;
  }
  if (sender is DataGridView dgv)
  {
    dgv.BeginEdit(true);           // 編集モードに移行
    if (dgv.EditingControl is ComboBox comboBox)
    {
      comboBox.DroppedDown = true; // DropDownを表示
    }
  }
}
```

キー操作でセル移動する場合には、特に気になることはありません。

しかし、マウス操作の場合、DropDown 表示が表示された状態で、任意のセルを選択しても、DropDown 表示のキャンセルとして扱われるので、１回のアクションで任意のセルに移動できなくなってしまいます。

DropDown 自動表示をしない場合、DropDown 表示をするためには、セルクリック、もしくは、キー入力（`F4` もしくは `ALT` + `↓`）する手間があります。

どちらがベターかといえば、、、

キー操作で移動を行うの方より、マウス操作を行う方のほうが習熟度は低いと思うので、マウス操作に着目して判断することとします。
マウスで DataGridViewComboBoxCell を選択ということは、編集を意図しているので、DropDown 自動表示で良いのかなぁ。
いや、マウスの場合、DataGridViewComboBoxCell の DropDown 表示ボタンを直接クリックすれば、自動表示としなくても DropDown 表示できるので不便と感じることはないかな。

そうすると、DropDown 自動表示とした場合、１回のアクションで任意のセルに移動できない不便が勝ると判断したので、自動表示を行うことはありませんでした。

::: note
編集コントール ComboBox の DrowDownClosed イベントを使って、マウスクリックされた位置に該当するコントロールを確認して、フォーカスを強制的に移動するという対応ができそうな気もしますが、そこまでの対応は冗長と判断しました。
:::


## おまけ
### DataGridViewCheckBoxCell

https://learn.microsoft.com/ja-jp/dotnet/api/system.windows.forms.datagridviewcheckboxcell

> 通常、チェック ボックスのセル値は、他のデータと同様にストレージ用、または一括操作を実行するためのものです。 ユーザーがチェック ボックスのセルをクリックしたときにすぐに応答する必要がある場合は、DataGridView.CellClick イベントを処理できますが、このイベントはセルの値が更新される前に発生します。 クリック時の新しい値が必要な場合は、現在の値に基づいて期待される値を計算する方法があります。 もう 1 つの方法は、変更をすぐにコミットし、それに応答する DataGridView.CellValueChanged イベントを処理することです。 セルがクリックされたときに変更をコミットするには、DataGridView.CurrentCellDirtyStateChanged イベントを処理する必要があります。 ハンドラーで、現在のセルがチェック ボックスセルの場合は、DataGridView.CommitEdit メソッドを呼び出し、Commit 値を渡します。


前述 EditMode で、DataGridViewCheckBoxCell が選択されていない状態でも、クリックでチェック表示が選択/非選択となると記載しましたが、この時点では、セルの値は確定していないので、CellValueChanged イベントは発生しません。

EditMode で提示した形態の DataGridView で、CellValueChanged イベントハンドラを用意してみます。

```CSharp
// デザイナで DataGridView dataGridView1 を配置
// 列定義
dataGridView1.Columns.AddRange(new DataGridViewColumn[]
{
  new DataGridViewTextBoxColumn { Name = "ID", Width = 20, ReadOnly = true },
  new DataGridViewTextBoxColumn { Name = "Name", Width = 200 },
  new DataGridViewComboBoxColumn { Name = "Type", Width = 200,
        DisplayStyle = DataGridViewComboBoxDisplayStyle.ComboBox },
  new DataGridViewCheckBoxColumn { Name = "Select 1", Width = 80 },
  new DataGridViewCheckBoxColumn { Name = "Select 2", Width = 80 },
  new DataGridViewButtonColumn { Name = "Click", Width = 80 }
});
if (dataGridView1.Columns["Type"] is DataGridViewComboBoxColumn col)
{
  col.Items.AddRange(new string[] { "Type1", "Type2", "Type3", "Type4" });
}
// テストデータ
for (int no = 0; no < 10; no++)
{
  DataGridViewRow row = new DataGridViewRow();
  row.CreateCells(dataGridView1);
  row.Cells[0].Value = no; 
  row.Cells[1].Value = $"情報{no}";
  // TODO - 以降のセルにも値を設定

  dataGridView1.Rows.Add(row);
}
// イベントハンドラ
dataGridView1.CellValueChanged += DataGridView_CellValueChanged;
```
```CSharp
// .NET Framework 時 object? の ? 不要
private void DataGridView_CellValueChanged(object? sender, DataGridViewCellEventArgs e)
{
  if (e.ColumnIndex < 0 || e.RowIndex < 0)
  {
    return;
  }
  if (sender is DataGridView dgv)
  {
    var cell = dgv.Rows[e.RowIndex].Cells[e.ColumnIndex];
    if (cell != null)
    {
      // TODO - 値変化時の処理
      MessageBox.Show($"col={e.ColumnIndex} row={e.RowIndex} value={cell.Value}");
    }
  }
}
```

任意の DataGridViewCheckBoxCell をクリックして、チェック表示を選択/非選択と変更しても、CellValueChanged の MessageBox.Show がされることはありません。
チェック表示を初期値から変更した状態でセルを移動すると、MesageBox.Show されます。

::: note
DataGridViewTextBoxCell、DataGridViewComboBoxCell でも同様に、セル移動で入力/選択が確定して、CellValueChanged が呼ばれます。

- DataGridViewTextBoxCell で文字列入力中は CellValueChanged が呼ばれません
- DataGridViewComboBoxCell で DrowDown 選択を変更しただけでは CellValueChanged が呼ばれません

DataGridViewCheckBoxCell も、上記挙動と同様ということです。
:::

DataGridViewCheckBoxCell でチェック表示を選択/非選択と変更直後に CellValueChanged を発生させたい場合は、CurrentCellDirtyStateChanged イベントで CommitEdit が必要となります。

```CSharp
// デザイナで DataGridView dataGridView1 を配置
dataGridView1.CurrentCellDirtyStateChanged += DataGridView_CurrentCellDirtyStateChanged;
```
```CSharp
// .NET Framework 時 object? の ? 不要
private void DataGridView_CurrentCellDirtyStateChanged(object? sender, EventArgs e)
{
  if (sender is DataGridView dgv)
  {
    if (dgv.CurrentCell is DataGridViewCheckBoxCell)
    {
      dgv.CommitEdit(DataGridViewDataErrorContexts.Commit);
    }
  }
}
```

dataGridView1.AllowUserToAddRows = true として、新しい行の DataGridViewCheckBoxCell のチェック表示部分以外をクリックでは行追加されませんが、チェック表示部分をクリックすると行追加されます。

::: note
dataGridView1.AllowUserToAddRows = true の場合、CurrentCellDirtyStateChanged イベントハンドラに記載する処理は、行追加されて同一行の他セルに値が入力がされてないケースの考慮が必要となります。
:::

チェック表示部分をクリックした場合、CurrentCellDirtyStateChanged 以前で行追加がされるので、下記のようなソースとしても意味がありません。

```CSharp
// .NET Framework 時 object? の ? 不要
private void DataGridView_CurrentCellDirtyStateChanged(object? sender, EventArgs e)
{
  if (sender is DataGridView dgv)
  {
    if (dgv.CurrentCell is DataGridViewCheckBoxCell)
    {
      // 行追加用の「新しい行」以外 - 既に行追加が動作してるので意味がない
      if (dgv.CurrentRow != null && !dgv.CurrentRow.IsNewRow)
      {
        dgv.CommitEdit(DataGridViewDataErrorContexts.Commit);
      }
    }
  }
}
```

::: note
新しい行に対する、以下の操作でも行追加されます。
- DataGridViewTextBoxCell に文字入力
- DataGridViewComboBoxCell で DropDown 選択

DataGridViewCheckBoxCell チェック表示変更での行追加は、上記操作と同等の根本的な仕様なので、許容すべき動作だと思います。
:::