ASP.NET Webフォームプロジェクトでオートコンプリートのドロップダウンを実現する

井上です。
ASP.NET Webフォームのドロップダウンコントロールでオートコンプリートをやろうというお話です。 ちょっとと面倒だったのでまとめておきます。

環境

.NET Framework 4.5.1
ASP.NET Web Form
C#

参考

jqueryui.com

やりたいこと

オートコンプリート化したいDropDownListコントロールを含むページで、お手軽にオートコンプリート化させる

コード

オートコンプリート関数の作成(jquery.autocomplete.js)

;(function ($) {
    $.fn.autocompleteExtend = function () {
        return this.each(function() {
            $.widget("custom.combobox", {
                _create: function () {
                    this.wrapper = $("<span>")
                      .addClass("custom-combobox")
                      .insertAfter(this.element);

                    this.element.hide();
                    this._createAutocomplete();
                    this._createShowAllButton();
                },

                _createAutocomplete: function () {
                    var selected = this.element.children(":selected"),
                      value = selected.val() ? selected.text() : "";

                    this.input = $("<input>")
                      .appendTo(this.wrapper)
                      .val(value)
                      .attr("title", "")
                      .attr("onKeydown", "return InvalidInput(event)") // enterキーの挙動を打ち消す
                      .attr("style", "float:left;") // ブラウザ差異対応
                      .addClass("custom-combobox-input ui-widget ui-widget-content ui-state-default ui-corner-left autocomplete")
                      .autocomplete({
                          delay: 0,
                          minLength: 0,
                          source: $.proxy(this, "_source")
                      })
                      .tooltip({
                          tooltipClass: "ui-state-highlight"
                      });

                    this._on(this.input, {
                        autocompleteselect: function (event, ui) {
                            ui.item.option.selected = true;
                            this._trigger("select", event, {
                                item: ui.item.option
                            });
                        },

                        autocompletechange: "_removeIfInvalid"
                    });
                },

                _createShowAllButton: function () {
                    var input = this.input,
                      wasOpen = false;

                    $("<a>")
                      .attr("tabIndex", -1)
                      .attr("title", "")
                      .tooltip()
                      .appendTo(this.wrapper)
                      .button({
                          icons: {
                              primary: "ui-icon-triangle-1-s"
                          },
                          text: false
                      })
                      .removeClass("ui-corner-all")
                      .addClass("custom-combobox-toggle ui-corner-right autocomplete")
                      .mousedown(function () {
                          wasOpen = input.autocomplete("widget").is(":visible");
                      })
                      .click(function () {
                          input.focus();

                          // Close if already visible
                          if (wasOpen) {
                              return;
                          }

                          // Pass empty string as value to search for, displaying all results
                          input.autocomplete("search", "");
                      });
                },

                _source: function (request, response) {
                    var matcher = new RegExp($.ui.autocomplete.escapeRegex(request.term), "i");
                    response(this.element.children("option").map(function () {
                        var text = $(this).text();
                        if (this.value && (!request.term || matcher.test(text)))
                            return {
                                label: text,
                                value: text,
                                option: this
                            };
                    }));
                },

                _removeIfInvalid: function (event, ui) {

                    // Selected an item, nothing to do
                    if (ui.item) {
                        // 選択値でポストバックさせる
                        setTimeout('__doPostBack(\' + $(this).attr("name") + \' ,\'\')', 0);
                        return;
                    }

                    // Search for a match (case-insensitive)
                    var value = this.input.val(),
                      valueLowerCase = value.toLowerCase(),
                      valid = false;
                    this.element.children("option").each(function () {
                        if ($(this).text().toLowerCase() === valueLowerCase) {
                            this.selected = valid = true;
                            return false;
                        }
                    });

                    // Found a match, nothing to do
                    if (valid) {

                        return;
                    }

                    // Remove invalid value
                    this.input
                      .val("")
                      .attr("title", "")
                      .tooltip("open");
                    this.element.val("");
                    this._delay(function () {
                        this.input.tooltip("close").attr("title", "");
                    }, 2500);
                    this.input.autocomplete("instance").term = "";
                },

                _destroy: function () {
                    this.wrapper.remove();
                    this.element.show();
                }
            });

            // 挙動設定
            $('#' + $(this).attr("id") ).combobox();

            // ドロップダウンのスクロール指定
            $('.ui-autocomplete').css('overflow-y', 'auto');
            $('.ui-autocomplete').css('overflow-x', 'hidden');
            $('.ui-autocomplete').css('height', '200px');
        });
    }
})(jQuery);

$(function () {
    $(".autocomplete").autocompleteExtend();
});

参考先のコードを元に作成。
入力値/選択値がリスト内の要素と完全一致するときのみポストバックさせます。
また動的に生成されるドロップダウンのスクロール指定等を行っています。
最後の処理はautocompleteクラスを持つ要素に対してautocompleteExtendを呼び出すためのものです。

呼び出し元コード(sample.aspx)

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

<!DOCTYPE html>

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
    <title></title>
    <script src="Scripts/jquery-1.12.4.min.js"></script>
    <script src="Scripts/jquery-ui-1.12.0.min.js"></script>
    <script src="Scripts/jquery.autocomplete.js"></script>
    <link rel="stylesheet" href="Content/themes/base/jquery-ui.min.css" />
    <style>
    .custom-combobox {
     position: relative;
     display: inline-block;
    }
    .custom-combobox-toggle {
     position: absolute;
     top: 0;
     bottom: 0;
        margin-left: -1px;
     padding: 0;
    }
    .custom-combobox-input {
     margin: 0;
     padding: 5px 10px;
    }
    </style>
</head>
<body>
    <form id="form1" runat="server">
    <div>
        <asp:DropDownList ID="HogeHogeList" runat="server" OnSelectedIndexChanged="HogeHogeList_SelectedIndexChanged" AutoPostBack="true" CssClass="autocomplete"></asp:DropDownList>   
    </div>
    </form>
</body>
</html>

DropDownListコントロールautocompleteクラスを指定しているのみです。

呼び出し元コード(sample.aspx.cs)

using System;
using System.Collections.Generic;

namespace WebApplication1
{
    public partial class sample : System.Web.UI.Page
    {
        protected void Page_Load(object sender, EventArgs e)
        {
            if (!IsPostBack)
            {
                var list = new Dictionary<string, string> { 
                { "", "0" }, 
                { "Aゴルフ場", "1" }, 
                { "Bカントリー倶楽部", "2" },
                { "Cゴルフコース", "3" },
                { "Dカントリークラブ", "4" },
                { "Eゴルフリゾート", "5" } 
            };
                this.HogeHogeList.DataSource = list;
                this.HogeHogeList.DataTextField = "Key";
                this.HogeHogeList.DataValueField = "Value";
                this.HogeHogeList.DataBind();
            }
        }

        protected void HogeHogeList_SelectedIndexChanged(object sender, EventArgs e)
        {
            // 選択時の処理
        }
    }
}

実行すると

f:id:ihisa:20160721154007p:plain

入力すると一致する候補がきっちり表示されます。

f:id:ihisa:20160721153935p:plain

リストから選択すればSelectedIndexChangedイベントが発生します。