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のカラム名と合わせる形にすれば、幅・書式・並び順は全部維持される。これが業務系で一番使う書き方。
以上!


コメント