Шаг 37.
Серверы и контроллеры автоматизации. Реализация нотификационных сообщений

    На этом шаге мы рассмотрим алгоритм изменения сервера для обработки нотификационных сообщений.

    Реализуем поддержку нотификаций в проекте AutoServ. Прежде всего надо определить, о каких событиях на сервере должен знать клиент? Первое событие - изменение содержимого компонента ТМеmо на сервере. Если об этом сообщить клиенту, то он может запросить у сервера это содержимое и изменить его, осуществив так называемую "горячую связь". Таким же образом будет реализовываться код в реальных проектах - там, например, можно уведомлять клиента об изменении данных на сервере, что достаточно часто требуется на практике. И второе событие, о котором следует сообщить клиенту, - это принудительное закрытие сервера после щелчка на кнопке Close. Если закрыть сервер принудительно (при таком закрытии возникнет предупреждение о нежелательности подобной операции), то на клиенте сохранится ссылка на интерфейс сервера. При первой же попытке обратиться к методу этого интерфейса генерируется исключение со следующим сообщением:

   The RPC server is unavailable

    Чтобы не доводить клиентское приложение до исключения, ему полезно сообщить о закрытии сервера. Ожидаемая реакция клиента в этом случае - отсоединение от сервера.

    Откроем созданный ранее проект AutoServ, при необходимости выберем в меню команду View | Type Library для открытия окна редактора библиотеки типов. В окне редактора выделим интерфейс ITestEvents и добавим два метода - OnTextChange и OnClose, оба без параметров. Кроме того, обработчики событий не должны возвращать результат. Поэтому для каждого из методов на странице Parameters измените содержимое раскрывающегося списка Return Type на void, как показано на рисунке 1.


Рис.1. Добавление обработчиков событий для сервера автоматизации

    После добавления новых событий к диспиитерфейсу и щелчка на кнопке Refresh панели инструментов не появляется никаких новых "заготовок" кода ни в одном из модулей, входящих в состав проекта сервера. И этого следовало ожидать: мы сами должны вызвать соответствующий метод в подходящем месте в ответ на событие на сервере. Событие OnTextChange необходимо вызывать из обработчика OnChange класса ТМеmо. Событие OnClose необходимо вызывать из обработчика события OnClose класса TForm1. Для вызова обработчика событий в этих методах должна быть доступна ссылка на нотификационный интерфейс клиента. Это достигается объявлением предназначенного только для чтения свойства Events: ITestEvents в секции public класса TTest.

    Однако это еще не все. Следует учитывать, что к одному экземпляру сервера может обращаться несколько клиентов одновременно. Им всем необходимо разослать нотификационпые сообщения - то есть серверу потребуется список клиентов. Такой список не формируется автоматически - его придется формировать в коде. Для формирования списка клиентов можно воспользоваться следующей особенностью сервера автоматизации (если при его создании в списке Instancing окна мастера создания объектов автоматизации был выбран пункт Multiple Instance): для каждого клиента формируется отдельный экземпляр класса TTest.


Рис.2. Значение из списка Instancing

    Соответственно, переписав конструктор класса TTest, можно запомнить указатель класса в списке. Но просто список (компонент TList) в данном случае не подходит, поскольку клиенты работают в разных потоках выполнения и при асинхронном доступе разных клиентов к списку могут происходить исключения. Требуется класс - аналог класса TList, имеющий потокозащищеииые секции. Его можно написать самостоятельно, используя в качестве класса-предка TList, но делать этого не надо. В Delphi имеется класс TThreadList, аналогичный классу TList, no потокозащищеииый. Именно в экземпляре этого класса и будет храниться список клиентов.

    Таким образом, к модулю, содержащему описание класса TTest, следует добавить следующий код:

interface
.   .   .   .
type
  TTest = class(TAutoObject, IConnectionPointContainer, ITest)
.   .   .   .
  public
.   .   .   .
    procedure AfterConstruction; override;
    destructor Destroy; override;
    property Events:  ITestEvents read FEvents;
.   .   .   .
  end;

var
ClientList: TThreadList = nil;

implementation
.   .   .   .
procedure TTest.AfterConstruction;
var
  L:TList;
begin
  inherited;
  try
    L := ClientList.LockList;
    L.Add(Self);
  finally
    ClientList.UnlockList;
  end;
end;

destructor TTest.Destroy;
var
  L:TList;
  N:integer;
begin
  try
    L := ClientList.LockList;
    N := L.IndexOf(Self);
    if N >= 0 then L.Delete(N);
  finally
    ClientList.UnlockList;
  end;
  inherited;
end;

initialization
.   .   .   .
  ClientList := TThreadList.Create;
finalization
  ClientList.Free;
end.

    Здесь многоточиями обозначен созданный ранее код. В секции initilization создается экземпляр класса TThreadList, где будет храниться список клиентов. Ссылка на экземпляр располагается в глобальной переменной ClientList, которая объявлена в секции interface - чтобы ее можно было использовать в других модулях. В перекрытом методе AfterConstruction запоминается ссылка на созданный экземпляр класса TTest в списке ClientList. Класс TThreadList имеет метод Lock. После вызова этого метода мы получаем ссылку на класс TList и одновременно запрещаем клиентам других потоков выполнения работать с этим экземпляром класса. После окончания работы обязательно необходимо вызвать метод UnlockList - иначе клиенты, обслуживаемые другими потоками, не смогут работать с переменной TThreadList. Поэтому вызовы методов LockList и UnlockList помещены в защищенный блок try...finally...end.

    Теперь можно вызвать добавленные методы OnTextChange и OnClose для каждого из клиентов в классе TForm1:

procedure TForm1.Memo1Change(Sender: TObject);
var
  L: TList;
  I: Integer;
  T: TTest;
begin
  try
    L := ClientList.LockList;
    for I :=0 to L.Count-1 do
    begin
      T := TTest(L[I]);
      if Assigned(T.Events) then T.Events.OnTextChange;
    end;
  finally
    ClientList.UnlockList;
  end; 
end;

procedure TForm1.FormClose(Sender: TObject; var Action: TCloseAction);
var
  L: TList;
  I: Integer;
  T: TTest;
begin
  try
    L := ClientList.LockList;
    for I := L.Count-1 downto 0 do
    begin
      T := TTest(L[I]);
      if Assigned(T.Events) then T.Events.OnClose;
    end;
  finally
    ClientList.UnlockList;
  end;
end;

end.

    Перед вызовом любого из нотификациоиных сообщений необходимо проверить факт реализации клиентом нотификациогшого интерфейса и передачи ссылки на него на сервер. Это достигается вызовом функции Assigned.

    И еще комментарий по поводу следующего цикла в обработчике события TForm1.FormClose:

  for I  := L.Count-1 downto 0 do 
  begin

    Ожидаемая реакция клиента при получении сообщения об этом событии - отсоединение от сервера. При этом в списке L ссылка на клиента будет удалена, а остальные - смещены по индексам. Для корректного выполнения кода в таких условиях необходимо отсчитывать цикл от больших индексов к меньшим. На этом изменения в сервере можно считать законченными.


    Замечание. Не забудьте подключить к модулю, содержащему описание класса TForm1 (в нашем случае, это Unut1), модуль, содержащий описание класса TTest (Unit2) и модуль AutoServ_TLB.
Текст этого приложения можно взять здесь (17,5 Кб).

    На следующем шаге мы рассмотрим алгоритм создания клиента для созданного сервера.




Предыдущий шаг Содержание Следующий шаг