みなさんこんにちは!ヒロポンです!!
今回は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].ReadOnlyはDataBindingCompleteイベントで設定(DataSourceバインド前は効かない)

定石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;
ポイント:
dataGridView1.ReadOnly = trueが最強(全部読み取り専用)- 列単位は
Columns["列名"].ReadOnly(列名指定が列順変更に強い) - 行単位は
Rows[i].ReadOnly(DataBound後に設定する) - セル単位は
Rows[i].Cells[j].ReadOnly(最も粒度細かい) - 読み取り専用化の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;
}
}
}
ポイント:
CellBeginEdit:編集モード「入る前」の制御(条件で編集禁止)CellEndEdit:編集モード「抜けた後」の値加工(再入防止フラグ必須)RowValidating:行を抜けるタイミングで行全体の検証(必須項目・整合性チェック)
業務系で頻出する入力検証はRowValidating寄せ、値加工はCellEndEdit +フラグパターン、編集禁止はCellBeginEditでe.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; // ←キー入力をキャンセル
}
}
ポイント:
EditingControlShowingイベントで編集コントロールへの介入KeyPressハンドラを追加して数値以外を弾く-=で既存ハンドラを外してから+=(重複登録防止)- 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; //確定済みの行は編集不可
}
}
}
ポイント:
- DataSource設定前後にRowsコレクションは再生成される
DataBindingCompleteイベントはDataBound完了後に発火- 行単位の動的ReadOnlyはDataBindingComplete内で設定
IsNewRowで新規追加行をスキップ(ぐるぐる挙動の予防)
俺が半日デバッガで追ってハマった事件はこれが原因でした。DataBound前後でRowsが再生成される仕様を知らないと、いつまで経っても原因が分からない。DataBindingCompleteで書き直すだけで一発で解決する罠っす。
ハマりポイント5つ—実体験ベースの本番事故
1. DataBound前にRows[i].ReadOnlyを設定しても効かない(半日デバッガで追ってハマった)
Form_LoadでDataSource = ...の直後に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. RowValidatingのe.Cancel = trueが効かない(30分溶かした)
BindingSource経由でDataBindしていたDataGridViewで、RowValidatingのe.Cancel = trueが効かない事件。30分溶かした末に、BindingSource.EndEditが先に走っていたのが原因と判明。BindingSource.RaiseListChangedEvents = falseで抑制するor Validating側でCancelEdit()明示呼びで解決。
俺の現場メモ—業務系チームでのDataGridView編集制御規約
流通系SIer時代に過去コードをgrep -rnE "dataGridView.*ReadOnly|EditMode|CellEndEdit|RowValidating" .で60箇所近くひっかけたら、DataBound前ReadOnly設定・CellEndEdit無限ループ・AllowUserToAddRows残しが全部入りだった。後輩と一緒に3行ルールにまとめた:
- 行単位ReadOnlyは
DataBindingCompleteイベント内で設定(Form_Load直書きは禁止) - 読み取り専用化は3点セット(ReadOnly + AllowUserToAddRows = false + AllowUserToDeleteRows = false)
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. 値変更でCellValueChanged → CellEndEditの再発火が起きるためです。回避策は(1)_isEditingフラグで再入を防ぐ、(2)Cell.Valueを直接書き換えるのではなくDataBoundItemのプロパティを書き換える、(3)CellValidatingイベントで値検証+ e.Cancel = trueでキャンセルする、の3パターン。業務系では_isEditingフラグパターンが一番分かりやすいです。
Q5. RowValidatingでe.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系の隣接トピックも貼っておきます。
関連記事
- C# DataGridViewのDataSourceを後から変更する全パターン — DataSource切替時の編集状態保持を整理する時に効く
- C# DataAdapter.Update()でDBNull例外が出た時の最短対処 — DataGridViewで編集した結果をDBに反映する時に効く
- WinForms ComboBoxのDataSourceバインディングとSelectedIndex / SelectedValue / SelectedItemの違い — DataGridView内のComboBoxColumnのDataBindを整える時に効く
以上!
同じ罠でハマってる業務SE仲間いたら、どんどんシェア待ってるぜ!!
執筆者
バイブス父さん — 業務 SE 7 年 (正社員 2 / フリーランス 5)。 現職は SEO 直轄部の AI アドバイザー兼 PL、 副業で中小 SIer の CTO。 SES 複数社・フリーランスエージェント複数経由の経験ベースで「業務 SE 視点」 の技術 + キャリア記事を書いています。
🐦 X: @hiro_progra0524 (日々の現場メモ更新中)
📝 About Me で経歴詳細を見る


コメント