WinForms ComboBox の DataSource バインディングと SelectedIndex / SelectedValue / SelectedItem の違い

みなさんこんにちは!ヒロポンです!!

今回は WinForms 業務SE現場でガチで踏みやすいやつ!!の話。

「DataAdapter で取った DataTable を ComboBox.DataSource に流したのに、SelectedValue で取った値が期待してた ID じゃなく表示名の string で来てキャストで死ぬ」「DisplayMember を設定したのに画面に出るのが ToString() のままで効いてない」「ComboBox の DataSource を更新したら SelectedIndexChanged二重発火して本番更新が2回走った」みたいなComboBox バインディングの事故って、業務SE で誰しも一回はやらかしますよね??

俺も2年目くらいの流通系SIer時代に、DataSource → DisplayMember → ValueMember の順で書いてしまって、画面に ID(数値)が出続けて表示名が出ない事故をやらかしたことがあります。朝、客先で席に着いた瞬間に「ComboBox に変な数字出てる」って肩を叩かれて半日デバッガで追ってハマったやつ。原因は1行で済む話で、設定順序が逆だっただけ。

この記事では VS2019 / .NET Framework 4.7.2 / C# 7.3 環境で、WinForms ComboBox の DataSource バインディングDisplayMember / ValueMember の設定順序Selected3兄弟(SelectedIndex / SelectedValue / SelectedItem)の使い分けSelectedIndexChanged 二重発火の回避DataRowView キャストの5つの定石を、コード7本でまとめます。後ろの「現場メモ」で、業務系チームでルール化した時の話も書いてる。

3行で結論:

  • 設定順序は DisplayMember → ValueMember → DataSource(逆だと効かないケースあり)
  • 取得は ValueMember + SelectedValue 寄せ(業務SE最頻)/行全体は SelectedItem を DataRowView キャスト
  • DataSource 差し替え時の二重発火回避は、イベントハンドラを -= / += で一時的に外すのが鉄板
目次

定石1: DataSource バインディング基本(DataTable / List / 配列の3パターン)

ComboBox の DataSource には3つのパターンを割り当てられる:

// ✅ 定石1-a: DataTable をバインド(業務SE最頻)
DataTable dt = LoadCustomerMaster();   // DataAdapter で取得済み
combo.DisplayMember = "name";           // 画面に出すカラム
combo.ValueMember   = "id";             // 内部で扱うカラム
combo.DataSource    = dt;

// ✅ 定石1-b: List<T> をバインド
public class CustomerVm { public int Id { get; set; } public string Name { get; set; } }

var list = new List<CustomerVm>
{
    new CustomerVm { Id = 1, Name = "鈴木商事" },
    new CustomerVm { Id = 2, Name = "山田工業" },
};
combo.DisplayMember = "Name";   // プロパティ名
combo.ValueMember   = "Id";
combo.DataSource    = list;

// ✅ 定石1-c: 配列をバインド(DisplayMember/ValueMember なし、ToString() だけ表示)
combo.DataSource = new[] { "東京", "大阪", "名古屋" };
// → 画面: 東京 / 大阪 / 名古屋、SelectedItem は string がそのまま返る

業務系で一番多いのは DataTable パターン。DataAdapter で取った結果を List<T> に詰め替えずにそのまま DataSource に流せるのが業務SE現場の鉄板。List<T> パターンは新規開発で型安全に扱いたい時、配列パターンは静的選択肢のみの簡易ケースで使い分けます。

定石2: DisplayMember / ValueMember の設定順序の罠

ここが業務SEで一番踏みやすい罠っす。DataSource を先にセットすると、その時点で DisplayMember 未指定 = 行の ToString() で表示が確定してしまうケースがあるんですよね:

// ❌ NG: DataSource を先に設定すると DisplayMember が効かないケース
combo.DataSource    = dt;          // ← この時点で表示が "DataRowView" の ToString() で固まる
combo.DisplayMember = "name";       // ← 後から設定しても効かない or 微妙な挙動
combo.ValueMember   = "id";

