WinForms DataGridView の編集モード完全ガイド — ReadOnly / EditMode / RowValidating の使い分け

みなさんこんにちは!ヒロポンです!!

今回は WinForms 業務SE 現場でガチで踏みやすいやつ!!の話。

「DataGridView で特定列だけ ReadOnly にしたい」「新規行追加を NG にしたい」「数値しか入力させたくない」「Rows[i].ReadOnly = true を書いたのに効かない」みたいなDataGridView 編集制御の事故って、業務SE で日常的に詰まりますよね??

俺も2社目くらいの流通系SIer 時代に、DataAdapter で取った DataTable を DataGridView に流して 「ID 列だけ ReadOnly に」 を実装したのに、画面ロード後も ID 列を編集できる状態になっていた事件をやらかしました。夕方の運用報告で気づいて半日デバッガで追ってハマったやつ。原因は完全に DataBound 前に Rows[i].ReadOnly を設定していた だけで、DataBindingComplete イベント側に移すだけで解決した。

DataGridView の編集制御は 3層 + タイミング制御 + 入力検証 の構造で考えると整理しやすい:

  • 3層: コントロール全体 / 列・行 / セル単位
  • タイミング制御: EditMode(4種類)+ CellBeginEdit / CellEndEdit
  • 入力検証: RowValidating / CellValidating / EditingControlShowing

この記事では VS2019 / .NET Framework 4.7.2 / C# 7.3 環境で、3層の ReadOnly 階層と編集タイミング制御、入力検証の業務SE 定石をコード5本でまとめます。後ろの「現場メモ」で、業務系チームでルール化した時の話も書いてる。

