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仲間いたら、どんどんシェア待ってるぜ!!

執筆者

バイブス父さん — 業務 SE 7 年 (正社員 2 / フリーランス 5)。 現職は SEO 直轄部の AI アドバイザー兼 PL、 副業で中小 SIer の CTO。 SES 複数社・フリーランスエージェント複数経由の経験ベースで「業務 SE 視点」 の技術 + キャリア記事を書いています。

🐦 X: @hiro_progra0524 (日々の現場メモ更新中)
📝 About Me で経歴詳細を見る

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

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

コメント

コメントする

CAPTCHA


目次