Каскадные выпадающие списки в ASP.NET на CascadingDropDown из AJAX Control Toolkit

РазделJavaScript и CSSУровеньСредний Чтение9 минОпубликовано ТегиASP.NET AJAXDropDownListVB.NETВеб-сервисы

Категория, затем товар; страна, затем город. Связанные списки без полной перезагрузки страницы в Web Forms проще всего собрать на CascadingDropDown и лёгком веб-сервисе ASMX.

Главное. CascadingDropDown превращает связку списков в лёгкие JSON-вызовы к веб-сервису. Главное: атрибут ScriptService, точная сигнатура методов, параметризованный SQL и проверка выбранных значений на сервере.

Почему не AutoPostBack

Классический способ связать два DropDownList в Web Forms: включить AutoPostBack у первого списка и в событии SelectedIndexChanged заполнять второй. Работает, но каждый выбор отправляет на сервер всю форму вместе с ViewState, перерисовывает страницу и сбрасывает прокрутку. На форме с десятком полей это заметно тормозит.

Расширитель CascadingDropDown из AJAX Control Toolkit решает задачу иначе. Он вызывает метод веб-сервиса из браузера, получает только пары «текст и значение» в формате JSON и заполняет список на клиенте. Объём трафика минимальный, страница не перезагружается, а логика выборки остаётся на сервере на VB.NET. Для справочников малого и среднего размера (сотни, в пределе несколько тысяч элементов) это быстрый и надёжный вариант.

Подготовка проекта

AJAX Control Toolkit поставляется отдельно от Visual Studio. После загрузки библиотеку AjaxControlToolkit.dll добавляют на панель элементов: правый щелчок по Toolbox, пункт «Choose Items», кнопка обзора. На странице обязательно должен быть ScriptManager, он подключает клиентские скрипты ASP.NET AJAX.

Для примера используем базу Northwind: первый список показывает категории, второй товары выбранной категории. Каждый DropDownList получает свой расширитель CascadingDropDown, а связь между ними задаётся свойством ParentControlID.

  • TargetControlID: какой DropDownList заполняет расширитель;
  • ServicePath и ServiceMethod: адрес веб-сервиса и имя метода;
  • Category: ключ, под которым выбранное значение передаётся дочерним спискам;
  • ParentControlID: список, от выбора в котором зависит текущий;
  • PromptText и LoadingText: подсказка в пустом списке и текст во время загрузки.

Разметка страницы

Сами DropDownList остаются обычными серверными элементами, просто без AutoPostBack и без источника данных. Всё заполнение берёт на себя расширитель. Второй список до выбора категории будет неактивен: CascadingDropDown выключает его автоматически, пока родитель пуст.

Default.aspxASPX
<%@ Register Assembly="AjaxControlToolkit" Namespace="AjaxControlToolkit" TagPrefix="cc1" %>

<asp:ScriptManager ID="ScriptManager1" runat="server" />

<asp:DropDownList ID="ddlCategory" runat="server" />
<cc1:CascadingDropDown ID="cddCategory" runat="server"
    TargetControlID="ddlCategory"
    ServicePath="NorthwindDataService.asmx" ServiceMethod="GetCategories"
    Category="CategoryId" PromptText="Выберите категорию" />

<asp:DropDownList ID="ddlProduct" runat="server" />
<cc1:CascadingDropDown ID="cddProduct" runat="server"
    TargetControlID="ddlProduct" ParentControlID="ddlCategory"
    ServicePath="NorthwindDataService.asmx" ServiceMethod="GetProducts"
    Category="ProductId" PromptText="Выберите товар"
    LoadingText="Загрузка..." />

<asp:Button ID="btnSave" runat="server" Text="Сохранить" />

Веб-сервис: первый уровень

Методы для CascadingDropDown обязаны иметь строго определённую сигнатуру: два строковых параметра с именами knownCategoryValues и category и возвращаемый массив CascadingDropDownNameValue. Имена параметров менять нельзя, клиентский скрипт передаёт аргументы именно под ними.

Класс сервиса должен быть помечен атрибутом ScriptService. Шаблон Visual Studio добавляет его закомментированным, и забыть убрать апостроф очень легко. Подключение к базе открываем внутри Using, чтобы оно гарантированно вернулось в пул даже при ошибке.

App_Code/NorthwindDataService.vbVB.NET
Imports System.Configuration
Imports System.Data
Imports System.Data.SqlClient
Imports System.Web.Services
Imports AjaxControlToolkit

