C# DataGridView の DataSource を後から変更する全パターン
検索ボックスでフィルタかけて DataGridView を更新するだけ、のはずだったのに、なぜか列が倍に増えて、ヘッダーが日本語と英語の混在になり、選択行のイベントが二重で飛んでくる──。VS2019・.NET Framework 4.7.2 で DataGridView の DataSource を後から差し替える現場の人なら、一度は踏んだ気がする。
俺もこれで何回も詰まったので、今回は C# の DataGridView の DataSource を後から変更する時の全パターン をまとめておく。.NET Framework 4.7.2 / VS2019 + DataTable 構成の業務系前提で、コピペで動く形でコードを置いていく。
結論:DataGridView の DataSource 差し替えは「3つの順序」を守れば事故らない
先に結論。後から DataSource を差し替える時は、次の順番で操作するとほぼ事故らない。
AutoGenerateColumns = falseを最初に立てる(列の増殖を止める)DataSource = nullで一度クリアしてから新しい DataSource を入れる(バインドの引きずりを切る)- 再バインドが必要な場合は
BindingSourceを間に挟む(イベント・整列状態を維持)
この3点をまず押さえると、9割の「DataGridView がおかしい」が消える。残りの1割はイベントハンドラの再登録周りだが、これは後ろのハマりポイントで触れる。

なぜ DataSource を差し替えると列が増える・消える・反映されないのか
DataGridView は DataSource をセットするタイミングで自動的に列定義を読み込む という挙動を持っている。これが事故の元になる。典型的な3パターンを並べておく。
パターン1: AutoGenerateColumns が true のまま差し替え
何も設定していない DataGridView は AutoGenerateColumns = true(既定値)。この状態で別の DataTable を DataSource に入れると、新しい DataTable のスキーマ通りに列が自動追加 される。前の列定義を「上書き」してくれるわけではない。
// NGパターン:AutoGenerateColumns=true のまま差し替える
dgv.DataSource = dt1; // 列が3個(ID/名前/金額)追加される
dgv.DataSource = dt2; // 列がさらに3個(ID/コード/数量)追加される ← 重複や増殖
VS のデザイナで日本語のヘッダーを設定していたカラムが、差し替えで英語の列名になって混ざる、というのもこのパターン。
パターン2: DataSource = null を挟まずに直接置き換え
null を経由せずに別の DataTable を直接代入すると、内部のバインド情報が前のソースを引きずったまま新しいものを乗せる形になる。タイミングによっては CurrentCell の例外、Selection 周りの NRE、Sort の状態崩れが出る。
// NGパターン:null を挟まない
dgv.DataSource = dt1;
// (ユーザーが操作)
dgv.DataSource = dt2; // 前の選択行・ソート状態を引きずる
パターン3: Columns.Clear() を呼ぶタイミングを間違えている
Columns.Clear() で列定義を一度全削除する手もあるが、DataSource がバインドされている状態で叩くと例外が飛ぶ。
// NGパターン:DataSource バインド中に Columns.Clear()
dgv.DataSource = dt1;
dgv.Columns.Clear(); // System.InvalidOperationException
正しい順序は「DataSource = null → Columns.Clear() → Columns.Add(...) → DataSource = newDt」。順序を間違えると本番障害の朝、ログに InvalidOperationException が並ぶことになる。俺もこの順序で1時間溶かした。
最短対処:コピペで動く3つの差し替えパターン
ここから本題。実際に現場で動かしている書き方を3つ並べる。全部 .NET Framework 4.7.2 / VS2019 で動作確認済み の書き方。
パターン1: AutoGenerateColumns を切って手動で列定義する(推奨)
業務系で一番安全なやつ。デザイナで列を組んで、AutoGenerateColumns = false にしておく。
// Form_Load などの初期化時に1回だけ
dgv.AutoGenerateColumns = false;
// デザイナで作っておいた列の DataPropertyName を DataTable のカラム名と合わせる
// dgv.Columns["colId"].DataPropertyName = "id";
// dgv.Columns["colName"].DataPropertyName = "name";
// 差し替え時
dgv.DataSource = null;
dgv.DataSource = newDataTable;
この書き方の強みは、ヘッダー・幅・並び順・書式(NumberFormat、DateFormat)がデザイナの設定そのまま固定される こと。データだけが入れ替わる。業務系の画面でユーザー教育コストを下げるのに効く。

