Нотификации в сервере OLE автомации

Сергей Трепалин

При анонсировании протокола OLE автомации в 1995г. нотификации – или передача сообщений от сервера к клиенту, не предусматривались. Например, если на сервере OLE автомации изменились данные, то клиент мог как угодно часто опрашивать сервер OLE автомации, изменились ли данные, но сервер не мог сообщить клиенту об этом. Соответственно, частый опрос сервера приводил к резкому возрастанию трафику сети (если сервер сетевой) или к напрасной трате времени и ресурсов приложения для опроса сервера. Соответственно, многих разработчиков такая ситуация не устраивала и это привело к разработке ряда оригинальных методик – например WOSA/XRT. В этой методике сервер экспонировал OLE интерфейс IDataObject, а клиент создавал IAdviseSink. IAdviseSink можно связать с IDataObject, и если с данными происходят какие-либо изменения, сервер уведомлял об этом клиента.

Для COM обьектов (к которым относится и сервер OLE автомации) нотификация осуществляется посредством использования относительно нового интерфейса IConnectionPoint. Однако, для in-process серверов OLE автомации (работающих в адресном пространстве клиента, DLL) нотификацию можно реализовать достаточно просто. Основная идея заключается в том, что если передать указатель серверу от клиента, то этот указатель будет действительным, то есть он реально будет указывать на какой-либо объект в памяти. В частности, этот объект может быть и адрес метода, который следует вызвать для уведомления клиента. При этом надо осознавать, что метод класса характеризуется двумя адресами – адрес метода и адрес данных.

В Delphi4 Inprise реализовала механизм поддержки нотификаций в сервере OLE автомации через IConnectionPoint. При выполнении команды File/New/ActiveX/Automation Object в возникающем диалоге, в отличии от Delphi3 и C++Builder, появляется возможность сообщить, что желательно создать сервер с поддержкой сообщений. Это достигается установкой метки на контроле Event support. Однако, примеры использования нотификаций Inprise не предоставила (Delphi 4.0 клиент-серверная версия 5.37), хотя они и были обещаны в файлах помощи. Поэтому, хотя метод получения нотификаций, быдет реализован полностью в приведенном ниже разборе материала, его не следует считать оптимальным.

После того, как отработает эксперт по созданию сервера OLE автомации с включенным значением Event support, в библиотеке типов появляются два интерфейса – один типа IDispatch, другой – dispinterface. К интерфейсу IDispatch следует добавлять свойства и методы, которые будут вызываться с клиента OLE автомации. А вот к dispinterface следует добавлять события, которые будут вызываться с сервера и передаваться клиенту OLE автомации. В модуле реализации библиотеки типов произошли следующие изменения, по сравнению с модулем реализации OLE сервера автомации без поддержки нотификаций:

1. Класс TAutoObject экспонирует теперь интерфейс IConnectionPointContainer.

2. Добавляется FEvents – указатель на интерфейс, передающий нотификации клиенту. Этот интерфейс не создается в сервере OLE автомации: он создается на клиенте OLE автомации и ссылка на него передается серверу.

3. Добавляется обьект FConnectionPoint, в котором реализован IConnectionPointContainer. Он создается при инициализации сервера автомации.

4. Добавляется метод EventSinkChanged. Этот метод будет вызываться всякий раз, когда будет передаваться интерфейс IDispatch от клиента для приема нотификаций. В частности, при разрушении клиента будет передаваться nil указатель.

5. При инициализации сервера создается интерфейс IConnectionPoint, который используется для связи с клиентом.

