в

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, должен инициализироваться раньше, чем заполняющий дочерний.

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

  • Знакомство со Script# - часть 3: используем foreach и создаем свои события

    Первые попытки использовать выражение foreach в Script# могут привести к генерации JS-скрипта, который не будет работать. Кроме того, в документации указано, что цикл foreach нельзя использовать с массивами, поскольку они не реализуют интерфейс IEnumerable. Тем не менее, реализация foreach в Script# полностью покрывает функционал выражения for..in в JS. Нужно лишь правильно им пользоваться.

    Итак, сперва рассмотрим различия между конструкциями foreach в С# (ведь Script# - это просто компилятор для C#) и for..in в JS.

    Во-первых, коду на C#

    foreach (object obj in col) {

        object val = obj;

    }

    соответствует код на JS

    for (var key in col) {

        var val = col[key];

    }

    Т.е., если в C# foreach перечисляет элементы коллекции, то в JS for..in перечисляет строковые ключи.

    Во-вторых, foreach в C# может работать только с объектами, реализующими интерфейс IEnumerable, а for..in в JS позволяет пройтись по всем пользовательским членам экземпляра класса.

    Теперь разберемся с тем, как for..in в JS работает с массивами. Вот небольшой пример JS-кода:

    var a = [1, 2, 3];

    var s1 = 0;

    var s2 = 0;

    for (var i = 0; i < a.length; i++) {

        s1 += i;

    }

    for (var i in a) {

        s2 += i;

    }

    alert("sum1: "+s1);

    alert("sum2: "+s2);

    Результат его выполнения, возможно, кого-то удивит. По идее, s1 и s2 должны быть равны. Однако, в первом случае i – это число, а во втором – строка. Таким образом, внутри конструкции for..in массив (так же как и любой другой JS-объект) воспринимается как словарь, для которого ключом является строка (а не число).

    Учитывая все вышенаписанное, принципы использования foreach в Script# выглядят вполне логичными.

    В пространстве имен System есть класс Dictionary, который реализует интерфейс IEnumerable, что позволяет использовать его в foreach, а также содержит статический метод GetDictionary, с помощью которого можно получить «Dictionary-обертку» для любого объекта. При генерации JS-кода он преобразуется в Object, так что никаких отдельных классов не создается.

    В качестве элемента Dictionary выступает тип DictionaryEntry, почти что точная копия своего «однофамильца» из FCL, с тем лишь отличием, что свойство Key имеет тип string, а не object:

    [imported]

    public sealed class DictionaryEntry {

        internal DictionaryEntry();

     

        [IntrinsicProperty]

        public string Key { get; }

        [IntrinsicProperty]

        public object Value { get; }
    }

    В результате, код на Script#:

    foreach (DictionaryEntry entry in Dictionary.GetDictionary(obj)) {

        string key = entry.Key;

        object value = entry.Value;

    }

    будет преобразован в такой JS-код:

    var $dict1 = obj;

    for (var $key2 in $dict1) {

        var entry = { key: $key2, value: $dict1[$key2] };

        var key = entry.key;

        var value = entry.value;
    }

    Таким образом, использование foreach вместе с классами Dictionary и DictionaryEntry полностью покрывает функционал цикла for..in.

    Теперь перейдем ко второму вопросу – создание собственных событий. Как уже было сказано ранее, из-за необходимости обеспечивать обратную совместимость, к типу Object не было добавлено никаких полей и методов, кроме статических. Т.е. информацию о событиях хранить по умолчанию негде. Поэтому, чтобы создать свое событие, нужно сперва добавить поле-хранилище. Для этого в MS AJAX Library существует тип Sys.EventHandlerList:

    using Sys;

    namespace Samples {

        <