// ✅ OK: DisplayMember → ValueMember → DataSource の順
combo.DisplayMember = "name";
combo.ValueMember   = "id";
combo.DataSource    = dt;          // ← 最後にセット、Display/Value 両方とも正しく適用

ん?逆順でも動くって聞いたよ??って思うかもだけど、.NET Framework 4.7.2 の業務系現場では順序起因で表示が壊れるケースが普通にある。Microsoft Docs にも「DataSource プロパティの設定後に Display/Value を変更する場合、再バインドが走る挙動」と書いてあるけど、実装依存の挙動なので最初から順序を守るのが安全っす。

ハマりどころ:

  • DisplayMember 未指定で DataSource を渡すと、DataRowViewToString()(だいたい System.Data.DataRowView の文字列)が全行同じ表示になって死ぬ
  • ValueMember 未指定だと SelectedValuenull になる
  • 業務系チームでルール化するなら Form_Load で順序を守って書くを死守

定石3: Selected3兄弟(Index / Value / Item)の使い分け

ComboBox の選択値を取る方法は3つあって、目的別に使い分けます:

// ✅ 定石3: Selected3兄弟の使い分け
private void btnSubmit_Click(object sender, EventArgs e)
{
    // 1. SelectedIndex: 選択行番号(int、未選択は -1)
    int idx = combo.SelectedIndex;
    if (idx < 0) { MessageBox.Show("選択してください"); return; }

    // 2. SelectedValue: ValueMember で指定したカラムの値(object)
    //    業務SE最頻、ID 取得に使う
    int customerId = (int)combo.SelectedValue;   // ValueMember="id" で id 列が int 型なら

    // 3. SelectedItem: 選択された行そのもの(DataRowView や T が返る)
    //    行全体を扱いたい時に使う
    if (combo.SelectedItem is DataRowView row)
    {
        string name  = row.Row.Field<string>("name");
        int    code  = row.Row.Field<int>("id");
        string memo  = row.Row.Field<string>("memo");
        // 行の他の列も読める
    }
}

使い分け早見表:

目的 プロパティ 戻り型
順番だけ知りたい SelectedIndex int(-1 = 未選択)
ID(ValueMember)が欲しい SelectedValue object(要キャスト)
行全体(複数列)が欲しい SelectedItem DataRowView or T

業務SE現場で一番使うのは SelectedValue(ID で取って後段の処理に渡す)。SelectedItem は行の他の列も触りたい時、SelectedIndex は順番ロジックだけで十分な時、と覚えると整理しやすいっす。

定石4: SelectedIndexChanged 二重発火の回避

DataSource を再代入すると、内部で「リセット → 新しいデータバインド → 最初の項目を選択」の3ステップが走って、その途中で SelectedIndex が動いて SelectedIndexChanged が複数回発火 する事故が起きます:

// ❌ NG: DataSource 切り替え時に二重発火、本番更新が2回走る
private void combo_SelectedIndexChanged(object sender, EventArgs e)
{
    int customerId = (int)combo.SelectedValue;
    UpdateCustomerView(customerId);   // ← DataSource 切替時に複数回呼ばれる
}

// ✅ OK 1: イベントを一時的に外す(鉄板パターン)
combo.SelectedIndexChanged -= combo_SelectedIndexChanged;
combo.DataSource = newDt;
combo.SelectedIndexChanged += combo_SelectedIndexChanged;

// ✅ OK 2: フラグで初期化中はスキップ
private bool _isInitializing = false;

private void combo_SelectedIndexChanged(object sender, EventArgs e)
{
    if (_isInitializing) return;
    int customerId = (int)combo.SelectedValue;
    UpdateCustomerView(customerId);
}

private void LoadCombo(DataTable newDt)
{
    _isInitializing = true;
    try
    {
        combo.DataSource = newDt;
    }
    finally
    {
        _isInitializing = false;
    }
}

俺の業務SE時代は イベントを -= / += で外すパターン が鉄板でした。try-finally 不要で書ける上に、フラグ変数を持ち回さなくていい。チーム規約として「DataSource 差し替えはイベント外しとセットで書く」を入れておくと事故率がいい感じに下がります。

