ASP.NET WebFormsで氏名+番号を部分一致検索できるドロップダウンの実装手順(Select2/Bootstrap‑select対応)

「Alfred K – AXN29109」のように“氏名+番号”を一行で表示する ASP.NET WebForms の <asp:DropDownList> を、名前でも番号でも部分一致で絞り込める検索ドロップダウンに作り替える方法を、実運用で詰まりがちなポイント(ポストバック、UpdatePanel、Bootstrap 併用、外観カスタム、各 <option> のクラス付与)までまとめて解説します。Select2 と Bootstrap‑select の両対応コード、コピペで動く最小構成、スケールさせる Ajax 版まで網羅します。

目次

ASP.NET WebForms で「氏名+番号」を検索できるドロップダウンにする全体像

本記事のゴールは、次の要件を満たすことです。

  • 表示は 氏名 - 番号 を1行で結合(例:Alfred K - AXN29109)。
  • 入力したキーワードでドロップダウンの候補を即時フィルタ(名前でも番号でも部分一致)。
  • ポストバック(AutoPostBack やボタン)後も検索 UI が壊れない。
  • Bootstrap の CSS と併用しても崩れない。外観(幅・高さ・フォント・枠線・「No result found」色)を柔軟に制御。
  • コードビハインドでのデータバインドでも各 <option> に独自クラスや検索トークンを付与可能。

採用アプローチの比較(結論:Select2 または Bootstrap‑select)

課題推奨アプローチポイント
検索可能ドロップダウンjQuery Select2 または Bootstrap‑select
・Bootstrap‑select は data-live-search="true" でライブサーチ有効化
・Select2 は $(el).select2() で即検索対応
Select2 はアクセシビリティ・多言語・拡張性が高く、Bootstrap‑select は Bootstrap との一体感が強い。
ポストバックで検索状態が消える① 初回のみ DataBind: if(!IsPostBack){ Bind(); }
② ポストバック後にプラグイン再初期化(RegisterStartupScript / Sys.Application.add_load)
UpdatePanel 内でも endRequest / add_load で再初期化する。
前方一致しかヒットしないBootstrap‑select の data-live-search-style="contains" を明示。Select2 は既定で全文一致(大文字小文字無視)。ハイフン・スペースを無視して探すなら Select2 の matcher をカスタム。
外観のカスタマイズ幅:data-width or CSS/高さ:.dropdown-menu.inner{max-height:...}
ボタン文字:.bootstrap-select .btn{font-size:...}
「No result found」色:.no-results{...}
CSS は Bootstrap の後に書いて上書き。影響範囲を .bootstrap-select 直下に限定。
<option>にクラスやトークン付与例:item.Attributes["class"]="option vip"
例:item.Attributes["data-tokens"]="alfred axn29109"
DataBind の直後に付与。Bootstrap‑select は data-tokens を検索に使える。

最小構成:Bootstrap‑select で「氏名+番号」検索を実装

まずはコピペで動く最小構成です。Bootstrap(3/4/5 いずれも可)と jQuery、Bootstrap‑select を読み込み、DropDownList にクラスと data-* 属性を付けます。

ASPX(マークアップ)

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Customer.aspx.cs" Inherits="Demo.Customer" %>







 

C#(コードビハインド:ページロード/データバインド)

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Web.UI.WebControls;

namespace Demo
{
public partial class Customer : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
if (!IsPostBack)
{
var customers = SampleData();
cbCustomer.DataSource = customers;
cbCustomer.DataBind();


            // DataBind 後に各 option へクラスや検索トークンを付与
            foreach (ListItem item in cbCustomer.Items)
            {
                // 表示テキストは「氏名 - 番号」
                // 検索用の追加トークン(Bootstrap‑select 用)
                var tokens = NormalizeTokens(item.Text);
                item.Attributes["data-tokens"] = tokens; // 例: "alfred k axn29109 alfredk"

                // 任意の独自クラスを付与(例:VIP 顧客は金色バッジ)
                if (item.Text.StartsWith("Alfred K"))
                    item.Attributes["class"] = "option vip";
                else
                    item.Attributes["class"] = "option option-muted";
            }
        }

