C# DataGridView の DataSource を後から変更する全パターン

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 を差し替える時は、次の順番で操作するとほぼ事故らない。

  1. AutoGenerateColumns = false を最初に立てる(列の増殖を止める)
  2. DataSource = null で一度クリアしてから新しい DataSource を入れる(バインドの引きずりを切る)
  3. 再バインドが必要な場合は BindingSource を間に挟む(イベント・整列状態を維持)

この3点をまず押さえると、9割の「DataGridView がおかしい」が消える。残りの1割はイベントハンドラの再登録周りだが、これは後ろのハマりポイントで触れる。

DataGridView DataSource 差し替え3ステップのフロー

なぜ 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 = nullColumns.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)がデザイナの設定そのまま固定される こと。データだけが入れ替わる。業務系の画面でユーザー教育コストを下げるのに効く。

AutoGenerateColumns true / false 比較イメージ

パターン2: BindingSource を間に挟む

検索/絞り込みのように頻繁に差し替える画面なら、BindingSource を中継するのが楽。

// フィールドとして1個持っておく
private readonly BindingSource _bs = new BindingSource();

// 初期化時
dgv.AutoGenerateColumns = false;
dgv.DataSource = _bs;

// 差し替え時は BindingSource の DataSource だけ書き換える
_bs.DataSource = newDataTable;

DataGridViewDataSource 自体は 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 = nullColumns.Clear()Columns.Add()DataSource = dt の流れ。AutoGenerateColumns = falseDataSource に新しい値を入れる前に立てておく。

ハマりポイント:列の制御が効いた後で踏むやつ

ここからは、上の3パターンを入れた で踏む典型ハマり。順番に書く。

イベントハンドラが二重で飛んでくる

SelectionChangedCellValueChanged のイベントハンドラを、初期化時にデザイナ経由で += していると、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.CurrentCellnull に飛ぶ。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 = falseDataSource = null → 新しい DataSource の順序で事故らない
  • 頻繁に差し替えるなら BindingSource を中継する。選択状態・ソート状態の引きずりが出にくい
  • 列定義ごと作り直すなら DataSource = nullColumns.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 のカラム名と合わせる形にすれば、幅・書式・並び順は全部維持される。これが業務系で一番使う書き方。


以上!

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

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

コメント

コメントする

CAPTCHA


目次