Как уже говорилось, к dispinterface следует добавлять методы, которые будут вызываться на клиенте в ответ на события, происходящие на сервере. Но после добавления нового события к dispinterface и команды Refresh никаких новых заготовок не появляются ни в одном модуле, входящих в состав проекта сервера. И это следовало ожидать: соответствующий метод в ответ на событие на сервере необходимо вызывать при обработке какого-либо события. Как это сделать – проще всего показать на примере. В Delphi4 дадим команду File/New application и на вновь созданное приложение поставим две кнопки. После этого выполним команду File/New/ActiveX/Automation Object. Имя класса дадим Basic и обязательно поставим отметку на Event support. После этого будет создана библиотека типов и модуль реализации. Отметим в библиотеке типов интерфейс IBasicEvents и создадим два метода. Первый назовем OnClick – он будет без передаваемых параметров, а второй – PerformWidth(Width:integer) – будет передаваться целое число как параметр. Этот пример интересен тем, что для out-of-process сервера OLE автомации невозможно передать сообщение клиенту, как это было описано в для in-process сервера. Далее, в модуле реализации (implementation unit) сделаем следующие изменения:

1. В секции interface определим глобальную переменную FBasicList:TList; Создадим рабочую копию списка в секции initialization и вызовем его деструктор в секции finalization. Название последней секции следует написать вручную.

initialization
FBasicList:=TList.Create;
TAutoObjectFactory.Create(ComServer, TBasic, Class_Basic,
ciMultiInstance, tmFree);
finalization
if Assigned(FBasicList) then FBasicList.Free;
Этот список будет использоваться для хранения ссылок на обьект TBasic. Список необходим потому, что сервер OLE автомации был создан с использование параметра ciMultiInstance. При этом будет создаваться новая копия класса TBasic для каждого соединения с новым клиентом.

2. Перепишем метод AfterConstruction класса TBasic. Для этого в секции public заголовка класса напишем procedure AfterConstruction; override; В секции implementation реализуем этот метод:

procedure TBasic.AfterConstruction;
begin
inherited AfterConstruction;
if Assigned(FBasicList) then FBasicList.Add(Self);
end;

то есть после создания рабочей копии класса ссылка на него будет запоминаться в глобальной переменной, определенной в секции 1.

3. Поскольку при разрушении класса необходимо удалить ссылку на него из списка, перепишем метод BeforeDestruction класса TBasic.

procedure TBasic.BeforeDestruction;
var
Index:integer;
begin
if Assigned(FBasicList) then begin
Index:=FBasicList.IndexOf(Self);
if Index>=0 then FBasicList.Delete(Index);
end;
inherited BeforeDestruction;
end;

4. Необходимо экспонировать интерфейс IBasicEvents, так чтобы он был виден из рабочей копии класса. Для этого в секции public заголовка класса TBasic напишем это свойство: property Events:IBasicEvents read FEvents;

Далее, вызовем главную форму проекта, на которой находятся две кнопки, и в обработчике события OnClick первой кнопки вызовем нотификацию OnClick из интерфейса IBasicEvents:

procedure TForm1.Button1Click(Sender: TObject);
var
FBasic:TBasic;
I:integer;
begin
if Assigned(FBasicList) and (FBasicList.Count>0) then
for I:=0 to FBasicList.Count-1 do begin
FBasic:=TBasic(FBasicList[I]);
if Assigned(FBasic) then if Assigned(FBasic.Events) then FBasic.Events.OnClick;
end;
end;

В приведенном выше методе вручную необходимо написать только выделенные строки – остальное сделал эксперт Delphi. Соответственно, в обработчике события на нажатие второй кнопки вызовем второй метод IBasicEvents: if Assigned(FBasic) then if Assigned(FBasic.Events) then FBasic.Events.PerformWidth(Width); Для того, чтобы данный проект компилировался, необходимо сослаться на модуль реализации библиотеки типов, указав его название в секции uses.

И последнее. Уже на этом этапе можно скопировать GUID (идентификатор интерфейса) для IBasicDisp из редактора библиотеки типов, как это показано на рисунке ниже, и запомнить его в отдельный файл.

Он понадобится для создания клиента OLE автомации. Далее необходимо скомпилировать проект и запустить его на выполнение. После этого сервер OLE автомации будет зарегистрирован в системном реестре.