        // ポストバックや UpdatePanel 後の再初期化をクライアントスクリプトへ通知
        var js = "$('.selectpicker').selectpicker('refresh');";
        ScriptManager.RegisterStartupScript(this, GetType(), "refreshSelect", js, true);
    }

    protected void btnSubmit_Click(object sender, EventArgs e)
    {
        // 選択値の取得例:cbCustomer.SelectedValue / SelectedItem.Text
        // 実処理はここに。
    }

    private static string NormalizeTokens(string text)
    {
        // ハイフン・スペースを除去し、大文字小文字を無視(検索強化)
        var t = text.Replace("-", " ").Replace("–", " ").Replace("—", " ")
                    .Replace(" ", " ").Trim();
        var simple = new string(t.Where(c => !char.IsWhiteSpace(c)).ToArray());
        return string.Join(" ",
            t.ToLowerInvariant(),
            simple.ToLowerInvariant());
    }

    private static List<CustomerDto> SampleData()
    {
        return new List<CustomerDto>
        {
            new CustomerDto{ Id="1", Name="Alfred K", Number="AXN29109" },
            new CustomerDto{ Id="2", Name="Alfred M", Number="KX20221"  },
            new CustomerDto{ Id="3", Name="Karen X",  Number="KX-99210" },
            new CustomerDto{ Id="4", Name="Brian T",  Number="B-102"    },
            new CustomerDto{ Id="5", Name="Kana O",   Number="AXN-77"   },
        }
        .Select(x => { x.Display = x.Name + " - " + x.Number; return x; })
        .ToList();
    }

    public class CustomerDto
    {
        public string Id { get; set; }
        public string Name { get; set; }
        public string Number { get; set; }
        public string Display { get; set; } // 「氏名 - 番号」
    }
}


} 

ポイント:この構成で Alfred と入力すれば 2件、KX と入力すれば「KX20221」「KX-99210」などが 部分一致でヒットします。ハイフンの有無や空白を問わず拾えるように data-tokens に正規化したトークンを追加しているため、ユーザーのタイピング癖に強い検索体験になります。

検索精度をさらに上げるテクニック(Bootstrap‑select)

  • 前方一致を回避: data-live-search-style="contains" を必ず指定。既定が前方一致のバージョンでは begins になるため要注意。
  • 隠しキーワード: 各 <option> に data-tokens="姓 名 かな 英字 番号" を付けると、その語も検索対象になります。
  • 正規化: 上の C# の NormalizeTokens() のように、ハイフン・スペース除去/小文字化 した語を追加すると、KX99210 でも KX-99210 でもヒット。
  • 見た目の強調: CSS で .no-results を装飾して行き止まり感を減らす。プレースホルダは title="氏名または番号で検索"。

ポストバック・UpdatePanel で壊れないための再初期化

WebForms はポストバックで DOM 再生成がおきます。プラグインは「初期化済みの DOM」を前提としているため、部分更新後の再初期化が必須です。

// C#:毎回クライアントに refresh を出す(重複しても安全)
ScriptManager.RegisterStartupScript(this, GetType(), "refreshSelect",
    "$('.selectpicker').selectpicker('refresh');", true);

// JS:UpdatePanel の endRequest / Application.load で再初期化
if (typeof (Sys) !== 'undefined' && Sys.WebForms && Sys.WebForms.PageRequestManager) {
var prm = Sys.WebForms.PageRequestManager.getInstance();
prm.add_endRequest(function () { $('.selectpicker').selectpicker('refresh'); });
Sys.Application.add_load(function () { $('.selectpicker').selectpicker('refresh'); });
} 

UpdatePanel を使わないページでも問題ありません(Sys 未定義を判定しているため)。

外観カスタム:幅・高さ・フォント・枠線・「No result found」色

