...
...

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

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

Столкнувшись с проблемой написания распределенного приложения для документооборота, я оказался перед выбором технической реализации взаимодействия клиентов и сервера. По началу я повернул голову в сторону Remoting. Но в одном из тредов на форуме GotDotNet.Ru прочитал, что только веб-сервисы достойны белого человека. Я сделал ставку на веб-службы XML и не прогадал.

В этой статье мне не хотелось бы загружать вас теорией, тем более что на данном этапе от этого все равно не будет никакого толку. Вкратце: веб-служба представляет собой изолированный структурный блок, который взаимодействует с клиентом посредством стандартных протоколов HTTP и XML (SOAP, WSDL, UDDI пока не трогаем). Для совместной работы необходимо лишь "понимание" обеими сторонами формата передаваемых данных (с этим проблем нет, т.к. и HTTP, и XML уже давно устоявшиеся стандарты). В силу своей изолированности, веб-службе все равно, на каком языке написан обращающийся к ней клиент и под какую платформу.

Сегодня мы создадим несложное распределенное приложение, которое будем совершенствовать от статьи к статье. Давайте представим, что в нашу программистскую контору поступил заказ от телефонного оператора на разработку распределенного приложения, в задачи которого входит предоставление информации о конкретном абоненте. Предполагается организация справки по телефону и через веб-интерфейс. Безусловно, речь не идет об операторах мобильной связи — в целях конфиденциальности своих клиентов они на это не пойдут, хотя я более чем уверен, что все операторы мобильной связи плотно сотрудничают с МВД, предоставляя им всю интересующую информацию, вплоть до местонахождения абонента. К тому же по рукам ходит база данных Velcom. Она, правда, не первой свежести (всего 300 тысяч с хвостиком абонентов), но не исключено, что у кого-то из вас есть и более поздняя "версия". Так что речь, естественно, идет об операторе стационарных телефонов. Придется позаботиться о безопасности и здесь, но это уже тема отдельной статьи.

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

Ну вот. У нас на руках уже есть бизнес-логика приложения. Перейдем к его практической реализации. Создадим Blank Solution, который назовем TDirectory. Сюда мы будем складывать все необходимые нам проекты. Чтобы не оговариваться каждый раз, все решение будет написано на C#. Глобальным пространством имен является Nesterov.TDirectory (вы, конечно, вправе использовать и любое другое). Пространство имен каждого отдельного проекта совпадает с его именем. Это играет свою роль в решениях с большим числом проектов: классы не валяются в общей куче, а распределены по категориям. При большом количестве разноплановых классов пришлось бы вводить дополнительные пространства имен (примерно так, как это сделано в библиотеках .NET Framework). Да, и я надеюсь, что вы уже чувствуете себя в Visual Studio .NET как рыба в воде, и мне не придется описывать каждое движение мыши.

Веб-служба
Добавляем в solution новый проект. В качестве шаблона выберем ASP.NET Web Service. В поле Location необходимо указать размещение будущего веб-сервиса. Заменим его на http://localhost/TWebService. После того, как студия установит соединение с сервером, в окне Solution Explorer отобразится список файлов, из которых нас пока интересует только один, с расширением *.asmx. Это и есть, собственно, веб-сервис. Переименуем его из безликого Service1 в TService.asmx. Открываем редактор кода. Как видите, ничего особенного веб-служба внутри из себя не представляет. Это обычный файл с расширением *.cs, практически ничем не отличающийся от заготовки, генерируемой по шаблону Windows Application. Несмотря на то, что в окне Solution Explorer нет файла TService.asmx.cs, он незримо присутствует, в чем можно удостовериться, отметив опцию Show All Files. Если же все-таки просмотреть код самого TService.asmx (с помощью любого текстового редактора), то можно в явной форме увидеть ссылку и на наш TService.asmx.cs — Codebehind = "TService.asmx.cs".

Делаем ссылку на библиотеку System.XML.dll и описываем пространство имен System.Xml. Это нам потребуется для работы с базой данных. Раз уж я заикнулся про базу данных, давайте-ка ее, не теряя времени, и создадим. Добавляем к проекту новый xml файл. Пусть это будет TBase.xml. Как вы, наверное, догадались, здесь будет храниться информация об абонентах.

<?xml version="1.0" encoding="utf-8"?>
<directory>
<record>
<name>Вова</name>
<phone>1870</phone>
</record>
<record>
<name>Иосиф</name>
<phone>1879</phone>
</record>
<record>
<name>Никита</name>
<phone>1894</phone>
</record>
<record>
<name>Леня</name>
<phone>1906</phone>
</record>
<record>
<name>Миша</name>
<phone>1931</phone>
</record>
</directory>