3行で結論:

  • 編集制御は3層: コントロール(dataGridView1.ReadOnly)/ 列(Columns[i].ReadOnly)/ セル(Cells[j].ReadOnly
  • タイミング制御は EditMode(4種類)+ CellBeginEdit / CellEndEdit / RowValidating イベント
  • DataBound 後の Rows[i].ReadOnlyDataBindingComplete イベントで設定(DataSource バインド前は効かない)
DataGridView 編集状態遷移: EditMode 4種類 + イベント順序
目次

定石1: ReadOnly の3層階層 — コントロール / 列・行 / セル

DataGridView の編集を止める書き方は階層的:

// ✅ 定石1: ReadOnly の3層階層
// 階層1: コントロール全体(最強・最もシンプル)
dataGridView1.ReadOnly = true;
dataGridView1.AllowUserToAddRows = false;      // 新規追加行も無効化
dataGridView1.AllowUserToDeleteRows = false;   // 行削除も無効化

// 階層2: 列単位(特定列だけ ReadOnly)
dataGridView1.Columns["id"].ReadOnly = true;
dataGridView1.Columns["created_at"].ReadOnly = true;

// 階層2: 行単位(特定行だけ ReadOnly)
dataGridView1.Rows[3].ReadOnly = true;

// 階層3: セル単位(行×列の交点だけ ReadOnly)
dataGridView1.Rows[3].Cells["price"].ReadOnly = true;

ポイント:

  1. dataGridView1.ReadOnly = true が最強(全部読み取り専用)
  2. 列単位は Columns["列名"].ReadOnly(列名指定が列順変更に強い)
  3. 行単位は Rows[i].ReadOnly(DataBound 後に設定する)
  4. セル単位は Rows[i].Cells[j].ReadOnly(最も粒度細かい)
  5. 読み取り専用化の3点セット: ReadOnly + AllowUserToAddRows = false + AllowUserToDeleteRows = false

業務系の判断軸: 「画面全体読み取り専用」なら ReadOnly = true + 3点セット、特定列・行ロックは列/行単位、と階層的に判断する。ん?AllowUserToAddRows まで触らないとダメなん??って思うかもだけど、新規追加行は常時編集可能扱いになるので3点セットで揃えるのが鉄板。混在は避けるのが保守性の鉄則っす。

定石2: EditMode 4種類の使い分け — 編集開始タイミング制御

EditMode プロパティで、ユーザーがセルを編集モードに入るタイミングを制御できる:

// ✅ 定石2: EditMode 4種類の挙動
// 1. EditOnEnter: セルにフォーカスが入った瞬間に編集モード(既定値: EditOnKeystrokeOrF2)
dataGridView1.EditMode = DataGridViewEditMode.EditOnEnter;

// 2. EditOnKeystroke: フォーカス入って何かキーを押した瞬間
dataGridView1.EditMode = DataGridViewEditMode.EditOnKeystroke;

// 3. EditOnF2: F2 キーで編集モード(既定挙動の一部)
dataGridView1.EditMode = DataGridViewEditMode.EditOnF2;

// 4. EditProgrammatically: BeginEdit() を呼ぶまで編集モードに入らない
dataGridView1.EditMode = DataGridViewEditMode.EditProgrammatically;

// プログラム側から編集モードに入る(EditProgrammatically と組み合わせ)
private void btnEdit_Click(object sender, EventArgs e)
{
    dataGridView1.CurrentCell = dataGridView1.Rows[0].Cells["name"];
    dataGridView1.BeginEdit(true);   // true = 現在の値を選択状態にする
}

使い分け表:

EditMode 編集開始タイミング 主な用途
EditOnEnter フォーカス入った瞬間 行入力フォーム的に使いたい
EditOnKeystrokeOrF2(既定) キー押下 or F2 一般的な業務系画面
EditOnKeystroke キー押下のみ F2 を避けたい場面
EditOnF2 F2 のみ クリック誤操作を防ぎたい
EditProgrammatically コード側で BeginEdit 編集モードを厳密に制御したい

業務系で多用されるのは既定の EditOnKeystrokeOrF2 または明示的編集を強制する EditProgrammatically「編集ボタンを押してから編集モード」の業務系画面では EditProgrammatically + BeginEdit() の組み合わせが鉄板で、こんな感じに使い分けるとユーザー操作のミスが減ります。

定石3: CellBeginEdit / CellEndEdit / RowValidating の使い分け

編集イベントは3段階で発火するので、目的別に使い分け:

// ✅ 定石3: 編集イベントの3段階制御
// 1. CellBeginEdit: 編集モードに入る直前
private void dataGridView1_CellBeginEdit(object sender, DataGridViewCellCancelEventArgs e)
{
    // 業務ロジックで「編集禁止条件」がある場合
    if (dataGridView1.Rows[e.RowIndex].Cells["status"].Value?.ToString() == "確定済み")
    {
        e.Cancel = true;   // ← 編集モードに入るのをキャンセル
    }
}

// 2. CellEndEdit: 編集モードを抜けた瞬間(値が確定)
private bool _isEditing = false;   // 無限ループ防止フラグ
private void dataGridView1_CellEndEdit(object sender, DataGridViewCellEventArgs e)
{
    if (_isEditing) return;   // 再入防止
    _isEditing = true;
    try
    {
        // セルの値を加工したい場合(例: 金額のカンマ整形)
        if (dataGridView1.Columns[e.ColumnIndex].Name == "amount")
        {
            var raw = dataGridView1.Rows[e.RowIndex].Cells[e.ColumnIndex].Value?.ToString();
            if (decimal.TryParse(raw, out var amount))
            {
                dataGridView1.Rows[e.RowIndex].Cells[e.ColumnIndex].Value = amount.ToString("N0");
            }
        }
    }
    finally
    {
        _isEditing = false;
    }
}

// 3. RowValidating: 行を抜けるタイミング(行全体の検証)
private void dataGridView1_RowValidating(object sender, DataGridViewCellCancelEventArgs e)
{
    var row = dataGridView1.Rows[e.RowIndex];
    if (row.IsNewRow) return;   // 新規行はスキップ

    // 必須項目チェック
    if (string.IsNullOrEmpty(row.Cells["name"].Value?.ToString()))
    {
        MessageBox.Show("名前は必須です");
        e.Cancel = true;   // ← 行確定をキャンセル
        return;
    }

    // 金額チェック
    if (decimal.TryParse(row.Cells["amount"].Value?.ToString(), out var amount))
    {
        if (amount < 0)
        {
            MessageBox.Show("金額は0以上を入力してください");
            e.Cancel = true;
        }
    }
}

ポイント:

  1. CellBeginEdit: 編集モード「入る前」の制御(条件で編集禁止)
  2. CellEndEdit: 編集モード「抜けた後」の値加工(再入防止フラグ必須
  3. RowValidating: 行を抜けるタイミングで行全体の検証(必須項目・整合性チェック)

業務系で頻出する 入力検証RowValidating 寄せ、値加工CellEndEdit + フラグパターン、編集禁止CellBeginEdite.Cancel = true、の使い分けっす。

定石4: EditingControlShowing で IME / 数値制限を仕込む

ユーザー入力にマスク制限(数値のみ・半角のみ・桁数制限)をかける時の本命:

// ✅ 定石4: EditingControlShowing で数値のみ入力を強制
private void dataGridView1_EditingControlShowing(object sender, DataGridViewEditingControlShowingEventArgs e)
{
    if (dataGridView1.CurrentCell?.OwningColumn.Name == "amount")
    {
        var textBox = e.Control as TextBox;
        if (textBox != null)
        {
            // 既存ハンドラを外す(重複登録防止)
            textBox.KeyPress -= NumericOnly_KeyPress;
            textBox.KeyPress += NumericOnly_KeyPress;

            // IME を OFF に(半角強制)
            textBox.ImeMode = ImeMode.Disable;
        }
    }
}

private void NumericOnly_KeyPress(object sender, KeyPressEventArgs e)
{
    // 数字 (0-9) と制御文字 (BackSpace 等) のみ許可
    if (!char.IsDigit(e.KeyChar) && !char.IsControl(e.KeyChar))
    {
        e.Handled = true;   // ← キー入力をキャンセル
    }
}

ポイント:

  1. EditingControlShowing イベントで編集コントロールへの介入
  2. KeyPress ハンドラを追加して数値以外を弾く
  3. -= で既存ハンドラを外してから +=(重複登録防止)
  4. IME を ImeMode.Disable で半角強制

業務系の 金額入力欄・電話番号・郵便番号・社員ID などで頻出のパターン。KeyPress で弾くか RowValidating で検証するかは好みだけど、入力中に弾く方がユーザー体験が良いので EditingControlShowing 寄せが業務系の鉄板っす。

定石5: DataBindingComplete で行単位 ReadOnly を維持

これが業務系で 一番ハマるパターン。DataSource バインドの前に Rows[i].ReadOnly = true を書いても効かない理由と対策:

// ❌ NG: DataBound 前に Rows[i].ReadOnly を設定(消える)
private void Form_Load(object sender, EventArgs e)
{
    dataGridView1.DataSource = LoadFromDb();
    dataGridView1.Rows[0].ReadOnly = true;   // ← DataBound 完了前で Rows が空 or 再生成される
}

// ✅ OK: DataBindingComplete イベントで設定
private void Form_Load(object sender, EventArgs e)
{
    dataGridView1.DataBindingComplete += DataGridView1_DataBindingComplete;
    dataGridView1.DataSource = LoadFromDb();
}

private void DataGridView1_DataBindingComplete(object sender, DataGridViewBindingCompleteEventArgs e)
{
    foreach (DataGridViewRow row in dataGridView1.Rows)
    {
        if (row.IsNewRow) continue;

        // 業務ロジックで動的に ReadOnly を決める
        var status = row.Cells["status"].Value?.ToString();
        if (status == "確定済み")
        {
            row.ReadOnly = true;   // 確定済みの行は編集不可
        }
    }
}

ポイント:

  1. DataSource 設定前後に Rows コレクションは再生成される
  2. DataBindingComplete イベントは DataBound 完了後に発火
  3. 行単位の動的 ReadOnly は DataBindingComplete 内で設定
  4. IsNewRow で新規追加行をスキップ(ぐるぐる挙動の予防)

俺が半日デバッガで追ってハマった事件はこれが原因でした。DataBound 前後で Rows が再生成される仕様を知らないと、いつまで経っても原因が分からない。DataBindingComplete で書き直すだけで一発で解決する罠っす。

ハマりポイント5つ — 実体験ベースの本番事故

1. DataBound 前に Rows[i].ReadOnly を設定しても効かない(半日デバッガで追ってハマった)

Form_LoadDataSource = ... の直後に Rows[0].ReadOnly = true を書いていて、画面ロード後も編集可能だった事件。夕方の運用報告で気づいて半日デバッガで追ってハマった末に、DataBindingComplete イベントに移して解決。連載第4回(ORM 3択)の DataBound 系の話とも整合する罠です。

2. ReadOnly = true なのに編集できる(30分溶かした)

dataGridView1.ReadOnly = true を設定したのに、画面の最下行(新規追加行)が編集可能だった事件。30分溶かした末に、AllowUserToAddRows = true(既定値)のままだったのが原因と判明。読み取り専用化の3点セット(ReadOnly + AllowUserToAddRows = false + AllowUserToDeleteRows = false)を業務系チーム規約にしました。

3. CellEndEdit で値を加工して無限ループ(数日プロファイラで追った)

CellEndEdit で金額をカンマ整形しようとして Cells.Value を書き換えたら、値変更で CellEndEdit が再発火 → 無限ループでアプリ固まる事件。数日プロファイラで追った末に、_isEditing フラグで再入防止する形にして解決。CellEndEdit で値加工する時はフラグ必須 を業務系チーム規約に揃えた。

4. EditMode = EditOnEnter で F4 ドロップダウンが効かない(夕方の運用報告で気づいた)

EditOnEnter を設定したら、ComboBoxColumn の F4 ドロップダウン開く挙動が効かなくなる事件。夕方の運用報告で気づいた末に、業務系標準の EditOnKeystrokeOrF2(既定値)に戻して解決。EditMode を変える時はコントロール側の挙動も検証する を業務系チーム規約に。

5. RowValidatinge.Cancel = true が効かない(30分溶かした)

BindingSource 経由で DataBind していた DataGridView で、RowValidatinge.Cancel = true が効かない事件。30分溶かした末に、BindingSource.EndEdit が先に走っていたのが原因と判明。BindingSource.RaiseListChangedEvents = false で抑制する or Validating 側で CancelEdit() 明示呼びで解決。

俺の現場メモ — 業務系チームでの DataGridView 編集制御規約

流通系SIer時代に過去コードを grep -rnE "dataGridView.*ReadOnly|EditMode|CellEndEdit|RowValidating" . で60箇所近くひっかけたら、DataBoundReadOnly 設定・CellEndEdit 無限ループ・AllowUserToAddRows 残しが全部入りだった。後輩と一緒に 3行ルール にまとめた:

  1. 行単位 ReadOnly は DataBindingComplete イベント内で設定Form_Load 直書きは禁止)
  2. 読み取り専用化は3点セット(ReadOnly + AllowUserToAddRows = false + AllowUserToDeleteRows = false)
  3. CellEndEdit で値加工する時は _isEditing フラグで再入防止(無限ループ予防)

このルール化で、DataGridView 周りの編集制御事故が消えた。書き方を1パターンに揃えるだけで保守工数と事故率が両方下がるおすすめルールっす。

まとめ

場面 推奨パターン
画面全体読み取り専用 ReadOnly + AllowUserToAddRows = false + AllowUserToDeleteRows = false(3点セット)
特定列だけ ReadOnly Columns["列名"].ReadOnly = true(列名指定)
動的に行単位 ReadOnly DataBindingComplete イベント内で設定
編集モード開始タイミング制御 EditMode = EditProgrammatically + BeginEdit()
行全体の入力検証 RowValidating + e.Cancel = true
セル値加工 CellEndEdit + _isEditing フラグ(無限ループ予防)
数値のみ入力 EditingControlShowing + KeyPress で弾く
BindingSource 経由の Validating BindingSource.RaiseListChangedEvents = false

DataGridView の編集制御は、「3層 ReadOnly + EditMode + イベント検証」 の組み合わせで業務系の要件は9割対応できます。DataBindingComplete で書き直すだけで「ReadOnly 効かない問題」は解決するし、_isEditing フラグで無限ループ事故も消える。書き方を1パターンに揃える のが業務SE の本命の対処です。

よくある質問

Q1. DataGridView 全体を読み取り専用にするには?

A. dataGridView1.ReadOnly = true; の1行で全体が読み取り専用になります。ただし AllowUserToAddRows = true(既定値)のままだと、最下行の新規追加行が編集可能なまま残るので、合わせて AllowUserToAddRows = false; も設定するのが業務系の鉄則。3点セット(ReadOnly + AllowUserToAddRows = false + AllowUserToDeleteRows = false)で完全な読み取り専用化が完成します。

Q2. 特定列だけを読み取り専用にしたい時は?

A. dataGridView1.Columns["price"].ReadOnly = true; で列単位の制御ができます。列名指定(Columns["name"])の方が列順変更に強いのでおすすめ。DataBound 後の行が編集可能で、特定列だけロックしたい業務系画面(ID 列・登録日時・計算結果列など)で頻出パターン。DataBindingComplete イベントで設定すると DataSource 切替後も維持されます。

Q3. DataBound 前に Rows[i].ReadOnly を設定したけど効かない時は?

A. DataBindingComplete イベントで設定してください。DataSource 設定の前に Rows[i].ReadOnly = true を書いても、DataSource バインド時に Rows コレクションが再生成されるので設定が消えます。dataGridView1.DataBindingComplete += (s, e) => { foreach (DataGridViewRow row in dataGridView1.Rows) { if (...) row.ReadOnly = true; } }; のパターンが業務系定番です。

Q4. CellEndEdit で値を加工すると無限ループするのは?

A. 値変更で CellValueChangedCellEndEdit の再発火が起きるためです。回避策は(1)_isEditing フラグで再入を防ぐ、(2)Cell.Value を直接書き換えるのではなく DataBoundItem のプロパティを書き換える、(3)CellValidating イベントで値検証 + e.Cancel = true でキャンセルする、の3パターン。業務系では _isEditing フラグパターンが一番分かりやすいです。

Q5. RowValidatinge.Cancel = true が効かない時は?

A. BindingSource 経由のデータバインドだと別の処理が走るケースがあります。DataSource を直接 DataTable にしてる場合は e.Cancel = true で行確定をキャンセルできますが、BindingSource 経由だと BindingSource.EndEdit が先に走って Cancel が効かない。対策は BindingSource.RaiseListChangedEvents = false でイベント発火を抑制するか、Validating イベント側で dataGridView1.CancelEdit() を明示的に呼ぶ形になります。

ここまでで DataGridView 編集制御の3層 ReadOnly・EditMode・イベント検証・ハマり5点は押さえた。DataGridView / WinForms 系の隣接トピックも貼っておきます。

関連記事

以上!

同じ罠でハマってる業務SE仲間いたら、どんどんシェア待ってるぜ!!

この記事が気に入ったら
いいねしてね!

どんどんシェア待ってるぜ!!
  • URLをコピーしました!

コメント

コメントする

CAPTCHA


目次