<WebService(Namespace:="http://tempuri.org/")> _
<System.Web.Script.Services.ScriptService()> _
Public Class NorthwindDataService
    Inherits WebService

    Private Shared ReadOnly ConnStr As String = _
        ConfigurationManager.ConnectionStrings("Northwind").ConnectionString

    <WebMethod()> _
    Public Function GetCategories(ByVal knownCategoryValues As String, _
            ByVal category As String) As CascadingDropDownNameValue()
        Dim result As New List(Of CascadingDropDownNameValue)
        Using cn As New SqlConnection(ConnStr), _
              cmd As New SqlCommand( _
                "SELECT CategoryID, CategoryName FROM Categories ORDER BY CategoryName", cn)
            cn.Open()
            Using r As SqlDataReader = cmd.ExecuteReader()
                While r.Read()
                    result.Add(New CascadingDropDownNameValue( _
                        r.GetString(1), r.GetInt32(0).ToString()))
                End While
            End Using
        End Using
        Return result.ToArray()
    End Function
End Class

Второй уровень: фильтр по выбранному значению

Дочерний метод получает в knownCategoryValues строку вида «CategoryId:3;». Разобрать её помогает статический метод CascadingDropDown.ParseKnownCategoryValuesString, который возвращает словарь «ключ Category родителя, выбранное значение».

Значение пришло из браузера, значит, ему нельзя доверять. Подставлять его в текст запроса склейкой строк нельзя: это прямой путь к SQL-инъекции. Проверяем, что это число, и передаём через параметр SqlCommand.

App_Code/NorthwindDataService.vbVB.NET
<WebMethod()> _
Public Function GetProducts(ByVal knownCategoryValues As String, _
        ByVal category As String) As CascadingDropDownNameValue()
    Dim known = CascadingDropDown.ParseKnownCategoryValuesString(knownCategoryValues)
    Dim categoryId As Integer
    If Not known.ContainsKey("CategoryId") OrElse _
       Not Integer.TryParse(known("CategoryId"), categoryId) Then
        Return New CascadingDropDownNameValue() {} ' нечего показывать
    End If

    Dim result As New List(Of CascadingDropDownNameValue)
    Using cn As New SqlConnection(ConnStr), _
          cmd As New SqlCommand("SELECT ProductID, ProductName FROM Products " & _
              "WHERE CategoryID = @id ORDER BY ProductName", cn)
        cmd.Parameters.Add("@id", SqlDbType.Int).Value = categoryId
        cn.Open()
        Using r As SqlDataReader = cmd.ExecuteReader()
            While r.Read()
                result.Add(New CascadingDropDownNameValue( _
                    r.GetString(1), r.GetInt32(0).ToString()))
            End While
        End Using
    End Using
    Return result.ToArray()
End Function

Типичные ошибки и как их найти

Если вместо списка появляется «[Method error 500]» или «[Method error 12031]», клиент не смог вызвать сервис. Самая частая причина: отсутствует атрибут ScriptService, и сервис не отвечает на JSON-запросы. Проверьте также ServicePath относительно страницы и откройте .asmx в браузере: он должен показать список методов.

Вторая классическая проблема проявляется на обратной отправке формы. ASP.NET проверяет, что выбранное значение DropDownList было в списке, отрисованном сервером, а элементы, добавленные скриптом, он не знает. Результат: исключение «Invalid postback or callback argument». Отключать EnableEventValidation для всей страницы не нужно. Читайте выбранное значение через свойство SelectedValue расширителя, а если ошибка всё же возникает, отключите проверку только на конкретной странице, где каскад заполняется скриптом, и обязательно проверяйте значения на сервере сами.

  • имена параметров методов строго knownCategoryValues и category;
  • ключ в ParseKnownCategoryValuesString совпадает со свойством Category родительского расширителя;
  • для отладки удобно временно вызвать метод из браузера через Fiddler или инструменты разработчика и посмотреть ответ.
Подключение, созданное в свойстве и не закрытое явно, быстро исчерпывает пул соединений под нагрузкой. Держите SqlConnection только внутри Using.

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

AJAX Control Toolkit и ASMX-сервисы сегодня поддерживаются только ради legacy. Каскадные списки делают через fetch к JSON-эндпоинту (minimal API в ASP.NET Core) и пару обработчиков change на клиенте, а в Blazor это обычная привязка второго InputSelect к выбранному значению первого. Для длинных справочников используют поиск с подгрузкой вместо огромного select.

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

Почему второй список всегда пустой?

Проверьте, что ключ, который вы ищете в словаре ParseKnownCategoryValuesString, совпадает со свойством Category родительского расширителя. Если ключи расходятся, метод не находит значение и возвращает пустой массив.

Как получить выбранное значение на сервере?

После обратной отправки используйте свойство SelectedValue расширителя CascadingDropDown или SelectedValue самого DropDownList. Значение всё равно проверяйте: оно пришло от клиента.

Сколько уровней можно связать?

Сколько угодно: каждый следующий расширитель указывает предыдущий список в ParentControlID и получает в knownCategoryValues значения всех родителей сразу.

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