対象やり方例
幅data-width または CSSdata-width="350px" / .bootstrap-select{width:350px;}
ボタン高さ・枠線トグルボタンを装飾.bootstrap-select > .dropdown-toggle{height:42px;border:1px solid #198754;}
リスト高さ内部コンテナの最大高さ.bootstrap-select .dropdown-menu .inner{max-height:240px;}
各行の行高アンカーの line-height.bootstrap-select .dropdown-menu li a{line-height:28px;}
「No result」背景色・文字色.bootstrap-select .no-results{background:#f8d7da;color:#842029;}
各項目のフォント<option class="..."> + CSS.option-danger{color:#dc3545} など

各 <option> に独自クラスやバッジを付ける

データバインド後に属性を付与できます。CSS と組み合わせると、VIP 顧客を目立たせるなどの UI が簡単です。

// バインド直後
foreach (ListItem item in cbCustomer.Items)
{
    if (item.Text.Contains("Alfred"))
        item.Attributes["class"] = "option vip"; // CSS で装飾
    else
        item.Attributes["class"] = "option";
}

Bootstrap‑select は data-content を使って HTML リッチ表示も可能です(太字やバッジ):

// 例: &lt;option data-content="Alfred K - AXN29109 &lt;span class='vip-badge'&gt;VIP&lt;/span&gt;"&gt;
item.Attributes["data-content"] = $"{item.Text} &lt;span class='vip-badge'&gt;VIP&lt;/span&gt;";

Select2 版:matcher で「ハイフン無視・部分一致」をより強力に

Select2 は検索の核となる matcher を差し替えられます。ハイフンやスペース、全角・半角差を無視する独自マッチャを用意すると、ユーザーは「KX99210」「kx‑99210」「KX 99210」どれでもヒットさせられます。

ASPX(Select2 の追加読み込み)

&lt;link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/select2/4.1.0-rc.0/css/select2.min.css" /&gt;
&lt;script src="https://cdnjs.cloudflare.com/ajax/libs/select2/4.1.0-rc.0/js/select2.min.js"&gt;&lt;/script&gt;

JS(Select2 初期化と matcher)

<script>
(function(){
  var $ddl = $('#<%= cbCustomer.ClientID %>');

function normalize(s){
return (s || '')
.toString()
.normalize('NFKC')               // 全半角差を正規化
.toLowerCase()
.replace(/[\s-‐‑‒–—―]+/g, '');   // 空白・各種ハイフン類を除去
}

function customMatcher(params, data) {
if ($.trim(params.term) === '') { return data; }
var term = normalize(params.term);
var text = normalize(data.text);
// data.element の data-tokens も検索対象に
var tokens = normalize($(data.element).attr('data-tokens'));
if (text.indexOf(term) > -1 || tokens.indexOf(term) > -1) {
return data;
}
return null;
}

function init(){
$ddl.select2({
width: 'resolve',
matcher: customMatcher,
language: {
noResults: function(){ return '一致する候補がありません'; },
searching: function(){ return '検索中...'; }
}
});
}

$(init);
if (typeof(Sys) !== 'undefined' && Sys.Application){
Sys.Application.add_load(function(){ $ddl.select2('destroy'); init(); });
}
})();
 

Bootstrap を使っていても Select2 は独立して動作します。width:'resolve' で親コンテナの幅に馴染ませられます。

大規模データ(数万件)へスケール:Ajax ロード(Select2)

候補が数万件ある場合は、全部を <option> に流し込まず、サーバー側で部分検索してページング返却するのが定石です。Select2 は ajax 設定で自然に実装できます。

ASPX:Select2 の Ajax 初期化

&lt;script&gt;
(function(){
  var $ddl = $('#&lt;%= cbCustomer.ClientID %&gt;');
  $ddl.select2({
    width: 'resolve',
    minimumInputLength: 1,
    ajax: {
      url: '&lt;%= ResolveUrl("~/Customer.aspx/SearchCustomers") %&gt;',
      dataType: 'json',
      delay: 200,
      type: 'POST',
      contentType: 'application/json; charset=utf-8',
      data: function(params){ return JSON.stringify({ term: params.term, page: params.page || 1 }); },
      processResults: function (data) {
        // ASMX/PageMethod の JSON 返却に合わせて整形
        var items = data.d || data;
        return { results: items, pagination: { more: items.length === 20 } };
      },
      cache: true
    },
    templateResult: function(item){ return item.text; },
    templateSelection: function(item){ return item.text; },
    language: { inputTooShort: function(){ return '1文字以上入力してください'; } }
  });
})();
&lt;/script&gt;

C#:PageMethod(ASPX コードビハインド)

using System.Web.Services;
using System.Web.Script.Services;

[WebMethod]
[ScriptMethod(ResponseFormat = ResponseFormat.Json)]
public static List SearchCustomers(string term, int page)
{
term = (term ?? string.Empty).Trim().ToLowerInvariant();
var pageSize = 20;
var skip = (page - 1) * pageSize;


// 実際は DB で氏名/番号の複合インデックス検索を推奨
var all = SampleData(); // 先のメソッドを再利用(実案件では DB)
var query = all.Where(x =>
    (x.Name ?? "").ToLowerInvariant().Contains(term) ||
    (x.Number ?? "").ToLowerInvariant().Replace("-", "").Contains(term.Replace("-", ""))
);

var result = query
    .Skip(skip).Take(pageSize)
    .Select(x => new Select2Item{ id = x.Id, text = x.Display })
    .ToList();

return result;


}

public class Select2Item { public string id { get; set; } public string text { get; set; } } 

この方式なら、初回描画は空の <select> で構いません。ユーザーがタイプするたびにサーバーで検索し、必要な分だけ返すため、レンダリングもネットワークも軽く保てます。

ASP.NET WebForms ならではの実務ノウハウ

  • DataBind は初回のみ: if(!IsPostBack) でバインド。毎回バインドすると選択状態が消えます。
  • ClientID の取り扱い: スクリプト内で <%= cbCustomer.ClientID %> を使うか、ClientIDMode="Static" を指定して ID を固定。
  • jQuery の競合回避: MS Ajax と競合する場合は jQuery.noConflict() を使い、var $jq = jQuery.noConflict(); のように別名で呼び出します。
  • 検証コントロールとの連携: RequiredFieldValidator を併用する場合、AppendDataBoundItems+先頭に空の <asp:ListItem> を入れると未選択を検知しやすい。
  • 選択直後に自動送信: AutoPostBack="true" の場合、プラグイン UI とポストバックのタイミングが競合することがあるため、セレクタに change ハンドラを付けて __doPostBack を明示的に呼ぶと安定します。

「氏名でも番号でも部分一致」を確実にするテスト観点

  • 名字だけ/名前だけ/イニシャルだけ(例:Alf、AK)でヒットするか。
  • 番号のハイフン有無(AXN29109 と AXN-29109)で同等にヒットするか。
  • 小文字/大文字/全角英数が混在してもヒットするか。
  • UpdatePanel 内のドロップダウンを動的に差し替えた後でも検索できるか(refresh 済みか)。
  • Bootstrap 由来の .form-control と競合していないか(高さ・余白が崩れていないか)。

完成版:要件を満たすフルサンプル(Bootstrap‑select 版)

要件を一通り満たす完成コードをまとめます。カスタムトークン、ポストバック対策、外観調整を含みます。

ASPX

<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.2/css/bootstrap.min.css" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-select/1.14.0-beta3/css/bootstrap-select.min.css" />
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-select/1.14.0-beta3/js/bootstrap-select.min.js"></script>




顧客選択







 

C#(コードビハインド)

protected void Page_Load(object sender, EventArgs e)
{
    if (!IsPostBack)
    {
        var customers = SampleData();
        cbCustomer.DataSource = customers;
        cbCustomer.DataBind();


    foreach (ListItem item in cbCustomer.Items)
    {
        var tokens = NormalizeTokens(item.Text);
        item.Attributes["data-tokens"] = tokens;

        if (item.Text.StartsWith("Alfred K"))
            item.Attributes["class"] = "option vip";
        else if (item.Text.Contains("B-"))
            item.Attributes["class"] = "option option-danger";
        else
            item.Attributes["class"] = "option option-muted";
    }
}

// ポストバックごとに refresh を指示(新規項目追加時にも対応)
ScriptManager.RegisterStartupScript(this, GetType(), "refreshSelect",
    "$('.selectpicker').selectpicker('refresh');", true);


} 

トラブルシュート(よくあるハマり)

  • 検索が前方一致しか効かない: data-live-search-style="contains" を入れ忘れていませんか?古いサンプルでは begins が指定されていることがあります。
  • ポストバック後に検索ボックスが消える: UpdatePanel の endRequest と Application.load の両方で refresh しているか確認。
  • 選択値が毎回リセットされる: IsPostBack を忘れて毎回 DataBind() していないか検査。ViewState を無効化している場合も見直し。
  • 複数ドロップダウンで片方しか動かない: セレクターが ID 固定で単一要素しか初期化していない可能性。$('.selectpicker') のクラスで初期化する。
  • バリデータが効かない: 先頭に空項目を追加し、AppendDataBoundItems="true" を指定して未選択を検知する。

要件別の設計判断早見表

要件おすすめ理由
Bootstrap サイトとの統一感重視Bootstrap‑select既存の .btn 系デザインにシームレス
多言語/アクセシビリティ/柔軟な検索ロジックSelect2matcher 等、拡張性が高い
候補数が数万件Select2 + Ajax仮想化・遅延ロードで軽量
各項目にバッジや色分けBootstrap‑select(data-content)HTML リッチ表示が簡単

チェックリスト(導入前に確認)

  • IsPostBack 条件付きの DataBind になっている。
  • プラグイン再初期化(refresh)のスクリプトが登録されている。
  • data-live-search と data-live-search-style="contains" を付けた。
  • 検索強化の data-tokens(名前・番号・正規化文字列)を仕込んだ。
  • 外観の最小限カスタム(width/height/no-results)を入れ、Bootstrap の上書き順も確認した。

まとめ

ASP.NET WebForms の <asp:DropDownList> を「氏名+番号」で検索できるドロップダウンへ置き換えるなら、Bootstrap‑select か Select2 を使うのが最短です。ポイントは以下の 4 つです。

  1. 「初回のみ DataBind」+「ポストバック後の再初期化」で安定動作。
  2. 部分一致は Bootstrap‑select なら data-live-search-style="contains"、Select2 なら matcher カスタム。
  3. 検索強化のトークン(data-tokens)や正規化で、名前でも番号でも確実にヒット。
  4. 外観は CSS で自在に(幅・高さ・フォント・枠線・「No result」色)。

これらを適用すれば、Alfred でも KX でも、ハイフンの有無を問わず候補が見つかり、ポストバックや UpdatePanel でも壊れない、使い勝手の良い検索ドロップダウンが完成します。WebForms の既存資産を活かしながらモダンな UX を加える、現実解のベストプラクティスとしてぜひご活用ください。

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次