業務系の本番事故ワースト級なので、grep -rn "DataSource = " . で全件チェックして、二重発火対策が入ってるか確認するのがレビュー観点の一つになる場面っす。

定石5: DataSource 切替時の選択状態保持パターン

DataSource を別の DataTable に差し替える時、選択状態を保持したいケースが業務SE現場では普通にあります(フィルタ条件変更時に元の選択を維持したい等):

// ✅ 定石5: DataSource 切替で選択状態を保持する3ステップ
private void RefreshCombo(DataTable newDt)
{
    // 1. 現在の SelectedValue を保存
    object prevValue = combo.SelectedValue;

    // 2. イベントを外して DataSource を切り替え
    combo.SelectedIndexChanged -= combo_SelectedIndexChanged;
    combo.DataSource = newDt;
    combo.SelectedIndexChanged += combo_SelectedIndexChanged;

    // 3. 保存した SelectedValue を復元(新しいデータに該当 ID があれば)
    if (prevValue != null)
    {
        combo.SelectedValue = prevValue;   // 該当なしなら -1(未選択)になる
    }
}

このパターンを業務系チームの規約として揃えておくと、画面リロード・フィルタ切替・データ再取得時の挙動がいい感じに統一されます。

ハマりポイント — 実体験ベースの本番事故3点

1. DisplayMember を DataSource 後に設定して効かなかった(半日デバッガで追ってハマった)

朝、客先で席に着いた瞬間に「ComboBox に変な数字(DataRowView の ToString)が出てる」って肩を叩かれた事件。半日デバッガで追ってハマった末に、combo.DataSource = dt; を最初に書いて combo.DisplayMember = "name"; を後ろに書いていたのが原因と判明。順序を逆にしただけで解決した。それ以来、業務系チームで「DisplayMember → ValueMember → DataSource の順を守る」をルール化しました。

2. SelectedIndexChanged 二重発火で本番更新が2回走った(30分溶かした)

別案件で、ComboBox の選択変更時に DB 更新するロジックが1回の選択で2回 UPDATE が飛ぶ事件。原因は DataSource 切替時の二重発火で、初期ロード時の自動選択が SelectedIndexChanged を呼んでた。30分溶かした末に、イベントハンドラを -= / += で外すパターンに書き換えて解決。「DataSource 差し替え時はイベント外す」を業務系チーム規約に入れた。

3. SelectedValue が予期せぬ型でキャスト失敗(夕方の運用報告で気づいた)

ValueMember = "code" でセットしたが、code カラムが string 型だったのに int customerId = (int)combo.SelectedValue; でキャストしていて InvalidCastException が飛んだ事件。夕方の運用報告で「画面が落ちる」って報告で気づいたSelectedValue の型は ValueMember カラムの SQL 型に依存するので、DB スキーマと C# 側の受け型を厳密に揃える運用に変えました。(int) ではなく Convert.ToInt32(combo.SelectedValue) で寛容に受けるパターンも業務系では検討の余地ありです。

俺の現場メモ — 業務系チームでの ComboBox バインディングルール

流通系SIer時代に、過去コードを grep -rn "ComboBox\|DataSource = " . でひっかけたら、70箇所近く 出てきたんですよね。書き方がバラバラで、DataSource → DisplayMember → ValueMember の順で書いてるやつ、二重発火対策なしで SelectedIndexChanged 内で UPDATE 飛ばしてるやつ、SelectedValue(int) でハードキャストしてるやつ、全部入り。

んで、後輩と一緒に 3行ルール にまとめた:

  1. DisplayMember → ValueMember → DataSource の順を守る(逆順は新規禁止)
  2. DataSource 切替時はイベントハンドラを -= / += で一時的に外す(二重発火回避テンプレ)
  3. SelectedValue のキャストは ValueMember カラムの型と完全一致(型ミスは Convert.ToXxx で寛容受けする選択肢もあり)

このルール化で、ComboBox 周りの本番事故が消えた。書き方を1パターンに揃えるだけで保守工数も事故率も両方下がるので、業務系チームには結構おすすめのルールっす。