Чтобы наша веб-служба выделялась на фоне остальных, необходимо задать собственное уникальное пространство имен (если же его оставить, как есть, http://tempuri.org/, то это может привести к конфликту при встрече двух веб-служб с одинаковым пространством имен и одинаковыми же именами методов: клиенты будут просто не в состоянии отличить их друг от друга). А чтобы сторонним разработчикам было ясно предназначение веб-службы, добавим свой комментарий. Все это делается с помощью атрибута WebService, который размещается перед объявлением класса.

[WebService(Namespace = "http:// localhost/TWebService/TService/", Description = "Represents methods to search phone data")]
public class TService: WebService {}

Любой метод, который предполагается сделать доступным для клиентов, помечается атрибутом WebMethod и модификатором доступа public. Для работы с базой данных нам потребуются методы класса XmlDocument пространства имен System.Xml: Load (для загрузки файла базы данных) и SelectSingleNode (для поиска узла, соответствующего запросу). В тело класса TService запишем следующий код (оставляя при этом код, сгенерированный мастером):

// Надо было бы добавить блок try{}catch{} — вдруг базы данных не существует
private XmlDocument xmlDoc;
public TService()
{
InitializeComponent();
this.xmlDoc = new XmlDocument();
// Загружаем файл базы данных
this.xmlDoc.Load(@"D:\TBase.xml");
}
[WebMethod]
// На входе строка запроса на языке XPath, на выходе — текст найденного узла (в случае успешного поиска)
public string GetPhoneData(string query)
{
XmlNode xmlNode = xmlDoc.Select SingleNode(query);
return xmlNode.InnerText;
}

В итоге у нас получился изолированный класс, которому абсолютно безразлично, какой запрос поступит на вход метода GetPhoneData и куда пойдут результаты поиска дальше.
Запускаем проект на выполнение. Перед вами появится любимец публики Internet Explorer. На сгенерированной странице мы видим заданное ранее описание сервиса, а также ключевой метод GetPhoneData. Предлагаю сразу же проверить работоспособность написанного кода. Переходим по ссылке GetPhoneData. Отобразится новая страница. Метод принимает только одни параметр — query, поэтому и текстовое поле всего одно. В него можно вбить свой запрос на языке XPath (XML Path Language), и если он был корректен, то отобразится ответ в формате xml. Отлаживается веб-служба точно так же, как и обычный класс.

Вспомогательная библиотека
Настало время написать библиотеку, в задачи которой будет входить построение запросов и отправка их веб-сервису. При таком раскладе клиенты и веб-сервис не будут знать о существовании друг друга. Все сообщение идет через вспомогательную библиотеку. По идее, ее бы тоже не мешало реализовать как отдельный веб-сервис, но это уже зависит от конкретных задач. Мы же создадим библиотеку классов, которую назовем TQueryBuilder. Только не подумайте, что это трехзвенка, как в случае с Remoting. Можно было бы обойтись и без этой библиотеки, подключая клиентов напрямую, но тогда нам бы пришлось два раз повторять один и тот же код для web- и windows-клиента. (Замечу, что можно было бы из клиента обращаться к библиотеке, получать сформированный запрос, а затем отправлять его на сервер.)

Добавляем к вновь созданному проекту web-ссылку. Щелкаем правой кнопкой мыши по TQueryBuilder и в контекстном меню выбираем пункт Add Web Reference. В появившемся диалоговом окне нас интересует только одна ссылка — Web services on the local machine. Проследуйте по ней и перед вами отобразится список всех найденных веб-сервисов. Ищем среди них наш TService. В поле Web reference name задаем то имя, под которым сервис будет значиться в проекте. По умолчанию это localhost, но я предлагаю вам задать TS. Жмем OK. Теперь мы можем обращаться к веб-сервису так, как будто это обыкновенный локальный (а не удаленный, как было бы на самом деле) класс со своими методами и свойствами. Строго говоря, мы имеем дело не с самим сервисом, а с его прокси, который только имитирует сам сервис. Точно так же дела обстоят и в Remoting. Только там уже целых два прокси — один настоящий (Real proxy), а другой прозрачный (Transparent proxy).

Чтобы получить доступ к TService, опишем пространство имен: using TQBuilder.TS;. Объявляется новый экземпляр класса самым обыкновенным образом: private TService tService = new TService();. Чтобы организовать вывод сообщений о ходе событий, опишем делегат SpeakOut: public delegate int SpeakOut(object outMessage);. Функциональность языка запросов XPath позволяет искать в обе стороны — как по имени, так и по телефону. Естественно, будут различаться и запросы. Чтобы указать алгоритму способ поиска, нам потребуется перечисление:

public enum SearchMode {
Name,
Phone
}
Теперь можно написать и сам класс:
public class XmlQueryBuilder
{
private SpeakOut speakOut;
public XmlQueryBuilder() {}
public XmlQueryBuilder(SpeakOut speakOut)
{
this.speakOut = speakOut;
}
// virtual >> Вдруг когда-нибудь мне потребуется расширить функциональность этого метода
public virtual string Transfer Query(string searchString, SearchMode searchMode)
{
string QueryString = "";
if(this.speakOut != null) {
this.speakOut("Построение запроса");
}
switch(searchMode) {
case SearchMode.Name:
QueryString = "//name[. = '" + searchString + "']/parent::node()/phone";
break;
case SearchMode.Phone:
QueryString = "//phone[. = '" + searchString + "']/parent::node()/name";
break;
}
if(this.speakOut != null)
this.speakOut("Получение данных");
return this.tService.GetPhoneData (QueryString);
}
}

Перед тем как ссылаться на TQueryBuilder, компилируем библиотеку. Реализация windows- и web-клиента будет зависеть от ваших пристрастий. Вы можете вообще ограничиться консольным приложением. Но я опишу, как это делал я, а вы, уловив основную идею, можете поступить на свое усмотрение.

Windows-клиент
Пользуясь моментом, заодно изложу вам некоторые моменты, которые следует учитывать при дизайне интерфейса. Во-первых, текст на форме должен читаться одним предложением, т.е. слова должны быть взаимосвязаны между собой. Посмотрите, как сделал я: Поиск … по номеру телефона … по имени. Представьте, если бы было написано просто: номер телефона, имя. Откуда, что и зачем — не понятно. Согласен, можно написать что-нибудь в поле ввода и посмотреть, что выдаст программа. Но в моем случае все интуитивно понятно без лишних движений.

Во-вторых, любое мало-мальски приличное приложение должно иметь строку состояния с прогрессбаром. Конечно, в игре Солитер прогрессбар будет ни к селу ни к городу. Но если какая-нибудь операция требует длительного выполнения, то прогрессбар просто обязан присутствовать. Присутствие прогрессбара в строке состояния для программиста не вопрос. Самым трудно выполнимым условием является равномерность его заполнения. Т.е. не должно быть так, что 90% заполняется в первые две секунды, а остальных 10% приходится ждать полчаса. Совсем недавно на форуме GotDotNet.Ru человек задал вопрос, как ему отобразить процесс загрузки базы данных. Никто не откликнулся, хотя, может быть, ему еще кто-нибудь и поможет. В нашем случае я тоже не представляю себе, как можно сделать так, чтобы прогрессбар заполнялся равномерно. Производительность машин разная, да и загрузка другими процессами отражается на скорости выполнения. Вопрос остается открытым. Лучше, наверное, просто пускать бегущих змеек, как при загрузке Windows XP. Заметьте, в своем приложении я вынес строку состояния на форму — зато у вас будет повод упрекнуть меня в том, что я ничего не понимаю в дизайне UI.

Хочу отметить еще одну функциональную нагрузку прогрессбаров и строк состояния: они субъективно повышают скорость работы приложения. Вы думаете, почему кажется, что в Опере страницы грузятся быстрее? Посмотрите на ее строку состояния: один прогрессбар отображает процент загрузки страницы, второй — прокачанный размер информации. Плюс ко всему есть еще и 3 текстовых поля: количество загруженных картинок, время, прошедшее с начала загрузки, текущее действие. Все движется, изменяется, и создается ощущение быстрой загрузки (в некоторых случаях скорость действительно выше, но таким образом пользователю не надо ничего доказывать, стоя с секундомером, — он сам все видит).
Субъективное удовлетворение от общения с программой включает в себя множество факторов, и одним из них является… что бы вы думали? Внешний вид интерфейса! Хоть мне *** и говорил на форуме газеты, что на свой браузер он не любуется, а работает с ним, в конце концов проговорился, что переливающаяся ящерка радует ему глаз. Значит, все-таки, кто бы там что ни говорил, приведенный выше фактор имеет место и его нельзя сбрасывать со счетов.

У меня как раз на винчестере завалялись контролы под Windows XP (Michael Dobler, http://www.windowsforms.net, раздел code heroes). Когда писал этот пример, я вспомнил про них и не преминул посмотреть, что они из себя представляют. Признайтесь, с какой программой вы бы предпочли работать, если бы при прочих равных условиях у одной из них интерфейс был приятным и радовал глаз, а у другой — скучным и невыразительным (предполагая встречный вопрос, заявлю — кряки под рукой:-)?) Согласен, на вкус и цвет товарища нет, но есть выход и из этого положения — скины. Опять приведу пример Оперы: все-таки уж очень грамотно работают норвежские ребята. Зайдите на их сайт — там просто уйма разнообразных скинов, панелек и пр. Все сделано для того, чтобы вы купили эту программу. А то, что по статистике Опера уступает Мозилле, так это не показатель: многие заставили свой браузер идентифицироваться как IE, отсюда и заоблачные 90 с лишним процентов (или я уже отстал от жизни?).
Ну да ладно. У меня в запасе всего чуть более 3000 знаков, а надо еще вам рассказать, как же все-таки довести наше приложение до победного конца.

Добавляем в решение TDirectory новый Windows-проект TWinClient. Ссылаемся на библиотеку TQueryBuilder. Описываем пространство имен: using qBuilder = Nesterov.TDirecotry.TQueryBuilder;. На форме располагаем два текстбокса, две кнопки, ListView и ListBox, который мы будем использовать как лог. Из сводной таблички можно узнать, как я назвал контролы, расположенные у меня на форме (где-то прочитал, что сейчас уже не модно называть элементы управления на манер txtPhone, btnName):

TextBox PhoneTextBox
TextBox NameTextBox
Button — goPhone
Button — goName
ListView resultsList
ListBox — log

Комментировать следующий код особо нечего: он до безобразия прост. Я приведу только код обработчика кнопки goPhone, а вы по аналогии напишите недостающий код и для goName:

private qBuilder.XmlQueryBuilder xmlQBuilder;
public MainForm()
{
InitializeComponent();
// Это колонки ListView. Устанавливаем их ширину так, чтобы при появлении вертикальной полосы прокрутки не появлялось горизонтальной. При дизайне интерфейса держите в голове и этот факт: пользователи ненавидят горизонтальные полосы прокрутки.
this.NameHeader.Width = this.results List.Width / 2 — 10;
this.PhoneHeader.Width = this.results List.Width / 2 — 10;
this.xmlQBuilder = new qBuilder. XmlQueryBuilder(new qBuilder.SpeakOut(this.log.Items.Add));
}
// Необходимо для заполнения списка результатами поиска
string[] str = new string[2];
private void goPhone_Click(object sender, System.EventArgs e)
{
try {
// В обработчике событий goName не забудьте поменять местами str[1] и str[0], ну и элемент перечисления используйте соответствующий
str[0] = xmlQBuilder.TransferQuery (this.PhoneTextBox.Text, qBuilder.SearchMode.Phone);
str[1] = this.PhoneTextBox.Text;
this.resultsList.Items.Add(new ListViewItem(str));
} catch(Exception ex) {
this.log.Items.Add(ex.Message);
}
}

И помните, что если у вас что-нибудь изменится в библиотеке TQueryBuilder (скажем, тип возвращаемого значения у метода XmlQueryBuilder), то, перекомпилировав библиотеку, обновите проект (кнопочка Refresh), ссылающийся на эту библиотеку (в нашем случае это TWinClient и TWebClient).

Web-клиент
Реализация веб-клиента абсолютно идентична. Добавляете новый проект ASP.NET Web Application. Ссылаетесь на проект TQueryBuilder. На форму бросаете те же текстбоксы, кнопки, списки. В данном случае я для вывода результатов ограничился обычным ListBox, т.к. заморачиваться с DataGrid не хотел, а искать (либо писать самому) подходящий контрол — тем более. Имя и номер телефона я отделял " :: ", т.е. пишите что-то вроде этого: this.resultsList.Items.Add(str[0] + " :: " + str[1]);. Да, и при объявлении экземпляра класса TQueryBuilder ограничьтесь конструктором без параметров. Дело в том, что кому-то пришло в голову сделать различной сигнатуру методов Add у windows и web ListBox'ов. В связи с этим номер с универсальным делегатом не проходит. Я пока не нашел красивого решения. Хотя на помощь всегда может прийти TVPattern — самое красивое решение в стиле ООП.
Наконец компилируем всю сборку, исправляем возможные ошибки (нет, не мои, а ваши — мой код 100% рабочий), выставляем проект TWinClient как StartUp. Пуск!



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

полезные ссылки
Корпусные камеры видеонаблюдения