Создадим новый проект для клиентного приложения - PNoteCli. Далее необходимо вызвать команду Project/Import type library и импортируем библиотеку типов сервера автомации, созданном на предыдущем этапе - NoteTest_TLB.pas. В этом проекте необходимо создать IDispatch с теми методами, которые были реализованы для IBasicEvents на предыдущем этапе создания сервера. Поскольку уже импортирована библиотека сервера NOTETEST.TLB, естественно было бы ожидать наличие эксперта Delphi, который создал бы данный интерфейс автоматически. Однако, при тщательном анализе прилагаемых вместе с Delphi4 инструментов, такой эксперт не обнаружен. Поэтому IDispatch на клиенте необходимо создавать при помощи традиционных методов.

Выполним команду File/New/ActiveX/Automation Object. Определим имя класса - MyClass. При этом будет создана библиотека типов, в котором будет заготовка для интерфейса IMyClass. Далее, GUID для IBasicDispatch, полученный и запомненный в прошлом проекте - NoteTest, необходимо ввести в качестве GUID IMyClass.

После этого необходимо определить два метода для IMyClass - с теми же самые названиями и с теми же самыми списками параметров, что были ранее определены в IBasicDispatch. После команды Refresh в секции реализации поместим код, в котором можно убедиться, что данные методы вызываются:

procedure TMyClass.OnClick;
begin
ShowMessage('Client receive notification');
end;

procedure TMyClass.PerformWidth(Width: Integer);
begin
ShowMessage('Width='+IntToStr(Width));
end;

Поступим как в проекте NoteTest: определим в модуле реализации библиотеки типов глобальную переменную EventInterface:TMyClass=nil; В этой переменной необходимо сохранить ссылку на рабочую копию TMyClass, когда он будет создан. Для этого в описание класса TMyClass добавим описание метода procedure AfterConstruction; override; и в секции implementation реализуем этот метод:

procedure TMyClass.AfterConstruction;
begin
inherited AfterConstruction;
EventInterface:=Self;
end;

На этом реализация интерфейса заканчивается. Теперь необходимо вызвать сервер OLE автомации из данного приложения и, после успешного вызова, передать ссылку на IMyClass используя интерфейс IConnectionPoint. Для этого в секции private описания класса TForm1 определим две переменные: V:IBasic; и FUserID:integer; В первой переменной будет сохраняться ссылка на сервер OLE автомации. Переменная FUserID необходима для корректной работы интерфейса IConnectionPoint. Далее, поставим кнопку на форму и для нее сделаем обработчик события OnClick:

procedure TForm1.Button1Click(Sender: TObject);
var
ESink:IConnectionPoint;
begin
V:=CoBasic.Create;
if not Assigned(EventInterface) then EventInterface:=TMyClass.Create;
with V as IConnectionPointContainer do begin
FindConnectionPoint(IBasicEvents,ESink);
ESink.Advise(EventInterface as IDispatch,FUserID);
end;
end;

Первоначально вызывается метод CoBasic.Create. Он определен в модуле NoteTest_TLB.pas. Этот метод запускает сервер OLE автомации (если он не был запущен ранее) и получает ссылку на экспонируемые им интерфейсы. Далее, происходит проверка, был ли создан IMyClass интерфейс внутри данного приложения. Данный интерфейс создается автоматически, при старте приложения, если только приложение вызывается как OLE сервер автомации из какого-либо клиента. Поэтому после запуска приложения как одиночного, IMyClass не будет создан и потребуется вызов конструктора TMyClass.Create. Далее вызывается метод интерфейса IConnectionPointContainer - FindConnectionPoint. Интерфейс IConnectionPointContainer экспонирует сервер NoteTest. Метод FindConnectionPoint для заданного GUID возвращает интерфейс IConnectionPoint. После получения ссылки на интерфейс IConnectionPoint следует воспользоваться его методом Advise. В качестве параметра этот метод принимает IDispatch, созданный в данном приложении и в переменной FUserID он возвращает целочисленный идентификатор, который будет использоваться в дальнейшем.

