みなさんこんにちは!ヒロポンです!!
今回は 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を渡すと、DataRowViewのToString()(だいたいSystem.Data.DataRowViewの文字列)が全行同じ表示になって死ぬValueMember未指定だとSelectedValueがnullになる- 業務系チームでルール化するなら
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行ルール にまとめた:
- DisplayMember → ValueMember → DataSource の順を守る(逆順は新規禁止)
- DataSource 切替時はイベントハンドラを
-=/+=で一時的に外す(二重発火回避テンプレ) SelectedValueのキャストは ValueMember カラムの型と完全一致(型ミスはConvert.ToXxxで寛容受けする選択肢もあり)
このルール化で、ComboBox 周りの本番事故が消えた。書き方を1パターンに揃えるだけで保守工数も事故率も両方下がるので、業務系チームには結構おすすめのルールっす。
C# 7.3 + .NET Framework 4.7.2 + WinForms のレガシー業務系って、ComboBox のバインディング API は10年以上変わってないのに、書き方が現場ごとにバラバラなコードベースが本当に多い。書き方のアップデートだけで保守工数が下がる実例だと思ってます。
まとめ
| 状況 | 推奨パターン |
|---|---|
| DataSource バインディング | DataTable / List |
| 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. DisplayMember と ValueMember はどっちを先に設定すれば?
A. DisplayMember → ValueMember → DataSource の順で設定してください。DataSource を先にセットすると、その時点で「DisplayMember 未指定 = 行のオブジェクト ToString()」で表示が確定してしまい、後から DisplayMember を変えても効かないケースがあります。Form_Load や初期化メソッドの中で、この順番で書くのが鉄則です。
Q2. SelectedValue と SelectedItem と SelectedIndex の使い分けは?
A. SelectedValue: ValueMember で指定したカラムの値(業務SE最頻、ID 取得用)。SelectedItem: 選択された行そのもの(DataRowView や List<T> の T が返る)。SelectedIndex: 行番号(int、0始まり、未選択は -1)。「ID で取りたい → SelectedValue」「行全体を扱いたい → SelectedItem」「順番だけ知りたい → SelectedIndex」で覚えると整理しやすいです。
Q3. SelectedIndexChanged が DataSource 設定時に二重発火するのはなぜ?
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. SelectedItem を DataRowView にキャストして Field<T> アクセスは安全?
A. DataSource が DataTable や DataView の時は安全です。var row = (DataRowView)combo.SelectedItem; var name = row.Row.Field<string>("name"); の形が業務SE定番。SelectedItem が null の可能性があるので null チェック or as キャストを噛ませてください。DataSource が List<T> なら直接 (T) キャストで OK です。
ここまでで WinForms ComboBox のバインディング・選択値取得・二重発火回避は押さえた。WinForms の DataSource 系の隣接トピックも貼っておきます。
関連記事
- C# DataGridView の DataSource を後から変更する全パターン — DataGridView の DataSource 切替を整える時に効く(ComboBox と同じ二重発火問題を扱う)
- C# WinForms の Form.ShowDialog と Form.Show の違いと使い分け完全ガイド — ComboBox の選択値を子フォームに渡す時のパターンに効く
- WinForms で UseWaitCursor が戻らないバグの解決法(業務SE目線) — DataSource 切替時の長時間処理で砂時計を出す時に効く
以上!
同じ罠でハマってる業務SE仲間いたら、どんどんシェア待ってるぜ!!


コメント