C# 7.3 + .NET Framework 4.7.2 + WinForms のレガシー業務系って、ComboBox のバインディング API は10年以上変わってないのに、書き方が現場ごとにバラバラなコードベースが本当に多い。書き方のアップデートだけで保守工数が下がる実例だと思ってます。

まとめ

状況 推奨パターン
DataSource バインディング DataTable / List / 配列の3択
Display / Value の設定順序 DisplayMember → ValueMember → DataSource
ID(ValueMember)取得 combo.SelectedValue を ValueMember 型でキャスト
行全体(複数列)取得 combo.SelectedItem as DataRowView 経由
順番だけ取得 combo.SelectedIndex(未選択 = -1)
DataSource 切替時の二重発火 イベントを -= / += で一時的に外す
選択状態保持 SelectedValue 保存 → DataSource 切替 → SelectedValue 復元

WinForms ComboBox のバインディング事故は、「設定順序を守る」「Selected3兄弟を目的別に使い分ける」「DataSource 切替時のイベント外し」 の3点で9割消えます。DataSource = を書く時に DisplayMember/ValueMember を上に置いてあるかを毎回確認する習慣がつくと、業務系の保守がいい感じに楽になる。

よくある質問

Q1. DisplayMemberValueMember はどっちを先に設定すれば?

A. DisplayMemberValueMemberDataSource の順で設定してください。DataSource を先にセットすると、その時点で「DisplayMember 未指定 = 行のオブジェクト ToString()」で表示が確定してしまい、後から DisplayMember を変えても効かないケースがあります。Form_Load や初期化メソッドの中で、この順番で書くのが鉄則です。

Q2. SelectedValueSelectedItemSelectedIndex の使い分けは?

A. SelectedValue: ValueMember で指定したカラムの値(業務SE最頻、ID 取得用)。SelectedItem: 選択された行そのもの(DataRowViewList<T>T が返る)。SelectedIndex: 行番号(int、0始まり、未選択は -1)。「ID で取りたい → SelectedValue」「行全体を扱いたい → SelectedItem」「順番だけ知りたい → SelectedIndex」で覚えると整理しやすいです。

Q3. SelectedIndexChangedDataSource 設定時に二重発火するのはなぜ?

A. ComboBox.DataSource を再代入すると、内部で「リセット → 新しいデータバインド → 最初の項目を選択」の3ステップが走り、その途中で SelectedIndex が変動して SelectedIndexChanged が複数回発火します。回避策は(1)DataSource 設定の前後でイベントハンドラを -= / += で一時的に外す、(2)BeginUpdate / EndUpdate で囲む、(3)フラグ変数で「初期化中はイベント処理をスキップ」する、の3パターン。業務SEの本番事故ワースト級なので順序起因として覚えておくと事故りにくいです。

Q4. DataSource を null にしないで別の DataTable に切り替えても大丈夫?

A. 大丈夫ですが、選択状態が消える事故が起きやすい。DataSource を別の DataTable に差し替える時は、(1)SelectedValue を保存、(2)DataSource を新しいデータに置き換え、(3)保存した SelectedValue を再代入、の3ステップで選択状態を保持できます。null を経由せず直接差し替える派と null 経由派は流派があるけど、業務系では3ステップ書ける選択状態保持パターンが安全です。

Q5. SelectedItemDataRowView にキャストして Field<T> アクセスは安全?

A. DataSourceDataTableDataView の時は安全です。var row = (DataRowView)combo.SelectedItem; var name = row.Row.Field<string>("name"); の形が業務SE定番。SelectedItem が null の可能性があるので null チェック or as キャストを噛ませてください。DataSourceList<T> なら直接 (T) キャストで OK です。

ここまでで WinForms ComboBox のバインディング・選択値取得・二重発火回避は押さえた。WinForms の DataSource 系の隣接トピックも貼っておきます。

関連記事

以上!

同じ罠でハマってる業務SE仲間いたら、どんどんシェア待ってるぜ!!

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

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

コメント

コメントする

CAPTCHA


目次