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

Как мне стало известно, не всем понятна идея распределенного приложения. К тому же, приведенный в статье пример почему-то был воспринят "дословно", т.е. подвергся критике избранный мной формат хранения данных об абонентах (XML), способ организации приложения (собственно тот факт, что за основу была взята веб-служба XML), да и вообще, что ради такой простой задачи незачем и "огород городить". На форуме газеты это был единичный случай, но раз есть один такой человек, значит, их вполне может быть и несколько. В связи с этим, давайте мы с вами разложим все по полочкам и разберемся, что к чему.

Достоинства и недостатки ООП по сравнению с обычным процедурным программированием я рассматривать не намерен – это в свое время сделал Герман Иванов. Мне бы хотелось заострить свое внимание на следующих вопросах.

Итак, первым у нас следует формат базы данных. Почему же я выбрал такой "никчемный" формат, как XML? Во-первых, это позволяет читателю с любым уровнем подготовки создать себе за считанные минуты файл базы данных (речь идет именно о файле из предыдущей статьи) без установки или использования сторонних инструментов. Во-вторых, это является своего рода заглушкой, которую вы в любой момент можете заменить, исходя из условий конкретной задачи. И, наконец, в третьих, XML очень полезен в повседневной жизни. Применяется не только в базах данных, но, скажем, для хранения настроек программы. На www.windowsforms.net можно найти пример парсера, который "на лету" на основе XML создает windows-формы с иерархическим меню (вот где раскрывается потенциал XML, к слову сказать, лишь его небольшая часть), кнопками, к которым цепляются события (обработчики событий, правда, жестко запрограммированы, но ведь .NET Framework позволяет осуществлять и динамическую компиляцию), табами. В этом примере есть одна ошибка. Если она всплывет и у вас, то можете написать мне на форуме об этом, и я с радостью подскажу, что именно нужно поменять – это всего одна строчка.

Здесь я позволю себе немного отклониться от курса и упомянуть ну просто набивший оскомину вопрос безопасности в .NET. Нет, с безопасностью типов все в порядке. Народ ломает голову над тем, как защитить свое приложение от несанкционированного использования. Всем известен, пожалуй, главный недостаток (он же огромное достоинство) .NET – это, несомненно, Reflection. Именно благодаря этой технологии у нас есть все прелести, предоставляемые .NET, в том числе и динамическая компиляция. Но одной из этих прелестей является возможность получить на руки исходный код сборки, причем не только на IL, но и на C# вместе с VB .NET. Дальше, думаю, ход событий человека, добывшего себе такой код, понятен.

Так вот. Запихав в ресурсы часть кода, впоследствии его зашифровав, с помощью все того же Reflection можно на этапе выполнения (т.е. динамически) создать все недостающие части. Впервые такой прием был замечен на утилите Лютца Ройдера Reflector for .NET. Вы, естественно, должны понимать, что поломать можно все ("Whatever can be made...Can be un-made."), но сделать сносную защиту вполне возможно.
Вернемся к XML. Это настолько интересная технология, что я просто не мог уйти от ее упоминания в статье про веб-службы XML (даже сам SOAP является своего рода подмножеством XML). Как-нибудь мы посмотрим, как выжать из XML максимум функциональности.
Следующим по номеру идет понятие распределенного приложения как таковое. Все дело в том, что приложения, нацеленные на корпоративный рынок, в большинстве случаев должны работать в локальной/глобальной сети. Т.е. клиентская часть устанавливается на рабочие станции, а на сервере хранится все та же база данных, содержащая, скажем, данные о сотрудниках компании (т.н. кадровая задача). Хорошо, если управленческий аппарат предприятия состоит из одного отдела, имеющего в своем распоряжении один компьютер. Тогда действительно, ничего распределять не имеет смысла. Программа будет работать с базой данных напрямую.

А теперь представьте себе контору, состоящую как минимум из двух отделов – отдел кадров и бухгалтерия. В каждом отделе по несколько компьютеров, и с каждого из них необходимо иметь доступ к данным о сотрудниках. Как же нам поступить в этом случае? Выделяем машину под сервер. Чем мощнее будет эта машина, тем лучше. С помощью витой пары объединяем все имеющиеся в наличии компьютеры в локальную сеть. Скорей всего, это уже было сделано до вас, но было сделано плохо, или, что вероятнее всего, был проложен коаксиальный кабель. А 10Mb/s нынче не позволяет достичь удовлетворительной скорости работы.