Это достаточно, чтобы получать нотификации от сервера автомации. Однако, при закрытии такого клиентского приложения (или при его отсоединении от сервера) необходимо уведомить сервер о том, что нотификации больше не требуется. Для этого перепишем деструктор формы, добавив строчку destructor Destroy; override; в секцию public. В секции implementation реализуем новый деструктор:

destructor TForm1.Destroy;
var
ESink:IConnectionPoint;
begin
if Assigned(EventInterface) then
with V as IConnectionPointContainer do begin
FindConnectionPoint(IBasicEvents,ESink);
ESink.UnAdvise(FUserID);
end;
inherited Destroy;
end;

Так же, как и при вызове сервера OLE автомации, здесь получается ссылка на IConnectionPoint. Однако, вызывается уже другой метод - UnAdvise, и в качестве параметра передается идентификатор, который был получен при вызове метода Advise. После успешного вызова метода UnAdvise сервер перестанет посылать нотификации данному клиенту.

Следует принимать во внимание, что несколько клиентных приложений теоретически могут подключиться к одному интерфейсу для получения нотификаций. Например, это возможно при использовании более мягкого метода - GetActiveObject вместо используемого в CoBasic.Create метода CreateComObject. В этом случае клиентному приложению будет возвращена ссылка на уже созданный интерфейс IBasic. Для того, чтобы можно было бы вызвать метод Advice интерфейса IConnectionPoint необходимо сделать следующие изменения:

1. В реализации метода Initialize класса TBasic изменить флаг ckSingle на ckMulti.

2. Изменить вызов методов IBasicEvents следующим образом:

procedure TForm1.Button1Click(Sender: TObject);
var
IP:IConnectionPoint;
IC:IEnumConnections;
I,K:integer;
Test:HResult;
IUnk:IUnknown;
IDisp:IDispatch;
V:Variant;
FBasic:TBasic;
begin
if Assigned(FBasicList) and (FBasicList.Count>0) then
for K:=0 to FBasicList.Count-1 do begin
FBasic:=TBasic(FBasicList[K]);
if Assigned(FBasic) then with FBasic as IConnectionPointContainer do FindConnectionPoint(IBasicEvents,IP);
if Assigned(IP) then begin
IP.EnumConnections(IC);
if Assigned(IC) then begin
I:=1;
Test:=IC.Next(I,IUnk,nil);
while (Test=S_OK) and Assigned(IUnk) do begin
IUnk.QueryInterface(IDispatch,IDisp);
if Assigned(IDisp) then begin
V:=IDisp;
V.OnClick;
end;
Inc(I);
Test:=IC.Next(I,IUnk,nil);
end;
end;
end;
end;
end;

здесь используется новый интерфейс - IEnumConnections для того, чтобы поочередно получить ссылки на IDispatch клиентов. Главный его метод - Next - возвращает ссылку на интерфейс с указанным номером I. Нумерация начинается с единицы.

Сейчас часто используются многозвенные приложения для работы с базами данных (Midas технология). Центральное звено в этой архитектуре называется сервер приложений (application server) и он поддерживает протокол OLE автомации для связи с клиентами. При создании удаленного модуля данных в Delphi4, метка Events support отсутствует на диалоге, который при этом возникает. Однако, вся обсуждаемая здесь методика поддержки сообщений, работает и для сервера приложений. При небольших дополнительных усилиях можно реализовать поддержку сообщений без помощи экспертов Delphi. Для сервера приложений имеет смысл потратить время, так как клиенты могут редактировать данные независимо, и нотификации просто необходимы для постоянного обновления данных клиентам.

Хотите изучить работу c Delphi 4?
Записывайтесь на курсы по этому продукту в Учебно-консалтинговом Центе Интерфейс Ltd.

Сергей В. Трепалин,
Преподаватель Учебно-консалтингового центра Интерфейс Лтд.
Тел: (095)135-2519, 135-5500
E-mail: mail@interface.ru


Interface Ltd.

Ваши замечания и предложения направляйте по адресу: webmaster@interface.ru

Reklama.Ru. The Banner Network.