Создание приложений с применением COM+. Часть 2
Управление распределенными транзакциями
Механизм уведомления о событиях в службах компонентов
Тестирование уведомлений о событиях
Эта часть статьи посвящена созданию и применению COM-серверов, используемых совместно с Microsoft Component Services, называемых иногда в отечественной литературе службами компонентов (само же расширение COM, позволяющее создавать приложения, использующие Component Services, получило название COM+). В первой части данной статьи (см. CD-ROM, прилагаемый к КомпьютерПресс № 4'2002) мы рассмотрели назначение служб компонентов Windows 2000 и особенности объектов COM+, а также создали пример простейшего объекта COM+, осуществляющего доступ к данным. В предлагаемой вашему вниманию второй части данной статьи мы расскажем о реализации распределенных транзакций и об обработке событий, возникающих в объектах COM+.
Управление транзакциями
тобы реализовать комплексную (в общем случае — распределенную) транзакцию, обычно требуется создание нескольких объектов COM+, а также организация их обращений друг к другу.
Реализация транзакций
Создадим пример приложения COM+, реализующий такую транзакцию, для чего добавим еще два объекта к уже имеющемуся. Первый из объектов будет обладать единственным методом, добавляющим запись в таблицу, в которой регистрируются заказы (назовем ее OrderedProducts, и, прежде чем создавать сам объект, создадим и эту таблицу). По своей реализации данный объект похож на объект Stock_Data, созанный нами ранее. Во втором из объектов мы реализуем комплексную транзакцию, которая будет состоять из следующих манипуляций:
- Добавление в таблицу OrderedProducts новой записи, содержащей наименование и количество единиц заказанного товара, а также сведения о заказчике, полученные из клиентского приложения.
- Уменьшение значения поля UnitsInStock выбранной записи таблицы Products на целое число, переданное из вызывающего приложения.
- Увеличение значения поля UnitsOnOrder той же самой записи таблицы Products на ту же величину.
Взаимодействие частей этого приложения схематически изображено на рис. 1.
Для реализации подобной транзакции один из объектов должен обращаться к двум дочерним объектам. Первый из них — stock.Stock_Data, реализующий второе и третье действие из приведенного списка, нами уже создан. Приступим к созданию объекта, реализующего первое действие. Но перед этим, как было обещано, создадим в базе данных Northwind таблицу OrderedProducts, выполнив следующее SQL-предложение:
CREATE TABLE [dbo].[OrderedProducts] ( [Ord_ID] [int] IDENTITY (1, 1) NOT NULL, [Address] [char] (40) NULL, [OrderedItem] [char] (50) NULL, [UnitPrice] [money] NULL, [Quantity] [int] NULL ) ON [PRIMARY]
Обратите внимание: в этой таблице мы создали поле Ord_Id типа Identity (в Microsoft SQL Server так называются поля, которым при создании новой записи значение присваивается автоматически; обычно данные такого типа используются в первичных ключах таблиц этой СУБД). Чуть позже с помощью этого поля мы продемонстрируем, что именно происходит в базе данных при отмене транзакции. Значения полей Address и Quantity будут получены из клиенского приложения, а полей Ordered_Item и UnitPrice — из таблицы Products.
Создав таблицу, займемся созданием объекта COM+. Используя уже описанную ранее последовательность действий, создадим библиотеку ORDERS.DLL, которая будет содержать объект COM+ с именем Orders_Data и значением свойства Transaction model, равным Requires a transaction.
В модуль данных, который при этом будет создан, поместим точно такой же набор компонентов, что был в предыдущем объекте, с точно такими же значениями свойств (для этой цели можно создать соответствующий шаблон компонентов). Далее откроем библиотеку типов созданного объекта и добавим к ней два метода.
Первый из этих методов, который мы назовем Add_Order, будет иметь такие параметры:
- Addr типа WideString — адрес заказчика, полученный из клиентского приложения.
- OrderedItem типа WideString — наименование заказанного товара.
- UnitPrice типа Currency — цена единицы заказанного товара.
- Quantity типа Integer — количество единиц заказанного товара.
Реализация указанного метода имеет следующий вид:
procedure TOrders_Data.Add_Order(const Addr, OrderedItem: WideString; UnitPrice: Currency; Quantity: Integer); begin try //Соединяемся с базой данных ADOCOnnection1.Open; //Текст запроса к базе данных ADOCommand1.CommandText := 'INSERT INTO OrderedProducts ' + ' VALUES(''' + Addr + ''', ''' + OrderedItem+''', ' + FloatToStr(UnitPrice) + ', ' + IntToStr(Quantity) + ')'; //Выполняем запрос ADOCommand1.Execute; //Разрываем соединение с базой данных ADOConnection1.Close; //Информируем службы компонентов об успешном //выполнении запроса SetComplete; except //Разрываем соединение с базой данных ADOCOnnection1.Close; //Информируем службы компонентов о неудачной попытке //выполнения запроса SetAbort; //Передаем исключение вызывающему приложению или объекту raise; end; end;
Второй метод — Get_Order_List — возвращает набор данных, являющийся результатом запроса к таблице OrderedProducts:
function TOrders_Data.Get_Order_List: Recordset; var QRY : string; begin //Текст запроса QRY := 'SELECT * FROM OrderedProducts ORDER BY Ord_ID'; try //Создаем набор данных Result := CoRecordset.Create; Result.CursorLocation := adUseClient; //Открываем его Result.Open(QRY,ADOConnection1.ConnectionString, adOpenStatic,adLockBatchOptimistic, adCmdText); //Разрываем соединение с базой данных ADOCOnnection1.Close; //Информируем службы компонентов об успешном //выполнении запроса SetComplete; except ADOConnection1.Close; //Информируем службы компонентов о неудачной попытке //выполнения запроса SetAbort; //Передаем исключение вызывающему приложению raise; end; end;
Как и в предыдущем случае, нам следует сослаться в секции uses на модули ADODB, ADODb_TLB, ADOR_TLB.
После компиляции и сохранения проекта скопируем созданную нами библиотеку ORDERS.DLL на серверный компьютер и зарегистрируем объект Orders_Data в службах компонентов в том же приложении COMPlus_Demo, что и предыдущий компонент.
Создав два объекта COM+, манипулирующих двумя таблицами, приступим к созданию третьего объекта, выступающего в роли родительского по отношению к уже созданным двум объектам. Для этого создадим новую библиотеку ActiveX (назовем ее PROC), а в ней — новый объект COM+ (назовем его Processing). В отличие от двух предыдущих, этот объект будет наследником не класса TMtsDataModule, а класса TMtsAutoObject (для его создания следует выбрать значок Transactional Object на странице ActiveX окна репозитария объектов), поскольку данный объект COM+ не будет содержать никаких компонентов доступа к данным.
При создании указанного объекта нам следует выбрать Requires a new transaction в качестве значения свойства Transaction model. Это означает, что при обращении к данному объекту для него создается собственный контекст транзакции и что он не может выполняться в контексте транзакции другого объекта. Напомним, что два ранее созданных объекта имели другое значение этого параметра — Requires a transaction, что позволяло выполнять их в контексте транзакции объектов, которые к ним обращаются.
Добавим к библиотеке типов вновь созданного объекта три метода. Два из них — Get_OrderList и Get_ProductList — возвращают наборы данных, являющиеся результатами запросов к таблицам OrderedProducts и Products соответственно. Как и в ранее созданных объектах, оба метода проще всего создать, описав два свойства с атрибутами «только для чтения»; назначение этих методов — позволить пользователю визуально контролировать, что происходит с данными. Реализация этих методов имеет вид:
function TProcess.Get_Order_List: Recordset; begin try //Создаем экземпляр контекста объекта Orders_Data OleCheck(ObjectContext.CreateInstance(CLASS_Orders_Data, IOrders_Data, FOrders_Data)); //Создаем набор данных Result := CoRecordset.Create; Result.CursorLocation := adUseClient; //Получаем данные от объекта Orders_Data Result := FOrders_Data.Get_Order_List; //Информируем службы компонентов об успешном //выполнении запроса SetComplete; except //Информируем службы компонентов о неудачной попытке //выполнения запроса SetAbort; //Передаем исключение вызывающему приложению raise; end; end; function TProcess.Get_Product_List: Recordset; begin try //Создаем экземпляр контекста объекта Stock_Data OleCheck(ObjectContext.CreateInstance(CLASS_Stock_Data, IStock_Data, FStock_Data)); //Создаем набор данных Result := CoRecordset.Create; Result.CursorLocation := adUseClient; //Получаем данные от объекта Stock_Data Result := FStock_Data.Get_ProductList; //Информируем службы компонентов об успешном //выполнении запроса SetComplete; except //Информируем службы компонентов о неудачной попытке //выполнения запроса SetAbort; //Передаем исключение вызывающему приложению raise; end; end;
В приведенном выше фрагменте кода мы используем метод CreateInstance контекста вызываемого объекта вместо создания самого объекта. Это позволяет нам использовать объекты именно тогда, когда они действительно нужны, и тем самым сэкономить ресурсы операционной системы. Чтобы заставить эти методы работать корректно, следует описать переменные, содержащие ссылки на вызываемые объекты:
private FOrders_Data: IOrders_Data ; FStock_Data: IStock_Data ;
Помимо этого следует сослаться на библиотеки типов ранее созданных объектов. Проще всего скопировать файлы PROC.TLB, PROC_TLB.PAS, STOCK.TLB и STOCK_TLB.PAS в каталог с текущим проектом и включить ссылку на модули PROC_TLB.PAS и STOCK_TLB.PAS в предложение uses, наряду со ссылками на модули ADODB, ADODB_TLB, ADOR_TLB.
Третий метод — Process_Order — будет реализовывать транзакцию, которая одновременно модифицирует обе таблицы: Product и OrderedProduct (или отменяет все изменения, если во время ее работы произошло исключение). Параметры этого метода будут следующими:
- Prod_ID типа Integer — первичный ключ модифицируемой записи таблицы Products.
- Prod_Name типа WideString — наименование заказанного товара.
- UnitPrice типа Currency — цена единицы заказанного товара.
- Quantity типа Integer — количество единиц заказанного товара.
- Addr типа WideString — адрес заказчика, полученный из клиентского приложения.
Реализация метода ProcessOrder имеет такой вид:
procedure TProcess.Process_Order(Prod_ID: Integer; const Prod_Name: WideString; UnitPrice: Currency; Quantity: Integer; const Addr: WideString); begin try //Создаем контекст объекта Stock_Data OleCheck(ObjectContext.CreateInstance(CLASS_Stock_Data, IStock_Data, FStock_Data)); //Создаем контекст объекта Orders_Data OleCheck(ObjectContext.CreateInstance(CLASS_Orders_Data, IOrders_Data, FOrders_Data)); //Добавляем запись к таблице OrderedProducts FOrders_Data.Add_Order(Addr, Prod_Name,UnitPrice,Quantity); //Уменьшаем значение поля UnitsInStock FStock_Data.Dec_UnitsInStock(Prod_ID,Quantity); //Увеличиваем значение UnitsOnOrder FStock_Data.Inc_UnitsOnOrder(Prod_ID,Quantity); //Сообщаем службам компонентов, //что транзакция может быть завершена EnableCommit; except //Сообщаем службам компонентов, //что транзакция должна быть отменена DisableCommit; //Передаем клиенту исключение raise; end; end;
Некоторые из строк приведенного выше фрагмента кода требуют дополнительных пояснений. После создания контекстов дочерних объектов мы вызываем их методы, причем наиболее «опасным» с точки зрения возникновения исключения является следующий вызов:
FStock_Data.Dec_UnitsInStock(Prod_ID,Quantity);
Причина этого заключается в возможном нарушении ограничения на значение поля UnitsInStock таблицы Products: оно всегда должно быть неотрицательным (это ведь число единиц товара на складе), а в методе из него вычитается целое число. При возникновении связанного с этим исключения результат выполнения предыдущего метода должен быть аннулирован, и чуть позже мы в этом убедимся.
Сохранив и скомпилировав проект, скопируем созданную библиотеку proc.dll на серверный компьютер и зарегистрируем в службах компонентов в том же приложении COMPlus_Demo, что и предыдущие два компонента (рис. 2).
Теперь нам следует создать клиентское приложение для проверки того, как осуществляется завершение и откат транзакции, реализованной в созданном объекте. Это мы рассмотрим в следующем разделе.
Тестирование транзакций
Создадим новый проект клиентского приложения. На его главную форму поместим компонент TRDSConnection и два компонента TADODataSet. Свойство ServerName компонента RDSConnection1 установим равным программному идентификатору (ProgID) третьего из созданных нами компонентов (в данном случае — proc.Process), а свойство ComputerName установим равным имени серверного компьютера. Как и в случае предыдущего клиентского приложения, никаких свойств компонентов TADODataSet устанавливать не нужно, поскольку на этапе выполнения эти компоненты получат наборы данных от объекта Process. Чтобы пользователь мог увидеть их содержимое, поместим на форму по два компонента TDataSource и TDBGrid и свяжем их с соответствующим компонентом TADODataSet. Наконец, поместим на форму два компонента TEdit для ввода количества единиц заказанного товара и адреса клиента, а также две кнопки, инициирующие вызовы методов объекта Process.
Создадим обработчики события OnClick этих кнопок. Первый из них инициирует получение и отображение двух наборов данных, предоставленных объектом Process, а именно результатов запросов к таблицам Products и OrderedProducts:
procedure TForm1.Button1Click(Sender: TObject); begin try //Инициируем создание контекста объекта Process RDSConnection1.Connected := True; //Получаем список заказов RS := RDSConnection1.GetRecordset('Order_List',''); ADODataSet2.Recordset := RS; ADODataSet2.Open; //Получаем список товаров RS := RDSConnection1.GetRecordset('Product_List',''); ADODataSet1.Recordset := RS; ADODataSet1.Open; //Можно принимать заказы Button2.Enabled := True; except //Некоторые из объектов недоступны ShowMessage('Данные не получены'); Button2.Enabled := False; end; RDSConnection1.Connected:=False; end;
Для того чтобы приведенный выше фрагмент кода был работоспособен, необходимо сослаться на модуль ADODB в предложении uses и добавить описание переменной RS:
var Form1 : TForm1; RS :_Recordset; Второй обработчик события инициирует транзакцию, вызывая метод ProcessOrder: procedure TForm1.Button2Click(Sender: TObject); begin try //Инициируем создание контекста объекта Process RDSConnection1.Connected := True; //Вызываем его метод Process_Order RDSConnection1.AppServer.Process_Order( ADODataSet1.FieldByName('ProductID').AsInteger, ADODataSet1.FieldByName('ProductName').AsString, ADODataSet1.FieldByName('UnitPrice').AsFloat, StrToInt(Edit1.Text),Edit2.Text); //Обновляем данные Button1Click(self); except //В одном из серверных объектов возникло исключение ShowMessage('Заказ не принят'); Button2.Enabled := False; end; RDSConnection1.Connected := False; end;
Скомпилируем и сохраним проект, а затем скопируем полученное приложение на клиентский компьютер.
Выполним клиентское приложение. После нажатия первой кнопки данные из таблиц Products и OrderedProducts будут отображены в компонентах TDBGrid. Далее мы можем выбрать строку в наборе данных из таблицы Products, ввести адрес и количество заказанного товара в соответствующих компонентах TEdit и нажать вторую кнопку, что приведет к выполнению транзакции. Если количество заказанных единиц товара не превышает значения поля UnitsInStock в выбранной записи, после обновления данных мы получим следующие результаты:
- Значение поля UnitsInStock будет уменьшено на величину, введенную в поле Количество.
- Значение поля UnitsOnOrder будет увеличено на величину, введенную в поле Количество.
- В таблице OrderedProducts появится новая запись.
Следует отметить, что если при выполнении транзакции не произойдет исключений, значение поля Ord_ID новой записи, которая появится в поле Ord_Id таблицы OrderedProducts, будет равно значению того же поля предыдущей записи плюс 1 (рис. 3), однако при возникновении исключения ситуация будет иной. Попробуем ввести в поле Количество число, превышающее значение в поле UnitsInStock (то есть заказать товаров больше, чем есть на складе). В этом случае сначала объект Oders_Data добавит запись в таблицу OrderedProducts, а затем объект Stock_Data начнет выполнять свой метод Dec_UnitsInStock, и в нем произойдет исключение, связанное с нарушением имеющегося в базе данных ограничения на значение этого поля (так как на складе не должно быть отрицательного числа единиц товара). Вследствие этого произойдет откат транзакции, и вновь созданная запись, естественно, будет удалена из таблицы, но при этом текущее значение поля Ord_Id сохранится (напомним, что это поле типа Identity и его значения генерируются автоматически). Если затем с помощью того же приложения мы попытаемся обработать другой заказ, удовлетворяющий ограничениям на значение поля UnitsInStock, мы обнаружим, что в значениях поля Ord_Id появились пропуски — запись с предыдущим значением этого поля в таблице OrderedProducts отсутствует. Следовательно, откат предыдущей транзакции действительно произошел.
Конечно, может возникнуть резонный вопрос: зачем нужно было создавать эти три объекта, когда можно просто использовать механизм поддержки транзакций, предоставляемый самим сервером баз данных? Действительно, в случае одной и той же базы данных (или даже разных баз данных, управляемых одинаковыми серверами) это может и не имееть особого значения. Однако при использовании серверов баз данных в этом есть прямой смысл: механизмы поддержки транзакций серверов баз данных обычно не поддерживают распределенные транзакции с участием СУБД других производителей, а с помощью служб компонентов их все-таки можно осуществить. Наш следующий пример продемонстрирует, как это сделать.
Управление распределенными транзакциями
Для иллюстрации реализации распределенных транзакций с помощью служб компонентов мы слегка модифицируем наше приложение. Теперь мы будем использовать две СУБД — Microsoft SQL Server 2000 и Oracle 8.0.4.
Отметим, что использование баз данных Oracle в распределенных транзакциях, основанных на применении объектов COM+, возможно начиная с Oracle 7.3.3. Именно эта версия Oracle и является версией по умолчанию, описанной в ключе реестра:
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\MSDTC\MTxOCI
По умолчанию значения этого ключа таковы:
OracleOciLib = "ociw32.dll" OracleXaLib = "xa73.dll" OracleSqlLib = "sqllib18.dll"
В случае применения Oracle8 следует внести изменения в эти значения:
OracleOciLib = "ociw32.dll" OracleXaLib = "xa80.dll" OracleSqlLib = "sqllib80.dll"
Следует также иметь в виду, что распределенные транзакции с участием Oracle возможны при применении ODBC-драйвера, входящего в комплект Microsoft Data Access Components, а не в комплект поставки Oracle 8.
В случае применения Oracle8i, помимо модификации ключей реестра, необходимо создать дополнительный сервис, предназначенный для поддержек распределенных транзакций. Интересующиеся этим вопросом могут обратиться к разделу документации Oracle «Using Microsoft Transaction Server with Oracle8i».
После этого краткого введения рассмотрим, как можно модифицировать приведенный пример для реализации в нем распределенной транзакции (описанные ниже действия соответствуют применению Microsoft SQL Server 2000, Oracle 8.0.4, ODBC-драйверов из состава Microsoft Data Access Components и OLE DB Provider for ODBC drivers).
Для начала скопируем таблицу Products из базы данных Northwind в базу данных Oracle (в нашем примере это стандартная база данных, которую можно сгенерировать при установке Oracle). Сделать это можно, например, с помощью служб трансформации данных (Microsoft Data Transformation services). При переносе данных лучше переименовать таблицу Products в PRODUCTS, чтобы не модифицировать тексты запросов в коде объекта Stock_Data.
Кроме того, нам следует создать для указанной таблицы серверное ограничение, аналогичное ограничению, имевшемуся в базе данных Northwind. Для этого с помощью утилиты SQL Plus нужно выполнить следующее SQL-предложение:
ALTER TABLE SCOTT.PRODUCTS ADD CONSTRAINT PRODUCTSCHECKCONSTRAINT1 CHECK (UNITSINSTOCK>-1)
Далее следует описать ODBC-источник для базы данных Oracle (с помощью приложения Data Sources раздела Administrative Tools панели управления Windows). Назовем его orcl_odbc.
Теперь нужно изменить наш объект Stock_Data. Все модификации заключаются в изменении свойства ConnectionString компонента ADOConnection1:
Provider=MSDASQL.1; Password=MANAGER; Persist Security Info=True; User ID=SYSTEM; Data Source=orcl_odbc
И наконец, можно поместить новую версию библиотеки STOCK.DLL на серверный компьютер и запустить клиентское приложение. Мы можем убедиться, что и в этом случае распределенные транзакции обрабатываются корректно. В частности, нарушение серверного ограничения, описанного в Oracle, как и в предыдущем случае, приводит к откату транзакции в Microsoft SQL Server, что подтверждается появлением «пропусков» в значениях первичного ключа таблицы OrderedProducts.
Таким образом, с помощью служб компонентов мы можем организовать распределенную транзакцию, в которой участвуют базы данных, управляемые СУБД различных производителей.
Применение событий COM+
Механизм уведомления о событиях в службах компонентов
Говоря о COM+, нельзя не сказать о механизме обработки событий, возникающих в таких объектах. В отличие от событий COM, где за уведомлением следит сам сервер, в COM+ за событиями и уведомлением о них объектов-подписчиков следят службы компонентов. При этом для генерации событий создается отдельный объект — издатель (publisher), к которому производится обращение в момент наступления события. После этого службы компонентов обращаются ко всем клиентам, подписанным на данное событие.
Объекты-издатели событий COM+ не должны содержать реализации своих интерфейсов — она содержится не в серверном объекте, а в клиентском приложении (в подписчике на данное событие). Поэтому после описания интерфейсов компонент компилируется и регистрируется в службах компонентов. При наступлении события в объекте COM+ следует инициировать создание объекта, описывающего события COM+, и вызвать метод, соответствующий данному событию. В объекте-подписчике (это обычный COM-объект) обычно реализуется интерфейс объекта-издателя и его методы — в данном случае реализация методов играет роль обработчиков этих событий. Отметим, что и объект-издатель, и объект-подписчик должны быть зарегистрированы в одном и том же приложении COM+.
Событием в объекте COM+ может быть наступление любого факта (например, в рассмотренном выше примере событиями могут быть появление сотого заказа, заказ какого-то конкретного товара, попытка заказа товара в большем количестве, чем есть на складе, и т.д.). В этом случае в коде объекта, инициирующего событие (в рассмотренном выше примере — в коде объекта Process) должен присутствовать код, который реализует создание экземпляра объекта событий и вызова метода, соответствующего этому событию. Например, если объект событий называется MyEvent, интерфейс объекта событий — IMyEvent, а метод, уведомляющий о событиях, — OrdMessage обладает одним строковым параметром, то фрагмент кода, инициирующий создание события, связанного с попыткой заказа товара в количестве, превышающем имеющееся на складе, может выглядеть, например, так:
type FMyEvent: IMyEvent ; ... procedure TProcess.Process_Order(Prod_ID: Integer; const Prod_Name: WideString; UnitPrice: Currency; Quantity: Integer; const Addr: WideString); begin try //Создаем контекст объекта Stock_Data OleCheck(ObjectContext.CreateInstance(CLASS_Stock_Data, IStock_Data, FStock_Data)); //Создаем контекст объекта Orders_Data OleCheck(ObjectContext.CreateInstance(CLASS_Orders_Data, IOrders_Data, FOrders_Data)); //Добавляем запись к таблице OrderedProducts FOrders_Data.Add_Order(Addr, Prod_Name,UnitPrice,Quantity); //Уменьшаем значение поля UnitsInStock FStock_Data.Dec_UnitsInStock(Prod_ID,Quantity); //Увеличиваем значение UnitsOnOrder FStock_Data.Inc_UnitsOnOrder(Prod_ID,Quantity); //Сообщаем службам компонентов, //что транзакция может быть завершена EnableCommit; except //Информируем службы компонентов о неудачной попытке //выполнения транзакции и генерируем событие, о котором //COM+ уведомит приложение, отвечающее за доставку //товара на склад OleCheck(ObjectContext.CreateInstance(CLASS_MyEvent, IMyEvent, FMyEvent)); FMyEvent.OrdMessage ('Товар ' + Prod_Name + 'нужен в количестве ' + IntToStr(Quantity)+ ' упаковок'); DisableCommit; //Передаем клиенту исключение raise; end; end;
Отметим, что экземпляр объекта событий может быть создан и из клиентского приложения.
Создание объекта-издателя
Создадим простейший пример, иллюстрирующий применение механизма обработки событий COM+. Для его создания требуется установить первый и второй пакеты обновления (update packs) Delphi 6 (эти пакеты обновления доступны зарегистрированным пользователям Delphi на Web-сайте компании Borland Software Corporation), поскольку исходная версия Delphi 6 содержала ряд ошибок, связанных с генерацией библиотек типов объектов событий и исправленных во втором пакете обновления.
Наш пример будет состоять из трех частей: объекта-издателя, генерирующего одно событие с одним строковым параметром, объекта-подписчика данного события (он будет просто выводить диалоговое окно с содержимым этого параметра) и клиентского приложения, инициирующего событие.
Начнем с создания объекта-издателя. С этой целью создадим новую библиотеку ActiveX, а в ней — объект события с помощью выбора значка COM+ Event Object на вкладке ActiveX окна репозитария объектов (присвоим событию имя EVT, а самому объекту — EVT_events). В результате будет сгенерирована библиотека типов, содержащая интерфейс IEVT, к которому можно добавлять методы, соответствующие различным возможным событиям.
Методов у интерфейса объекта-издателя может быть несколько. Обязательное требование к этим методам — они должны возвращать значение HRESULT, а их параметры не должны иметь модификаторов. Пример добавления такого метода изображен на рис. 4.
Отметим, что при разработке объекта событий не следует создавать реализацию методов его интерфейса. Эти методы должны быть реализованы в объекте-подписчике, где данная реализация будет играть роль обработчиков событий. Поэтому после описания всех методов можно скомпилировать созданную библиотеку и перенести ее на серверный компьютер.
Далее объект события следует зарегистрировать в службах компонентов. Рекомендуется делать это не средствами Delphi, а средствами Component Services Explorer. Для этого создадим в службах компонентов новое приложение COM+ (назовем его event_demo) и выберем из контекстного меню раздела Components этого приложения опцию New | Component. После этого будет запущен мастер COM Component Install Wizard, в котором нужно нажать на кнопку рядом с надписью Install new event classes (рис. 5).
Затем следует выбрать файл EVT_event.dll, а потом те интерфейсы или отдельные события, на которые могут быть подписаны будущие подписчики (в данном случае это единственное событие — Event1, рис. 6).
Таким образом, мы зарегистрировали объект-издатель в службах компонентов. Нашей следующей задачей будет создание объекта-подписчика.
Создание объекта-подписчика
Объект-подписчик представляет собой обычный внутрипроцессный COM-сервер, реализующий интерфейс объекта-издателя. Его создание мы начнем с генерации новой библиотеки ActiveX. Далее на странице ActiveX окна репозитария объектов выберем значок COM Object и в окне мастера COM Object Wizard укажем имя нового объекта (пусть он называется Subscrl). Поскольку нам следует реализовать готовый интерфейс, нажмем на кнопку List, потом в появившемся окне нажмем на кнопку Add Library, выберем созданную ранее библиотеку EVT_events.dll, а затем найдем и выберем в списке доступных интерфейсов интерфейс IEVT.
Теперь нам осталось реализовать метод Event1 данного интерфейса. В исходном интерфейсе этот метод, как и все методы объектов событий, объявлен виртуальным и абстрактным (это сделано для того, чтобы не позволить создать его реализацию в объекте событий). Поэтому первое, что следует сделать при создании кода объекта-подписчика, — удалить ключевые слова virtual и abstract из определения этого метода. Теперь можно установить курсор на строку с определением указанного метода и выбрать из контекстного меню редактора кода пункт Complete class at cursor.
Реализация метода в нашем примере будет достаточно проста: мы выведем диагностическое сообщение на экран серверного компьютера:
procedure TSubscr.Event1(const Event1_data: WideString); begin ShowMessage(Event1_data); end;
Нужно иметь в виду, что в общем случае действия, выполняемые подписчиком, могут быть практически любыми, причем вывод диагностического сообщения — далеко не самый лучший (точнее говоря — не самый правильный) способ обработки таких событий в реальных проектах.
Теперь нашу библиотеку можно сохранить (назовем ее subdll), скомпилировать, перенести на серверный компьютер и установить в то же самое приложение, что и предыдущий созданный объект.
Поскольку за уведомление подписчика о событии отвечают службы компонентов, их следует проинформировать о том, что данный подписчик в них нуждается. Делается это путем создания подписки (subscription). Создать подписку можно, выбрав из контекстного меню раздела Subscriptions компонент-подписчик (в данном случае subdll.Subscr) пункт New | Subscription. После этого будет запущен мастер COM Subscription Wizard, где мы выберем программный идентификатор объекта-издателя, о котором следует уведомлять наш объект-подписчик (рис. 7).
При описании свойств подписки можно указать, что она должна стать доступной немедленно. Кроме того, свойства подписки (например, правила фильтрации событий, помещение уведомлений в очередь сообщений и т.д.) можно изменить, выбрав пункт контекстного меню Properties соответствующего значка.
Состав созданного и сконфигурированного нами приложения COM+ изображен на рис. 8.
Далее нам следует протестировать работу созданного приложения.
Тестирование уведомлений о событиях
Теперь нам осталось проверить, как осуществляются уведомления о событиях в службах компонентов. Для этой цели создадим приложение, которое будет инициировать генерацию событий. Это обычное Wndows-приложение, которое будет создавать экземпляр объекта событий и вызывать его метод Events1.
Создадим новый проект и добавим на главную форму создаваемого приложения компоненты TButton, TEdit и TRDSConnection. Значение свойства ServerName компонента TRDSConnection установим равным имени объекта события — EVT_events.evt. В качестве свойства ComputerName укажем имя серверного компьютера (при совпадении серверного и клиентского компьютера можно оставить это свойство пустым).
Создадим обработчик события, связанного со щелчком по кнопке:
procedure TForm1.Button1Click(Sender: TObject); begin try RDSConnection1.Connected:=True; RDSConnection1.AppServer.Event1(Edit1.Text); except Showmessage(‘Событие не сгенерировано’); end; RDSConnection1.Connected:=False; end;
Теперь скомпилируем проект и запустим его на выполнение. Если ввести в компонент Edit1 строку и нажать на кнопку, будет создан экземпляр объекта-издателя и произведено обращение к его методу Event1, после чего службы компонентов создадут экземпляр объекта-подписчика и вызовут его метод с таким же именем и с тем же значением его параметра. В результате этого приложение dllhost.exe, в адресном пространстве которого выполняется подписчик, выведет на экран серверного компьютера диалоговое окно со строкой, введенной в клиентском приложении (рис. 9).
Итак, мы создали простейший объект событий и объект-подписчик, протестировали их совместную работу и убедились на этом примере, что механизм нотификаций COM+ довольно удобен для применения на практике.
Мы убедились, что службы компонентов позволяют осуществлять коллективное использование COM-объектов и ресурсов, организовывать авторизованный доступ к объектам, реализовывать распределенные транзакции, в том числе затрагивающие базы данных, управляемые серверами различных производителей. Мы также узнали, что службы компонентов сами следят за событиями и уведомлением о них клиентов-подписчиков и иницируют обработку событий этими клиентами. Все это позволяет создавать на основе служб компонентов распределенные приложения, реализующие указанную выше функциональность. При этом затраты, связанные с подобными разработками, будут минимальными.
КомпьютерПресс 5'2002