в

Kazan Dev Alliance

Казанское Сообщество Разработчиков Программного Обеспечения

Персональный блог Александра Шера

Professional blog for .Net programmers. Main topics: Microsoft ASP.NET AJAX, WPF/E, OOP, Refactoring

Разбор CascadingDropDown

От обиды, что на проекте отклонили идею использования датаконтекстов, этнузиазм кончился и появилось-таки время написать про CascadingDropDown, и на его примере ответить на несколько вопросов, являющихся общими для многих контролов из AjaxControlToolkit.

Для тех, кто не в курсе. CascadingDropDown - это контрол-экстендер, предназначенный для заполнения данными одного DropDownList'а в зависимости от значения, выбранного в другом DropDownList'е. Необходимые данные он подтягивает через ajax с помощью метода веб-сервиса. Это если вкратце. Теперь рассмотрим его устройство более детально.

Итак, CascadingDropDown является экстендером для DropDownList, причем в качестве target задается "зависимый" DropDownList (т.е. тот, который будет заполнятся). Для задания ID родительского контрола используется свойство ParentControlID, которое кстати является необязательным (т.е. родительского DropDownList'а может и не быть). А вот к обязательным свойствам (в довесок к TargetControlID) относятся Category и ServiceMethod. ServiceMethod содержит имя используемого CascadingDropDown веб-метода, а Category выполняет роль имени фильтра, и будет передана в веб-метод в качестве одного из параметров. К необязательным относятся PromptText (текст, который будет предлагать пользователю выбрать одно из значений), LoadingText (текст, отображаемый во время загрузки), ServicePath (путь к веб-сервису; если его не задать, то будет использоваться PageMethod старницы), ContextKey (метсто для хранения некоторой дополнительной информации), UseContextKey (сообщает, передавать или нет ContextKey в качестве одного из параметров в веб-метод) и SelectedValue (дефолтное значение для дочернего DropDownList). Кроме вышеописанных свойств, у CascadingDropDown есть ещё пара вспомогательных статических методов, которые могут быть использованы в веб-методе, но о них попозже. Все, что делает CascadingDropDown на сервере, это собирает значения вышеописанных свойств в скрипт-дескрипторы и отправляет связанному с ним клиентскому классу – наследнику Sys.UI.Behavior (а если точнее - то AjaxControlToolkit.BehaviorBase). Весь основной процесс происходит у пользователя на клиенте. Плавно туда перемещаемся:

В методе initialize происходит подписка на событие change дочернего и родительского (если таковой задан) select’ов. Также, если задан родительский select, то между ним и дочерним устанавливается связь: дочернему добавляется поле CascadingDropDownParentControlID (ссылающееся на родителя), а у родителя создается (если ещё не создан) поле-массив childDropDown, в который добавляется ссылка на дочерний select. Кроме всего этого выполняется ещё ряд действий: дочерний select очищается от имеющихся данных, ему задается поле CascadingDropDownCategory, которое будет содержать значение свойства Category, подставляется текст из PromptText или LoadingText и ещё кое-какие для нас малозаметные действия. Также, если у дочернего select’а нет родительского, либо же родительский есть и в момент вызова initialize в нем выбрано некоторое значение, дочерний select будет заполнен данными (т.е. произойдет то же самое, что и при смене значения родительского select’а). Самое время посмотреть, как оно (заполнение данными) происходит.

Начинается все в методе _onParentChange. Как уже говорилось, в веб-метод передается либо два, либо три параметра, в зависимости от значения UseContextKey. Эти параметры – knownCategoryValues, category и contextKey. Все они строковые. В качестве последних двух передаются значения из одноименных полей класса. С первым чуть интереснее. Его значение формируется так: если у дочернего select’а нет родительского, то значение пустое. Если же родительский select задан, то у него считывается CascadingDropDownCategory и выбранное значение (value), и эта пара добавляется к knownCategoryValues. Если у родительского select задано значение поля CascadingDropDownParentControlID,  то берется «родитель» родительского select’а и вышеописанные действия повторяются, и так далее до конца цепочки. Новая пара добавляется к началу knownCategoryValues, и в результате, получается вот такая строка:

'category1:value1;category2:value2;...'.

Здесь хочу обратить внимание на одну не очень очевидную вещь. Для успешной работы требуется, чтобы у родительского select’а было задано поле CascadingDropDownCategory. Если он также имеет связанный с ним CascadingDropDownBehavior, то это поле будет задано автоматически. В противном случае Вам придется задать значение этого поля явно.

Но вернемся к веб-методу. Вызывается он не через сгенерированные прокси, а напрямую – через метод Sys.Net.WebServiceProxy.invoke. После сбора всех параметров для веб-метода выбрасывается клиентское событие populating(Object, Sys.CancelEventArgs), и если в обработчике свойство Sys.CancelEventArgs.cancel было выставлено в true, запрос отправляется на сервер. Веб-метод должен вернуть массив объектов типа CascadingDropDownNameValue:

Если в процессе обработки запроса произошла ошибка, то по совершенно непонятной мне причине, информация об ошибке будет засунута в дочерний select. Если же все прошло без ошибок, то дочерний select будет заполнен пришедшими данными, и если среди одного из объектов value равно SelectedValue или isDefaultValue == true, то это будет выбрано в select’е. Независимо от того, были ошибки или нет, в конце будет выброшено клиентское событие populated(Object, Sys.EventArgs). Все, процесс обновления закончен.

Стоит отметить, что при любом изменении выбранного значения у дочернего select’а будет выброшено клиентское событие SelectionChanged(Object, AjaxControltoolkit.CascadingDropDownSelectionChangedEventArgs):

Ну и немного о грустном. На момент написания статьи существовал баг. LoadingText выставляется до того, как выбрасывается событие populating, и если запрос был отменен, LoadingText остается введенным в дочерний select вместо PromptText, что может ввести пользователя в недоумение.

Теперь, когда мы более-менее разобрались с принципом работы , можно сделать некоторые выводы.

  • Поскольку CascadingDropDown работает через веб-метод (метод веб-сервиса или PageMethod), он никак не связан с жизненным циклом страницы, как на клиенте, так и на сервере. И следовательно, при обновлении select’а  с помошью CascadingDropDown не будет постбэка, не сработают валидаторы, не вызовятся события жизненного цикла, не сработает SelectedIndexChanged и т.д. Кроме того, для работы CascadingDropDown не нужно помещать его в UpdatePanel - он ей все равно не воспользуется.
  • Поскольку CascadingDropDown работает через веб-метод, принципиальным в сигнатуре метода является кроме типа параметров не их порядок, а их имена. Т.е. имена параметров должны быть именно такие, какие указаны в хэлпе. При этом опять же не важно, что Вы используете - метод веб-сервиса или PageMethod.
  • CascadingDropDown не имеет никакой серверной логики (веб-метод не в счет - он не является частью контрола). Все, что делает серверная часть контрола - это помогает облегчить создание клиентской части. Поэтому CascadingDropDownBehavior можно создать и использовать в отдельности от серверной части, в том числе и без ASP.NET 2.0 вообще. Достаточно подключить к странице клиентские скрипты и воспользоваться методом $create:

Sys.Application.add_init(function() {
    $create(
        AjaxControlToolkit.CascadingDropDownBehavior,
        {
            "Category":"Model",
            "ClientStateFieldID":"hiddenFieldClientStateId",
            "ParentControlID":"parentDropDownList",
            "PromptText":"Please select a model",
            "ServiceMethod":"GetDropDownContents",
            "ServicePath":"MyService.asmx",
            "id":"exCascadingDropDown"
        },
        null,
        null,
        $get("childDropDownList")
    );
});

из нерассмотренных ранее тут только свойство ClientStateFieldID . В качестве этого параметра нужно передать id некоторого хидден-поля, куда CascadingDropDownBehavior будет сохранять выбранное значение на время перезагрузки страницы.
  • В отличие от UpdatePanel, запросы, посланные через Sys.Net.WebServiceProxy.invoke, выполняются независимо друг от друга. Поэтому в случае, когда для выполнения запроса требуется много времени, возможна неприятная ситуация. Пользователь выбрал в родительском select’е некоторое значение. Ушел запрос. Не дожидаясь ответа, пользователь выбирает в этом же родительском select’е другое значение. Уходит второй запрос. Предположим, что для выполнения второго запроса нужно значительно менше времени, чем для выполнения первого, и в результате ответ на второй запрос пришел первым. Дочерний select заполнился, пользователь уже собрался в нем что-то выбрать, и в этот момент приходит ответ на первый запрос, и select заполняется невалидными в данной ситуации данными. В результате, занчения в дочернем select’е не соответствуют выбору в родительском select’е. Можно или нет считать это багом я не знаю. Однако разрешать эту ситуацию придется самостоятельно.
  • Для того, чтобы проводить клиентскую валидацию, управлять приоритетом запросов, показывать и прятать анимированный гиф вместо отображения текста Loading, нужно использовать события populating и populated.
  • Из-за необходимости иметь поле CascadingDropDownCategory, предпочтительнее (хотя и не обязательно) заполнять самый "верхний" DropDownList тоже с помошью CascadingDropDown.
  • Расположение CascadingDropDown в вебформе (и, следовательно, порядок их инициализации) играет важную роль. CascadingDropDown, заполняющий родительский select, должен инициализироваться раньше, чем заполняющий дочерний.

 Ну, пока вроде бы все. Если будет что-то, то либо эта статья будет дополнятся, либо выйдет отдельный пост с дополнениями.

Published Aug 31 2007, 05:58 AM by Aib

Комментарии

 

Raimon сказал:

про названия эт ты точно подметил ;)

August 31, 2007 12:12 PM
 

doctorsolberg сказал:

Спасибо, полезно!

September 1, 2007 4:28 AM

Оставить комментарий

(required)  
(optional)
(required)  

© 2007 Kazan Developers Community and Post`s Authors