Автодополнение в TextBox из базы данных: ASP.NET AJAX AutoCompleteExtender

РазделASP.NET AjaxУровеньСредний Чтение8 минОпубликовано ТегиASP.NET AJAXAutoCompleteSQL ServerVB.NET

Пользователь набирает первые буквы, а под полем появляется список вариантов из базы. Собираем такое автодополнение на AJAX Control Toolkit и веб-методе страницы.

Главное. AutoCompleteExtender берёт на себя всю клиентскую механику, а от разработчика нужен лишь Shared WebMethod с правильной сигнатурой и быстрым параметризованным запросом по индексу.

Как устроено автодополнение

Механика простая: после каждого нажатия клавиши (с небольшой задержкой) браузер отправляет на сервер введённый префикс, сервер ищет подходящие значения в базе и возвращает короткий список строк, а скрипт показывает его выпадающим списком под полем. Пользователь выбирает вариант мышью или стрелками, и текст подставляется в TextBox.

В мире Web Forms всю клиентскую часть берёт на себя AutoCompleteExtender из AJAX Control Toolkit. Это расширитель: он не заменяет TextBox, а «навешивает» на существующее поле дополнительное поведение. Разработчику остаётся написать серверный метод, который по префиксу вернёт массив строк.

Метод можно разместить в двух местах: прямо в классе страницы как статический веб-метод или в отдельном веб-сервисе .asmx, если подсказки нужны на нескольких страницах. Второй вариант удобнее для повторного использования, первый проще для одиночного поля. Ниже рассмотрен метод страницы, а переход к .asmx сводится к указанию свойства ServicePath и атрибута ScriptService у класса сервиса.

Подготовка страницы

На странице должен быть ScriptManager (или ToolkitScriptManager из того же пакета, он лучше объединяет скрипты). Сборка AjaxControlToolkit.dll подключается к проекту и регистрируется директивой Register. Расширитель ссылается на поле через TargetControlID.

Ключевые свойства: ServiceMethod задаёт имя серверного метода, MinimumPrefixLength определяет, со скольких символов начинать поиск, CompletionInterval задаёт паузу в миллисекундах после последнего нажатия, CompletionSetCount ограничивает число вариантов. EnableCaching включает кэш на клиенте: повторный ввод того же префикса не пойдёт на сервер.

Search.aspxASPX
<%@ Register Assembly="AjaxControlToolkit" Namespace="AjaxControlToolkit"
    TagPrefix="ajax" %>

<ajax:ToolkitScriptManager ID="tsm" runat="server" />

<label for="txtCustomer">Клиент</label>
<asp:TextBox ID="txtCustomer" runat="server" autocomplete="off" />

<ajax:AutoCompleteExtender ID="acCustomer" runat="server"
    TargetControlID="txtCustomer"
    ServiceMethod="GetCompanies"
    MinimumPrefixLength="2"
    CompletionInterval="300"
    CompletionSetCount="15"
    EnableCaching="true"
    FirstRowSelected="true"
    CompletionListCssClass="ac-list"
    CompletionListHighlightedItemCssClass="ac-item-active" />
Атрибут autocomplete="off" отключает собственные подсказки браузера, иначе два выпадающих списка будут накладываться друг на друга.

Серверный метод с запросом к базе

Если ServicePath не указан, расширитель ищет метод прямо в классе страницы. Такой метод должен быть Shared, помечен атрибутами WebMethod и ScriptMethod и иметь строго определённую сигнатуру: параметры prefixText и count (имена важны, они передаются по имени), возвращаемое значение массив строк. Можно добавить третий параметр contextKey, если поиску нужен дополнительный контекст, например выбранный регион.

Запрос обязательно параметризуется. Префикс приходит от пользователя, и склеивать его в строку SQL значит открыть дверь для SQL-инъекции. Символы % и _ в самом префиксе стоит экранировать, иначе ввод «50%» превратится в шаблон поиска.