На сервере заводим базу данных. Вариантов реализации доступа к базе данных просто уйма. .NET предоставляет нам две технологии удаленного взаимодействия – Remoting и Web Services XML. На одной из них я и остановился. Соответственно, в наши задачи входит написать веб-сервис. ОК, поручаем это отдельной группе разработчиков и продолжаем рассуждать дальше.

Реализация клиентской части будет зависеть от того, какие машины установлены в этих отделах. Пускай у нас в наличии есть среднестатистический Celeron 366MHz. На такой машине легко становится Win98SE (а также Win2000 и WinXP, правда памяти при этом должно быть не менее 128Mb) вместе с .NET Framework, а значит, мы, скорее всего, будем иметь дело с windows-клиентом. На машинах послабее будем работать через IE, а значит, вместо windows-клиента придется писать его аналог для web.

Реализацию клиентов вполне возможно поручить разным группам разработчиков, которые к тому же могут ничего не знать друг о друге. Желательно, конечно, чтобы ими управлял один и тот же человек, иначе они могут решить задачу отличными путями, а значит, обучение работе с системой будет происходить сложнее. Ввиду того, что клиентское приложение довольно легкое на подъем, его может довольно быстро написать и один программист. Пускай остальные сражаются с уровнем бизнес-процессов…

Самое интересное – это то, что вы с таким же успехом можете наладить связь с веб-службой, находящейся за сотни километров от клиентской машины посредством глобальной паутины. Помните, мы с вами в самом начале статьи задавали местонахождение нашей веб-службы – http://localhost/...? Теперь же нам надо задать адрес веб-службы в Интернете, а не на локальной машине, и организовать к ней постоянный доступ. Гиганты, разросшиеся до таких размеров, что не умещаются в одном здании, могут себе это позволить. Мне останется только позавидовать, если вы на VB6.0 напишите работоспособное приложение с аналогичной функциональностью за то же время, что и я.

И еще более того. У вас вполне может быть свой взгляд на вещи, свои потребности и возможности. Давайте мы с вами договоримся, что я расскажу, как решить поставленную в первой части статьи задачу с помощью веб-сервисов (а именно, написать распределенное приложение, впоследствии добавив несколько фич), а за то, подойдет эта реализация под ваши задачи, или нет, я ответственности нести не буду. Идет?

Едем дальше

Поначалу в этой статье я планировал изложить некоторые теоретические факты, но, написав оставшуюся часть, понял, что очень многое придется выбросить. Фактически, я успел бы привести всего несколько определений. Я посчитал это неразумным, поэтому решил отвести под теорию отдельную статью. Не надейтесь найти там все про внутреннее устройство веб-служб. По каждой из тем, которую я хочу затронуть, написаны достаточно объемные статьи. Как вы сами понимаете, пересказывать их содержание не имеет никакого смысла. В мои задачи входит дать вам начальные знания по данному вопросу. Если вы заинтересуетесь, то и сами найдете все, что вам необходимо.

Возможные усовершенствования

TVPattern

Вы, уверен, заметили, что в нашем приложении постоянно передаются данные как от уровня к уровню (from tier to tier), так и внутри каждого из уровней. Оставим в покое процесс "общения" с веб-службой. Сегодня мы сконцентрируем наши усилия на оптимизации процесса передачи данных между уровнем представления данных (Presentation Tier) в виде клиентов и, так сказать, уровнем бизнес-процессов (Business Process Tier), т.е. той самой вспомогательной библиотекой, формирующей запрос для отправки его веб-службе.

