От простого к сложному. Веб-службы XML в .NET Framework

От простого к сложному. Веб-службы XML в .NET Framework

Начало в КГ № 37

Если вы работаете в Visual Studio 2003, то она сама добавит в код члены этого интерфейса. Пользователям VS2002 придется вбивать их вручную. Чтобы облегчить ваш труд, я приведу вам, как это должно выглядеть:
public event MyCommandHandler OnMyCommand; public void HandleEvent(MyEvent my Event) { // Здес
ь мы будем обрабатывать поступившие событи

Теперь немного модифицируем код самого класса. Если вы помните, в предыдущем примере у нас был метод TransferQuery. Теперь мы будем посылать сообщение MyCmd.TransferQuery. Как только оно дойдет до XmlQueryBuilder.HandleEvent, мы передадим наш запрос веб-сервису. Да, для того, чтобы передавать параметры поиска, я создал вспомогательный класс QueryParameters. Его конструктор в качестве параметров принимает строку для поиска и метод поиска. Также нам потребуется класс ResultsParameters для передачи результатов поиска внешнему миру. Я думаю, приводить здесь код этих классов не стоит. Вы сами без труда их себе напишете. Я вам дам лишь необходимую информацию для их написания.

Класс QueryParameters должен содержать два закрытых (private) поля типа string и SearchMode. Первое предназначено для хранения строки, по которой будет вестись поиск. Второе — для хранения метода поиска, т.е. как будем искать — по номеру телефона или же по имени. Организуйте публичные свойства только для чтения для доступа к информации, хранящейся в этих полях. Естественно, для записи информации в эти поля нам понадобится конструктор. Он практически идентичен первому конструктору класса MyEvent. Класс Results Parameters как две капли воды похож на QueryParameters. Только здесь у нас два поля типа string, хранящих номер телефона и соответствующее имя (дальше эта информация будет использоваться для заполнения ListView в случае winforms и ListBox в случае web-формы). Чтобы оповестить класс Xml QueryBuilder о том, что надо передать запрос веб-службе, добавим в перечисление MyCmd элемент TransferQuery, а чтобы проинформировать вызывающий код о том, что необходимо принять результаты поиска, добавим в перечисление еще один элемент — CatchResults. Как вы можете видеть из кода, я завел еще два элемента — Error (оповещает о произошедшей ошибке) и LogAdd (добавляет строчки в лог, а также в строку состояния). Саму логику поиска я вынес в отдельный метод SendQ, который принимает на вход строку для поиска и метод поиска. Его код очень похож на код метода TransferQuery. Но я вам все-таки приведу его, т.к. на пальцах объяснить отличия мне будет тяжело.
private void SendQ(string searchString, SearchMode searchMode) { string QueryString = "&q
uot;; OnMyCommand(new MyEvent(this, MyCmd.LogAdd, "Построение запроса")); switch(searchMod
e) { case SearchMode.Name: QueryString = "//name[. = '" + searchString + "']/parent::
node()/phone"; break; case SearchMode.Phone: QueryString = "//phone[. = '" + searchSt
ring + "']/parent::node()/name"; break; } OnMyCommand(new MyEvent(this, MyCmd.LogAdd, &quo
t;Получение данных")); string SearchResults = ""; try { SearchResults = tService.Get 
PhoneData(QueryString); switch(searchMode) { case SearchMode.Name: OnMyCommand(new My Event(this, My
Cmd.CatchResult, new ResultParameters(searchString, Search Results))); break; case SearchMode.Phone:
 OnMyCommand(new My Event(this, MyCmd.CatchResult, new ResultParameters(SearchResults, search String
))); break; } } catch { OnMyCommand(new MyEvent (this, MyCmd.Error, "Запись, соответствующа

Этот код мне не нравится, т.к. он далеко не самый оптимальный, но использовать полиморфизм я здесь не хотел, т.к., во-первых, это заняло бы побольше места, а во-вторых, дело было к вечеру… Займитесь этим самостоятельно в свободное от работы время, если вам, в отличие от меня, будет не лень. В реальном же приложении я лениться никому не советую, т.к. при изменении внешних условий придется активно поработать пальчиками.
Видите, за счет того, что мы отсылаем события OnMyCom-mand, мы избавляемся от заморочек с делегатами, к тому же, наш код получился универсальным как для windows- так и для web-клиента. И теперь лог будет вестись абсолютно нормально в обоих этих клиентах. Код метода HandleEvent я по возможности разредил. Для начала осуществим проверку, не является ли OnMyCommand или параметр myEvent null'ом. В первом случае это означает, что никто нас не слушает, а значит, и говорить попусту нечего, во втором — что нам не передали информацию, необходимую для поиска. Соответственно так же выходим из метода.
// Выполняем проверку if(OnMyCommand == null || myEvent == null) return; switch(myEvent.Cmd) {
 case(MyCmd.TransferQuery): // Если в качестве параметра нам передали именно то, что нужно... if(myE
vent.Parameter is QueryParameters) { this.SendQ((myEvent.Parameter as QueryParameters).GetSearchStri
ng, (myEvent.Parameter as QueryParameters).GetSearchMode); } else { OnMyCommand(new MyEvent (this, M
yCmd.Error, "Параметры поиска не заданы в должном виде")); } // Т.к. это сообщение больше 
никому не нужно, то очищаем память Service.GarbageCollect(myEvent); break; }

Ну вот, мы с вами реализовали объект XmlQueryBuilder в стиле TVPattern. Осталось дописать код в главной форме. Здесь по нажатии на кнопку нужно лишь отправить сообщение конкретно объекту XmlQuery Builder с параметрами, необходимыми для поиска. Это можно сделать, например, так:
this.xmlQBuilder.HandleEvent(new MyEvent(this, MyCmd.TransferQuery, new qBuilder.QueryParameters(searchString, searchMode)));
Вместо searchString и searchMode подставляете необходимые значения. Осталось только добавить обработку входящих событий для главной формы. Для этого добавим в код главной формы метод HandleEvent, сигнатура которого должна соответствовать делегату MyCommandHandler. Я покажу, как реализовать только один блок выборки. Обработка команд MyCmd.LogAdd и MyCmd.Error самоочевидна.
case MyCmd.CatchResult: if(myEvent.Parameter is qBuilder.Result Parameters) { this.str[0] = (m
yEvent.Parameter as qBuilder.ResultParameters).GetName; this.str[1] = (myEvent.Parameter as qBuilder
.ResultParameters).GetPhone; this.resultsList.Items.Add(new ListViewItem(str)); } break;

На всякий случай поясню, что qBuilder — это псевдоним (alias) пространства имен Nesterov.TDirectory.TQueryBuilder. Также не забудьте в главной форме описать то пространство имен, где у вас лежит базовая логика TVPattern (например, using Nesterov. TVPattern). Я надеюсь, вы догадались, что сам по себе метод HandlEvent главной формы никому не нужен. Чтобы он мог обрабатывать входящие сообщения, необходимо подключиться к событию OnMy Command. В конструкторе формы пишем:
this.xmlQBuilder.OnMyCommand += new MyCommandHandler(this.Handle Event);

Все. Теперь по нашему приложению гуляют сообщения, уровень клиента и вспомогательной библиотеки изолированы и не зависят друг от друга. Вы видите, как все изящно получилось? А теперь представьте, что бы было, если бы мы связались с глобальными свойствами. В принципе, ничего страшного, только при малейшем изменении любого из уровней нам пришлось бы править и второй. Сейчас же я могу как угодно крутить сообщениями и логикой их обработки — все останется на своих местах. Жыве TVPattern!
В web-клиенте предпринимаем точно такие же шаги, только несколько по-другому. Этот вопрос мы с вами оговаривали в предыдущей моей статье. С этого момента я не буду больше развивать web-клиента, т.к. его код мало чем отличается от такового в клиенте под windows. Хотя, если встретится какое-то принципиальное отличие, я непременно об этом упомяну.

Многопоточность
Я думаю, вы уже успели вдоволь наиграться с написанным общими усилиями приложением. Самые внимательные из вас заметили, что как только вы начинаете поиск, форма замораживается. Снова реагировать на события Windows она начинает только после того, как веб-служба вернет результат. С точки зрения дружественности интерфейса это очень плохо. К тому же, наша строка состояния вместе с прогрессбаром становится просто бесполезным элементом управления. Разрешить проблему поможет многопоточность.

Сначала научимся вызывать асинхронно методы веб-служб. Как вы знаете, обращаясь к методам веб-службы, мы на самом деле обращаемся к соответствующим методам прокси этой веб-службы, который был услужливо создан утилитой wsdl.exe. К ее услугам прибегает Visual Studio при создании веб-ссылки. Кстати, для обновления веб-ссылки щелкните правой кнопкой мыши по требуемой ссылке и выберите в контекстном меню пункт Update WebReference. Автоматически сгенерированный прокси обладает всей необходимой функциональностью для организации многопоточности. Помните, я говорил о том, что веб-служба является изолированным структурным блоком? Это ее свойство позволяет нам при написании веб-службы не думать, как и кто впоследствии будет ее вызывать. Другими словами, веб-службе все равно, будет она вызываться синхронно или асинхронно. За это отвечает прокси-класс. Именно он содержит методы Begin и End, необходимые для асинхронного вызова веб-службы.

Если вызываемый синхронно метод носил имя GetPho-neData, то эта парочка будет называться BeginGetPhoneData и EndGetPhoneData соответственно. Существует два способа вызвать веб-службу асинхронно. Первый из них — предоставить методу Begin ссылку на callback-метод, который будет вызван по окончании операции. Второй — ждать завершения операции с использованием методов класса WaitHandle. Фокус с callback мы с вами обыграем на примере windowsforms, а сейчас посмотрим чуть-чуть поближе на класс WaitHandle. Этот класс имеет в своем распоряжении три метода: WaitOne, WaitAny, WaitAll. В первом случае клиент будет ждать ответа от единственной веб-службы, во втором — одной из нескольких (а конкретнее, той, которая первой завершит свою работу), в третьем случае клиент будет дожидаться окончания работы всех веб-служб, и только после этого управление будет передано вызывающему потоку. На первый взгляд, все довольно просто. Многопоточность — это такая штука, которая поначалу кажется простой, но провозиться с ней можно долгие часы.

Для начала опишем пространство имен System.Threading в файле XmlQueryBuilder.cs. В методе SendQ класса XmlQue-ryBuilder после отправки сообщения "Получение данных" дописываем следующие нехитрые строки:
IAsyncResult asyncResult = this. tService.BeginGetPhoneData(QueryString, null, null); WaitHand
le[] waitHandle = {async Result.AsyncWaitHandle}; WaitHandle.WaitAll(waitHandle);

Интерфейс IAsyncResult предоставляет разработчику несколько свойств для контроля степени выполнения. Так, свойство IsCompleted возвращает true по окончании операции, в ином случае — false. А, скажем, свойство AsyncState возвращает третий по счету параметр метода Begin. Объект, реализующий интерфейс IAsyncResult (в данном случае это объект типа WebClientAsyncResult из пространства имен System.Web. Services.Protocols, возвращаемый методом Begin), используется для передачи в качестве параметра методу End, и тем самым указывается, какой запрос следует закончить (ведь их может быть и несколько).
Собственно говоря, когда мы вызываем только одну веб-службу, можно использовать любой из вышеприведенных методов класса WaitHandle. В блоке try{}catch{} этого же метода заменяем строку синхронного вызова веб-службы на SearchResults = tService.EndGet PhoneData(asyncResult);. Можете запускать и наслаждаться результатом. Да, вот только получить эффект от вызова одной веб-службы вы не сможете. Как ни крутись, а придется высвобождать UI-поток. Micro-soft установила "красную границу" для задержек в обработке сообщений формой — 250 мс (на самом деле требования еще жестче, но это самые лояльные по отношению к разработчику). Если форма заморозится больше, чем на 250 мс, необходимо прибегать к многопоточности. Что мы и сделаем.
Переходим в главную форму. Подготовка к работе с callback-методом несколько сложнее. Для начала напишем метод, который предполагается вызывать в отдельном потоке.
private void QM(MyEvent myEvent) { this.xmlQBuilder.HandleEvent(myEvent); }

Следующим этапом будет описание делегата, чья сигнатура должна совпадать с сигнатурой только что написанного метода. Этот делегат нам потребуется для асинхронного вызова метода QM.
private delegate void QMDelegate (MyEvent myEvent);

Далее в методе TransferData передаем новому экземпляру делегата QMDelegate ссылку на метод QM:
QMDelegate qm = new QMDelegate (this.QM);

Также нам потребуется собственно callback-метод:
private void QMCallback(IAsyncResult async Result) { this.qm.EndInvoke(asyncResult); this.log.
Items.Add("--- Поиск завершен ---"); }

Снова возвращаемся в метод TransferData, где, наконец, обращаемся к вспомогательной библиотеке в отдельном потоке:
this.qm.BeginInvoke(new MyEvent(this, MyCmd. TransferQuery, new qBuilder. QueryParameters (sea
rchString, search Mode)), new Async Callback(this. QMCallback), this.xmlQBuilder);.

Логика действий программы проста. После вызова метода BeginInvoke мы тем самым создаем второй поток, освобождая UI для обработки сообщений и предотвращая заморозку формы. Верный признак того, что все идет как положено, — мигающий курсор в текстбоксе. Как только операции во втором потоке завершились, вызывается callback-метод, в котором мы извещаем пользователя о том, что процесс поиска окончен. С помощью все того же делегата QMDelegate можно вызвать QM и синхронно. Для этого предназначен метод Invoke.

И все бы ничего, если бы не одно но. Существует такое понятие, как потокобезопасность. И наряду с этим понятием программист всегда должен помнить правило: "Не работай с окном из потока, его не создавшего". Имеется в виду, что нельзя из другого потока просто так взять и изменить, скажем, надпись метки. Нет, конечно, можно, и у меня даже все проходило. Но никто вам этого не гарантирует. Поэтому, чтобы все было по уму, надо возиться с BeginInvoke. А удовольствие это ниже среднего, особенно при активном обновлении UI из других потоков. Тогда у меня родилась идея приспособить под это дело TVPattern. Т.е. из другого потока посылаем сообщения главной форме, а она уже сама обновляет элементы управления. На форуме КГ мы с Германом Ивановым как-то общались на эту тему, и им была предложена несколько иная схема работы. Думаю, он не будет возражать, если я о ней здесь упомяну.

Смысл ее сводится к следующему. У нас есть коллекция в виде ArrayList (в оригинале была StringCollection), а также таймер. Эта коллекция заполняется из другого потока. А по таймеру осуществляется выборка сообщений из коллекции и их обработка. За один такт обрабатывается одно сообщение, а затем удаляется. Поскольку что в моем варианте, что у Германа контролы обновляются в UI-потоке, нам не надо больше мучиться с Invoke. Вот как реализовать вышесказанное в коде:
private Timer timer1 = new Timer(); private StringCollection msglst = new StringCollection(); 
timer1.Interval = 100; timer1.Tick += new EventHandler (timer1_Tick); timer1.Start(); // Запустил та
ймер в цикле выполнять вот такой обработчик. private void timer1_Tick(object sender, EventArgs e) { 
while(msglst.Count > 0){ string msg = msglst[0]; ProcessMessages(msg); msglst.RemoveAt(0); } }

Герман Иванов

На сегодня, думаю, хватит. Когда мы с вами встретимся в следующий раз, сказать сложно, но все же…

2004, Алексей Нестеров. Продолжение следует

P.S. Чтобы вы не охладели к веб-сервисам, скажу, что можно получить веб-сервис, который сохраняет свое состояние между вызовами (stateful вместо stateless), т.е. что-то вроде singleton, если сравнивать с Remoting. А также существует возможность не только обращаться к веб-сервису, но и сделать так, чтобы веб-сервис сам рассылал сообщения всем клиентам либо какому-то определенному из них (P2P). Если вам интересно, можете самостоятельно поработать в этом направлении.

компьютерная газета


Компьютерная газета. Статья была опубликована в номере 38 за 2004 год в рубрике программирование :: разное

©1997-2024 Компьютерная газета