パターン2: BindingSource を間に挟む
検索/絞り込みのように頻繁に差し替える画面なら、BindingSource を中継するのが楽。
// フィールドとして1個持っておく
private readonly BindingSource _bs = new BindingSource();
// 初期化時
dgv.AutoGenerateColumns = false;
dgv.DataSource = _bs;
// 差し替え時は BindingSource の DataSource だけ書き換える
_bs.DataSource = newDataTable;
DataGridView の DataSource 自体は BindingSource のままなので、選択状態・ソート方向・スクロール位置の引きずりが出にくい。Filter / Sort をライブで切り替えたい画面はこの形が定石。
パターン3: 列を完全に作り直す(複数の異なるテーブルを切り替えたい時)
「商品マスタ用の表示」と「在庫推移用の表示」みたいに、そもそも列の意味が違う DataTable を1つの DataGridView で切り替えたいケース。
private void SwitchToInventory(DataTable dt)
{
dgv.DataSource = null;
dgv.Columns.Clear();
dgv.AutoGenerateColumns = false;
dgv.Columns.Add(new DataGridViewTextBoxColumn
{
Name = "colDate",
HeaderText = "日付",
DataPropertyName = "date",
Width = 100,
});
dgv.Columns.Add(new DataGridViewTextBoxColumn
{
Name = "colQty",
HeaderText = "在庫数",
DataPropertyName = "qty",
DefaultCellStyle = { Format = "N0", Alignment = DataGridViewContentAlignment.MiddleRight },
Width = 80,
});
dgv.DataSource = dt;
}
順序が大事:DataSource = null → Columns.Clear() → Columns.Add() → DataSource = dt の流れ。AutoGenerateColumns = false も DataSource に新しい値を入れる前に立てておく。
ハマりポイント:列の制御が効いた後で踏むやつ
ここからは、上の3パターンを入れた 後 で踏む典型ハマり。順番に書く。
イベントハンドラが二重で飛んでくる
SelectionChanged や CellValueChanged のイベントハンドラを、初期化時にデザイナ経由で += していると、DataSource を差し替えるたびに 内部的に再評価が走って大量にイベントが飛ぶ。差し替え処理の前後で一時的に外す。
private void RebindGrid(DataTable newDt)
{
dgv.SelectionChanged -= Dgv_SelectionChanged;
try
{
dgv.DataSource = null;
dgv.DataSource = newDt;
}
finally
{
dgv.SelectionChanged += Dgv_SelectionChanged;
}
}
finally で確実に戻す書き方にしておかないと、例外が出たときにイベントが永久に剥がれた状態になる。これも俺が現場で踏んで30分溶かした。
CurrentCell の null 参照例外
DataSource = null を入れた瞬間、dgv.CurrentCell も null に飛ぶ。SelectionChanged ハンドラの中で dgv.CurrentCell.Value.ToString() のような書き方をしていると、ここで NRE が出る。
// NGパターン:null チェック無し
private void Dgv_SelectionChanged(object sender, EventArgs e)
{
label1.Text = dgv.CurrentCell.Value.ToString(); // 差し替え直後にここで落ちる
}
// 安全な書き方
private void Dgv_SelectionChanged(object sender, EventArgs e)
{
var cell = dgv.CurrentCell;
if (cell?.Value == null || cell.Value is DBNull) return;
label1.Text = cell.Value.ToString();
}
?. と is DBNull のセットでガードする書き方がレガシー業務系では一番事故が少ない。
System.NullReferenceException: オブジェクト参照がオブジェクト インスタンスに設定されていません。
このスタックトレースが Dgv_SelectionChanged を指している時、9割このパターン。
Sort の方向と矢印が消える
DataSource = null を挟んで差し替えると、ヘッダーの ソート矢印(▲▼)が消える ことがある。SortMode = Automatic で運用していて、ユーザーが特定列で並べた状態を維持したい時は、Sort を再適用する。
// 並び順を保持して差し替える
var sortedCol = dgv.SortedColumn?.Name;
var sortDir = dgv.SortOrder;
dgv.DataSource = null;
dgv.DataSource = newDt;
if (sortedCol != null && sortDir != SortOrder.None)
{
var dir = sortDir == SortOrder.Ascending
? ListSortDirection.Ascending
: ListSortDirection.Descending;
dgv.Sort(dgv.Columns[sortedCol], dir);
}
このひと手間で、ユーザーから見ると「画面の状態がそのまま、データだけ更新された」風の挙動になる。
現場メモ:DataGridView は枯れているが、だからこそ価値がある
ここから少し本題から離れる。
C# WinForms の DataGridView は、もう新しい機能が増えない領域。WPF の DataGrid や MAUI の CollectionView の方が見た目もモダンで、SNS で出てくる .NET 系の話題はだいたいそっち。同期は転職して モダン Web をやっている、みたいな話を聞いて、自分の現場の VS2019 がやけに古く見える日もあると思う。家族との生活もあって、いきなりキャリアを切り替える話でもないので、温度感を上げすぎず付き合うのが現実的。
ただ、業務系の DataGridView は ハマりどころが固定されていて、答えが20年前から決まっている という強みがある。今日書いた AutoGenerateColumns / BindingSource / Columns.Clear() の順序の話は、2005年の .NET 2.0 の時代から同じ書き方が効いているし、たぶん2031年のレガシー保守でも同じやり方が効く。枯れた構成のハマりどころを3つ知っているだけで、障害対応の時間が体感で半分になる。
俺自身、流通系のSIerに正社員でいた時は、DataGridView の挙動を理解しないまま「とりあえずデータが見えればいい」で書いていた。後から BindingSource の存在を知って、それまで自分が書いていたコードの大半が無駄だった、と気づいた瞬間が一番効いた。額面450万・残業30hの時代に、夜中に DataGridView のドキュメントを1時間だけ読む時間を作ったのが、後で効いた一番大きな投資だった気がする。
「VS2019 + DataGridView はもう古い」と言われると気が滅入るが、枯れた構成を確実に動かせる人は、AI が書いたコードを直せる人 でもある。2028年以降、AI が書いた WinForms 保守コードの修正案件は確実に出てくる。その時に強いのは、デザイナと挙動の癖を肌で知っている人。
まとめ
- DataGridView の DataSource 差し替えは、
AutoGenerateColumns = false→DataSource = null→ 新しい DataSource の順序で事故らない - 頻繁に差し替えるなら
BindingSourceを中継する。選択状態・ソート状態の引きずりが出にくい - 列定義ごと作り直すなら
DataSource = null→Columns.Clear()→Columns.Add()→DataSource = dtの順序を守る SelectionChangedなどのイベントは差し替え前後で-=/+=で剥がして戻す。finally必須CurrentCellは差し替え直後に null になる。?.とis DBNullでガード- ソート方向はリバインドで消えるので
SortedColumn/SortOrderを退避してSort()を再適用
ここまで覚えておけば、検索フォームを作るたびに DataGridView と格闘しなくて済む。
よくある質問
Q1. DataSource = null をしないで dgv.Rows.Clear() で済ませてはダメ?
Rows.Clear() は DataSource がバインドされた状態だと例外 が出る(バインド済みの行コレクションは直接削除できない)。DataSource = null を経由するか、DataTable.Rows.Clear() 側を呼ぶのが正解。
Q2. BindingSource を毎回 new しないとダメ?
new しなくていい。フィールドで1個持っておいて、_bs.DataSource = ... だけ書き換える形にする。new し直すと選択状態・Filter・Sort の設定が全部初期化されるので、むしろ意図しない挙動の元になる。
Q3. DataGridView を継承して独自クラスにすると幸せになれる?
業務系で複数画面を作るなら効くが、最初は標準の DataGridView でハマりどころを把握する方が先。継承して OnDataBindingComplete をオーバーライドする手はあるが、入った現場ごとに作法が違うので、まずはイベントを +=/-= で外側から制御する書き方に慣れたほうがつぶしが効く。
Q4. Entity Framework や ORM 経由のデータでも同じ書き方で動く?
ほぼ同じ。DataSource に渡すのが DataTable でも List<T> でも BindingList<T> でも、列の自動生成・null クリア・BindingSource 中継のロジックは共通。List<T> だと Sort() の挙動が一部違うので、ソートを保持したい場合は BindingList<T> か SortableBindingList<T> 系を検討する。
Q5. デザイナで設定したヘッダー幅が差し替え時にリセットされるのは?
AutoGenerateColumns = true のままだとリセットされる。AutoGenerateColumns = false にして、デザイナで定義した列の DataPropertyName だけ DataTable のカラム名と合わせる形にすれば、幅・書式・並び順は全部維持される。これが業務系で一番使う書き方。
以上!


コメント