Существует несколько моделей построения приложений в стиле ООП, но одной из лучших является TVPattern, разработанный Германом Ивановым. В своих статьях (см. КГ №№1, 2, 4 за 2004 г. или на сайте http://german2004.da.ru) он изложил принципы, заложенные в TVPattern (а по ходу и в библиотеку TurboVision, которая была взята за основу), а также написал простенький примерчик, дабы наглядно продемонстрировать преимущества от использования TVPattern. Для лучшего усвоения нижеизложенного материала я вам рекомендую обратиться к этим статьям и внимательно их перечитать. На тот случай, если статей вы не читали, а старых газет и Интернета у вас нет, я вам вкратце расскажу все, что нам потребуется для продуктивной работы.

Здесь я представлю вашему вниманию лишь слегка модифицированный TVPattern, чтобы не было расхождений с тем, что написано у Германа (во избежание всякого рода недопониманий). TVPattern (да и ООП вообще) очень точно совпадает с окружающим нас миром. И чтобы доказать это, предлагаю остановиться на всем знакомом человеческом обществе.

Любой человек (ну, или практически любой) может отправлять сообщения во внешний мир (говорить), а также воспринимать информацию, полученную из внешнего мира (слышать). Когда этот человек выходит на площадь и начинает кричать "Жыве Беларусь!", окружающие по-своему реагируют на это событие. Кто-то подкрикивает "Жыве!". Кто-то хватает дубинку из-за пояса и достает рацию, чтобы оповестить находящихся неподалеку "объектов" о происходящем безобразии. А кто-то надел на голову наушники и слушает плеер – этому "объекту" вообще все равно. Тому же, кто кричит на площади "Жыве Беларусь!" безразлично конкретное число человек, которые его услышат (от этого будет зависеть разве только его моральное удовлетворение). Оттого, что кто-то слушает плеер, он хуже кричать не станет. По сути, он посылает сообщения всем объектам, вопрос только в том, как они на это отреагируют. А вот человек, переговаривающийся по рации, посылает сообщения определенной группе "объектов". Примерно такое же поведение можно реализовать и силами TVPattern.

Представив себе подобную ситуацию, вы поймете всю абсурдность активно пропагандируемой техники передачи параметров между объектами с помощью глобальных свойств. Ну представьте себе, какое свойство вы можете применить к кричащему на площади, чтобы узнать его имя? Все, что вы можете сделать, это спросить его: "А как тебя зовут?". В ответ он выдаст (или не выдаст – решать ему) сообщение, из которого можно узнать его имя. А если захочет, то пошлет лично вам сообщение с предложением стать рядом с ним и взять в руки транспаранты.

К слову сказать, все внутреннее устройство компьютера построено по точно такому же принципу. Скажем, когда вы прокручиваете текст в текстбоксе, полоса прокрутки посылает сообщение WM_VSCROLL самому текстбоксу, а он уже сам решает, что с ним делать – в большинстве случаев перемещает текст синхронно с лифтом. Или тот же принтер, когда у вас закончилась бумага, посылает сообщение об этом прискорбном факте.

А теперь давайте посмотрим, что станет, если вдруг весь мир перевернется, и останутся одни свойства. Прохожие будут сильно зависеть от человека, кричащего на площади. Если он уйдет, то в программировании придется переписывать каждый из объектов, участвовавших в извлечении информации с помощью свойств, а в случае с человеком придется переписывать ДНК.

Так зачем же нам устраивать что-либо подобное в своем собственном приложении? TVPattern как раз и поможет нам поставить все на свои места. Посему, добавляем в наше решение новый проект TVPattern (шаблон Class Library). Переименовываем Class1.cs в Controller.cs. Это будет сердце нашего TVPattern'a (ничего общего с тем, что в модели MVC понимается под словом Controller, здесь нет).

Для передачи параметров между объектами мы будем использовать экземпляры класса MyEvent. Все, что будет нужно окружающему миру, можно извлечь с помощью соответствующих свойств. Вот не надо меня здесь "ловить на слове". Из опыта общения с окружающим миром мы все с вами знаем, что такие характеристики, как цвет глаз или волос, можно узнать и без непосредственного вступления в контакт – это и есть свойства. Безусловно, можно общаться с помощью одних лишь сообщений, но… Опять представим себе человеческое общество. Один спрашивает другого: "А ты кто: брюнет или блондин?". Глупо, не правда ли? Чтобы не заниматься подобной ерундой, и придуманы свойства, иначе мы бы с вами ушли в такие дебри, что даже сами с трудом сквозь них пробирались (должен заметить, что в WinAPI именно так и приходится делать).

Для того чтобы проинформировать внешний мир о типе происходящих событий, нам потребуется перечисление MyCmd. Это перечисление будет содержать набор команд, которые любой объект может отдавать другим объектам. Самое главное – это интерфейс IHandleEvent. Любой объект, который захочет поучаствовать в обмене сообщениями должен реализовать этот интерфейс.

Позволю себе повторить сравнение Германа, что, в принципе, у нас получается дорога с двухсторонним движением. По одной стороне события идут от объекта, по другой – к объекту. Причем эти события обходят по очереди все подключившиеся объекты, примерно так, как "дизель" останавливается в каждой захудалой деревеньке, рядом с которой построена станция. Для того чтобы сигнатура обработчика исходящих событий имела требуемый нам вид, опишем делегат MyCommandHandler. Объект будет обрабатывать входящие события в методе HandleEvent, поэтому в описание интерфейса IHandleEvent добавим и его. Реализуем это в коде.

public class MyEvent
{
private object sender = null;
private MyCmd cmd = MyCmd.None;
private object parameter = null;
public MyEvent(object sender, MyCmd cmd)
{
this.sender = sender;
this.cmd = cmd;
}
public MyEvent(object sender, MyCmd cmd, object parameter): this(sender, cmd)
{
this.parameter = parameter;
}
public object Sender {
get {
return this.sender;
}
}
public MyCmd Cmd {
get {
return this.cmd;
}
}
public object Parameter {
get {
return this.parameter;
}
}
}

Класс MyEvent содержит два конструктора. Одному из них в качестве параметров передается объект, инициировавший событие (имя кричащего на площади, а точнее его тип), а также команда, которую хочет отдать объект окружающим, например, добавить строчку в лог. Второй конструктор плюс ко всему принимает еще и дополнительный параметр. Т.к. он имеет тип object, параметром может выступать что угодно. В нашем случае мы будем передавать экземпляры классов. Если проводить аналогию с человеком на площади, то это слова "Жыве Беларусь!". Для доступа к этой информации предусмотрены свойства только для чтения.

public enum MyCmd {
None
};

Перечисление MyCmd по умолчанию содержит только один элемент, None. Если какой-то объект вдруг передаст (хотя по логике вещей не должен) сообщение с командой None, то это значит, что он просто разговаривает сам с собой.

public delegate void MyCommandHandler(MyEvent myEvent);
public interface IHandleEvent
{
event MyCommandHandler OnMyCommand;
void HandleEvent(MyEvent myEvent);
}

Любой объект, реализующий интерфейс IHandleEvent, должен отвечать конкретным требования, а именно, должен уметь посылать свои собственные сообщения и обрабатывать поступающие.

public class Service
{
public static void GarbageCollect(MyEvent myEvent)
{
myEvent = null;
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
}
}

Я для удобства написал себе еще сервисный класс. Он необходим для того, чтобы высвобождать память от ставших ненужными экземпляров класса MyEvent. Как вы знаете, сборщик мусора не торопится делать свое дело, поэтому приходится вызывать его вручную. Методы GC необходимо вызывать именно в той последовательности, как указано выше, иначе мусор может и не пойти на переработку. Следует отметить, что отдавать myEvent на съедение сборщику нужно не всегда, а только тогда, когда бесполезность этого объекта очевидна для разработчика, т.е. когда вы точно знаете, что никому другому это сообщение не понадобится.

Компилируем проект в библиотеку классов. Теперь мы готовы к реализации интерфейса IHandleEvent в каком-нибудь объекте. В нашем случае этим объектом может стать класс XmlQueryBuilder. В моем тестовом примере это единственный объект, реализующий данный интерфейс. В принципе, можно было бы реализовать его и у кнопок, лога и строки состояния, но сейчас это излишне – суть ведь не в этом, нам нужно лишь передать параметры библиотеке и от нее, уровню представления данных, т.е. windows- и web-клиенту. Укажем, что наш класс реализует интерфейс IHandleEvent:

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

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

Класс ResultsParameters как две капли воды похож на QueryParameters. Только здесь у нас два поля типа string, хранящих номер телефона и соответствующее имя (дальше эта информация будет использоваться для заполнения ListView в случае winforms и ListBox в случае web-формы).

Чтобы оповестить класс XmlQueryBuilder о том, что надо передать запрос веб-службе, добавим в перечисление MyCmd элемент TransferQuery, а для того, чтобы проинформировать вызывающий код о том, что необходимо принять результаты поиска, добавим еще один элемент в перечисление – CatchResults.

Как вы можете видеть из кода, я завел еще два элемента – Error (оповещает о произошедшей ошибке) и LogAdd (добавляет строчки в лог, а также в строку состояния).

Саму логику поиска я вынес в отдельный метод SendQ, который принимает на вход строку для поиска и метод поиска. Его код очень похож на код метода TransferQuery. Но я вам все-таки приведу его, т.к. на пальцах объяснить отличия мне будет тяжело.

private void SendQ(string searchString, SearchMode searchMode)
{
string QueryString = "";
OnMyCommand(new MyEvent(this, MyCmd.LogAdd, "Построение запроса"));
switch(searchMode) {
case SearchMode.Name:
QueryString = "//name[. = '" + searchString + "']/parent::node()/phone";
break;
case SearchMode.Phone:
QueryString = "//phone[. = '" + searchString + "']/parent::node()/name";
break;
}
OnMyCommand(new MyEvent(this, MyCmd.LogAdd, "Получение данных"));
string SearchResults = "";
try {
SearchResults = tService.GetPhoneData(QueryString);
switch(searchMode) {
case SearchMode.Name:
OnMyCommand(new MyEvent(this, MyCmd.CatchResult, new ResultParameters(searchString, SearchResults)));
break;
case SearchMode.Phone:
OnMyCommand(new MyEvent(this, MyCmd.CatchResult, new ResultParameters(SearchResults, searchString)));
break;
}
} catch {
OnMyCommand(new MyEvent(this, MyCmd.Error, "Запись, соответствующая запросу, не найдена"));
}
}

Этот код мне не нравится, т.к. он далеко не самый оптимальный, но использовать полиморфизм я здесь не хотел, т.к. во-первых, это заняло бы побольше места, а во-вторых, дело было к вечеру… Займитесь этим самостоятельно в свободное от работы время, если вам, в отличие от меня, будет не лень. В реальном же приложении я лениться никому не советую, т.к. при изменении внешних условий придется активно поработать пальчиками.

Видите, за счет того, что мы отсылаем события OnMyCommand, мы избавляемся от заморочек с делегатами, к тому же наш код получился универсальным как для windows- так и для web-клиента. И теперь лог будет вестись абсолютно нормально в обоих этих клиентах.
Код метода HandleEvent я по возможности разредил. Для начала осуществим проверку, не является ли OnMyCommand или параметр myEvent null'ом. В первом случае это означает, что никто нас не слушает, а значит, и говорить попусту нечего, во втором – что нам не передали информацию, необходимую для поиска. Соответственно, также выходим из метода.

// Выполняем проверку
if(OnMyCommand == null || myEvent == null)
return;
switch(myEvent.Cmd) {
case(MyCmd.TransferQuery):
// Если в качестве параметра нам передали именно то, что нужно...
if(myEvent.Parameter is QueryParameters) {
this.SendQ((myEvent.Parameter as QueryParameters).GetSearchString, (myEvent.Parameter as QueryParameters).GetSearchMode);
} else {
OnMyCommand(new MyEvent(this, MyCmd.Error, "Параметры поиска не заданы в должном виде"));
}
// Т.к. это сообщение больше никому не нужно, то очищаем память
Service.GarbageCollect(myEvent);
break;
}

Ну вот, мы с вами реализовали объект XmlQueryBuilder в стиле TVPattern. Осталось дописать код в главной форме. Здесь по нажатии на кнопку нужно лишь отправить сообщение конкретно объекту XmlQueryBuilder с параметрами, необходимыми для поиска. Это можно сделать, например, так:

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.ResultParameters) {
this.str[0] = (myEvent.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 главной формы никому не нужен. Чтобы он мог обрабатывать входящие сообщения, необходимо подключиться к событию OnMyCommand. В конструкторе формы пишем:

this.xmlQBuilder.OnMyCommand += new MyCommandHandler(this.HandleEvent);
Все. Теперь по нашему приложению гуляют сообщения, уровень клиента и вспомогательной библиотеки изолированы и не зависят друг от друга. Вы видите, как все изящно получилось? А теперь представьте, что бы было, если бы мы связались с глобальными свойствами? В принципе, ничего страшного, только при малейшем изменении любого из уровней, нам пришлось бы править и второй. Сейчас же я могу как угодно крутить сообщениями и логикой их обработки – все останется на своих местах. Жыве TVPattern!

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

Многопоточность

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

Сначала научимся вызывать асинхронно методы веб-служб. Как вы знаете, обращаясь к методам веб-службы, мы на самом деле обращаемся к соответствующим методами прокси этой веб-службы, который был услужливо создан утилитой wsdl.exe. К ее услугам прибегает Visual Studio при создании веб-ссылки. Кстати, для обновления веб-ссылки щелкните правой кнопкой мыши по требуемой ссылке и выберите в контекстном меню пункт Update WebReference.

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

Если вызываемый синхронно метод носил имя GetPhoneData, то эта парочка будет называться BeginGetPhoneData и EndGetPhoneData соответственно. Существует два способа вызвать веб-службу асинхронно. Первый из них – предоставить методу Begin ссылку на callback метод, который будет вызван по окончании операции. Второй – ждать завершение операции с использованием методов класса WaitHandle. Фокус с callback мы с вами обыграем на примере windowsforms, а сейчас посмотрим чуть-чуть поближе на класс WaitHandle.

Этот класс имеет в своем распоряжении три метода – WaitOne, WaitAny, WaitAll. В первом случае клиент будет ждать ответа от единственной веб-службы, во втором – одной из нескольких (а конкретнее, той, которая первой завершит свою работу), в третьем случае клиент будет дожидаться окончания работы всех веб-служб и только после этого управление будет передано вызывающему потоку. На первый взгляд, все довольно просто. Многопоточность – это такая штука, которая поначалу кажется простой, но провозиться с ней можно долгие часы.

Для начала опишем пространство имен System.Threading в файле XmlQueryBuilder.cs. В методе SendQ класса XmlQueryBuilder после отправки сообщения "Получение данных" дописываем следующие нехитрые строки:

IAsyncResult asyncResult = this.tService.BeginGetPhoneData(QueryString, null, null);
WaitHandle[] waitHandle = {asyncResult.AsyncWaitHandle};
WaitHandle.WaitAll(waitHandle);

Интерфейс IAsyncResult предоставляет разработчику несколько свойств для контроля степени выполнения. Так, свойство IsCompleted возвращает true по окончании операции, в ином случае – false. А, скажем, свойство AsyncState возвращает третий по счету параметр метода Begin. Объект, реализующий интерфейс IAsyncResult (в данном случае это объект типа WebClientAsyncResult из пространства имен System.Web.Services.Protocols, возвращаемый методом Begin), используется для передачи в качестве параметра методу End и тем самым указывается, какой запрос следует закончить (ведь их может быть и несколько).

Собственно говоря, когда мы вызываем только одну веб-службу, можно использовать любой из вышеприведенных методов класса WaitHandle. В блоке try{}catch{} этого же метода заменяем строку синхронного вызова веб-службы на SearchResults = tService.EndGetPhoneData(asyncResult);. Можете запускать и наслаждаться результатом.

Да вот только получить эффект от вызова одной веб-службы вы не сможете. Как ни крутись, а придется высвобождать UI поток. Microsoft установила "красную границу" для задержек в обработке сообщений формой – 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 asyncResult)
{
this.qm.EndInvoke(asyncResult);
this.log.Items.Add("--- Поиск завершен ---");
}

Снова возвращаемся в метод TransferData, где, наконец, обращаемся к вспомогательной библиотеке в отдельном потоке:
this.qm.BeginInvoke(new MyEvent(this, MyCmd.TransferQuery, new qBuilder.QueryParameters(searchString, searchMode)), new AsyncCallback(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);
}
}
Герман Иванов

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

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

…продолжение следует

Алексей Нестеров


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

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