
Каскадные выпадающие списки в ASP.NET на CascadingDropDown из AJAX Control Toolkit
Категория, затем товар; страна, затем город. Связанные списки без полной перезагрузки страницы в Web Forms проще всего собрать на CascadingDropDown и лёгком веб-сервисе ASMX.
Почему не 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 выключает его автоматически, пока родитель пуст.
<%@ 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, чтобы оно гарантированно вернулось в пул даже при ошибке.
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.
<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 или инструменты разработчика и посмотреть ответ.
Как это делают сегодня
Вопросы и ответы
Почему второй список всегда пустой?
Проверьте, что ключ, который вы ищете в словаре ParseKnownCategoryValuesString, совпадает со свойством Category родительского расширителя. Если ключи расходятся, метод не находит значение и возвращает пустой массив.
Как получить выбранное значение на сервере?
После обратной отправки используйте свойство SelectedValue расширителя CascadingDropDown или SelectedValue самого DropDownList. Значение всё равно проверяйте: оно пришло от клиента.
Сколько уровней можно связать?
Сколько угодно: каждый следующий расширитель указывает предыдущий список в ParentControlID и получает в knownCategoryValues значения всех родителей сразу.