Search.aspx.vbVB.NET
Imports System.Web.Services
Imports System.Web.Script.Services
Imports System.Data.SqlClient

Partial Class Search
    Inherits System.Web.UI.Page

    <WebMethod(), ScriptMethod()> _
    Public Shared Function GetCompanies(prefixText As String, count As Integer) As String()
        Dim result As New List(Of String)
        ' Экранируем спецсимволы LIKE
        Dim safe = prefixText.Replace("[", "[[]").Replace("%", "[%]").Replace("_", "[_]")
        Const sql = "SELECT TOP (@Count) CompanyName FROM Customers " &
                    "WHERE CompanyName LIKE @Prefix ORDER BY CompanyName"
        Using cn As New SqlConnection(ConfigurationManager.ConnectionStrings("Main").ConnectionString),
              cmd As New SqlCommand(sql, cn)
            cmd.Parameters.Add("@Count", SqlDbType.Int).Value = Math.Min(count, 20)
            cmd.Parameters.Add("@Prefix", SqlDbType.NVarChar, 100).Value = safe & "%"
            cn.Open()
            Using rd = cmd.ExecuteReader()
                While rd.Read()
                    result.Add(rd.GetString(0))
                End While
            End Using
        End Using
        Return result.ToArray()
    End Function
End Class

Значение и подпись: выбор идентификатора

Часто пользователю нужно показать название, а серверу передать идентификатор. Для этого метод возвращает строки, собранные методом AutoCompleteExtender.CreateAutoCompleteItem(текст, значение). В списке видна только подпись, а значение доступно в клиентском событии OnClientItemSelected через eventArgs.get_value(). Его записывают в HiddenField и читают на сервере при отправке формы.

Не забудьте сценарий, когда пользователь ничего не выбрал из списка, а просто ввёл текст: скрытое поле останется пустым или со старым значением. Очищайте его в обработчике ввода и проверяйте на сервере.

Выбор значенияJavaScript
// В разметке расширителя: OnClientItemSelected="onCompanySelected"
function onCompanySelected(sender, e) {
  document.getElementById('<%= hfCustomerId.ClientID %>').value = e.get_value();
}

Производительность и удобство

Автодополнение генерирует много мелких запросов, поэтому важно, чтобы каждый из них был дешёвым. Поиск по началу строки (LIKE 'abc%') использует индекс по столбцу, а поиск по вхождению (LIKE '%abc%') вынужден просматривать всю таблицу. На больших справочниках это разница между миллисекундами и секундами.

Минимальная длина префикса в 2-3 символа отсекает самые широкие и бесполезные запросы, а интервал 250-400 мс не даёт отправлять запрос на каждую букву при быстром наборе.

  • Индекс по столбцу поиска обязателен.
  • TOP ограничивает объём ответа, даже если клиент попросит больше.
  • Кэш на сервере (Cache) для неизменных справочников снимает нагрузку с базы.
  • Стили списка задаются через CompletionListCssClass: контрастная подсветка активного пункта важна для доступности.

Как это делают сегодня

AJAX Control Toolkit давно не развивается. В современных проектах автодополнение строят на нативном элементе input с datalist для простых случаев или на компонентах вроде Choices.js и Tom Select, получая данные через fetch из Minimal API. В Blazor есть готовые компоненты Autocomplete в MudBlazor и Fluent UI. Для полнотекстового поиска по большим объёмам используют полнотекстовые индексы SQL Server или Elasticsearch.

Вопросы и ответы

Почему метод не вызывается, а в консоли ошибка 500?

Чаще всего неверна сигнатура: метод не Shared, нет атрибута ScriptMethod или параметры названы не prefixText и count.

Как передать в поиск дополнительный параметр?

Включите UseContextKey и задайте ContextKey у расширителя, а в метод добавьте третий параметр contextKey As String.

Можно ли искать не только по началу строки?

Можно, через LIKE '%текст%', но такой запрос не использует обычный индекс. Для больших таблиц лучше полнотекстовый поиск.

Читайте также