みなさんこんにちは!ヒロポンです!!
今回は 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仲間いたら、どんどんシェア待ってるぜ!!


コメント