Compago

...free knowledge

 
  • Increase font size
  • Default font size
  • Decrease font size
Home Manuali Programmazione Esempio di Automation Server con gestione eventi

Esempio di Automation Server con gestione eventi

E-mail Stampa PDF
Indice
Esempio di Automation Server con gestione eventi
Broadcasting events
Tutte le pagine

Indice

Automation event

Un tipico sistema COM client/server generalmente consente al client di chiamare il server tramite un'interfaccia.
Un server COM è caratterizzato da una o più interfacce e ognuna di queste può avere dei metodi o delle proprietà che il client può chiamare. Il processo è quindi unidirezionale, cioè il client ordina e il server esegue.
Questo va bene quando un client chiede al server di eseguire un'azione o recuperare i dati, ma se ci fosse la necessità da parte del server di chiedere qualcosa al client? E' chiaro che le interfacce di ingresso sono solo inbound.
Non vi è alcun modo per il server di interrogare uno dei suoi client.
E' a questo punto che entrano in gioco il modello di  Server Events. Un server che supporta gli eventi non solo risponde alle richieste del cliente, ma può anche segnalare il suo stato e fare le proprie richieste al client.
Ad esempio: un client effettua una richiesta per il server per scaricare un file. Ora invece di aspettare che il server completi il suo lavoro, il client potrebbe occuparsi d'altro. Al termine del download il server, di sua iniziativa, potrebbe comunicare la fine del suo lavoro e il client potrebbe reagire con una determinata azione.

Per un esempio di COM server senza gestione eventi leggere gli articoli:

 

Funzionamento

Il modo in cui un client viene chiamato dal server non è troppo diverso dal procedimento contrario. Il server definisce e genera gli eventi, mentre il client è responsabile della connessione e realizzazione degli eventi. Questo si realizza attraverso una interfaccia eventi o interfaccia di uscita (outgoing interface) la quale è definita nella type library del server.

Ora, prima di iniziare davvero, è meglio chiarire un po di termini.

  • Connection Point (Punto di connessione): E' un'entità che descrive l'accesso ad una interfaccia eventi.
  • Event Source (Origine evento): un oggetto che definisce un'interfaccia in uscita ed innesca gli eventi, in genere è proprio l'automation server.
  • Event Sink: E' un oggetto che implementa un'interfaccia eventi e risponde agli eventi, in genere è nel client.
    In pratica gestisce è il codice che deve essere eseguito quando l'evento viene innescato.
  • Advise: Significa collegare un event sink ad un connection point così che l'event source possa accedere al codice del sink.

Questi sono i componenti principali del modello a Eventi. In poche parole, un evento sink si connetterà a una sorgente tramite un punto di connessione, consentendo così la sorgente di innescare un evento che a sua volta richiama una determinata funzione.

Il modo più semplice per spiegare il meccanismo degli eventi è quello di fare un esempio molto semplicel di client/server.
Il client avrà un pulsante; premendo quest'ultimo viene richiamato un metodo sul server. Questo metodo innescherà un evento che verrà gestito da una funzione sul client. Tutto il processo verrà tracciato da un log su un componente memo sia sul client che sul server.

Esempio

Il Server

Ogni server di automazione che vuole comunicare con gli eventi ha bisogno di definire una interfaccia in uscita, e deve implementare un'interfaccia in ingresso per poter ricercare e connettersi a quella precedente.
L'interfaccia in ingresso a cui ci riferiamo è la IConnectionPointContainer. Il client userà questa interfaccia per trovare (FindConnectionPoint) o enumerare (EnumConnectionPoints) i punti di connessione supportati dal suo event sink (gestore dell'evento). Con queste funzioni ci procuriamo un IConnectionPoint, cioè un punto di connessione, tramite il quale il client chiamerà la funzione Advise con cui il gestore dell'evento(sink) viene legato all'evento del server.
Entrambe le interfacce sono definite nel file ACTIVEX.PAS e delphi si occuperà di inserirle automaticamente.

Iniziamo:
Creare una nuova applicazione con una form e inserirci dentro un TMemo.

Ora, andare a New->Other->Activex->Automation Object per avviare la procedura guidata alla creazione di un Automation Object.

Qui viene richiesto un nome per la coClasse che andremo a creare. Per questo esempio useremo il nome MioServerCom.

Prima di fare clic su OK, selezionare la casella Generate Event Support Code. Questo server per creare l'interfaccia IConnectionPointContainer come descritto nel paragrafo precedente. Il risultato sarà una unit (UnitServer) che contiene il vostro oggetto TMioServerCom e la definizione della coClass. Delphi inoltre genera una type library che include la tipica doppia interfaccia di ingresso IMioServerCom e IMioserverComDisp, più un'altra che è l'interfaccia in uscita per gli eventi IMioServerComEvents.

L'interfaccia IMioServerCom ha ora bisogno di avere un metodo che sarà chiamato dal client. Quindi apriamo il Type Library Editor (se questo non fosse già attivo) e aggiungiamo il metodo CallServer().

Dopo abbiamo bisogno di definire un evento che verrà innescato dal server. Questo evento, che chiameremo EventFired(), è solo un metodo senza una implementazione e verrà dichiarato come metodo nell'interfaccia IMioServerComEvents. A noi infatti non server sul server il codice che deve essere eseguito al momento dell'evento, questo infatti risiederà nel client. L'unica cosa che importa per ora è definirlo parametricamente e dichiararlo nell'interfaccia degli eventi.

Fatto questo premiamo sul pulsante Refresh Implementation e tornando al codice nella UnitServer vedremo che è stato aggiunto un nuovo metodo.

In pratica quando il client chiama il server, il server informa l'utente tramite il memo, poi attiva l'evento EventFired. L'implementazione è la seguente:

uses
...
UnitForm,
...
 
procedure CallServer; safecall;
begin
FormServer.Memo1.Lines.Add('Richiesta del client');
if FEvents <> nil then begin
FEvents.EventFired;
FormServer.Memo1.Lines.Add('Evento innescato');
end;
end;

NOTA: FEvents è la nostra interfaccia in uscita. Notate che prima di tutto abbiamo bisogno di verificare che non sia nulla, perché questo ci indica se c'è o meno un Event Sink in "ascolto", altrimenti rischieremo di chiamare una funzione senza che sia stata associata ad alcun codice. Se non è stato assegnato nessun gestore per l'evento la FEvents sarà NIL.

A questo punto il server è completo! Ora occorre crearlo (built) e "registrarlo" nel sistema.

Il client

Creiamo una nuova applicazione con una form con all'interno un pulsante e un TMemo.

Nel codice della unit aggiungiamo la unit TLB (server_TLB) che era stata creata e usata dal server, così da avere accesso alle interfacce e i vari metodi dell'oggetto COM creato precedentemente, più le altre unit necessarie.

uses
...
server_TLB, ActiveX, ComOb,
... 

Prima di procedere è bene chiarire che un event sink serve a interfacciare una semplice funzione di callback (OnFiredEvent) con la tecnologia COM. Un event sink quindi non sarà solo una procedura, bensì una particolare classe che implementa alcune interfacce e di cui descriveremo i dettagli nei paragrafi successivi.

La classe della form per il client necessita di alcuni campi  (l'interfaccia del server COM e l'event sink), infatti come quando si vuole usare uno oggetto COM è necessario dichiarare le istanze delle sue interfacce. Li dichiareremo nella sezione private:

private
FServer: IMioServerCom;
FEventSink: IUnknown;
FConnectionToken: integer;

Ora dobbiamo pensare all'implementazione dell'Event Sink. Iniziamo con la definizione della classe Event Sink, il quale è un automation object, e quindi deve implementare l'interfaccia IDispatch. Il suo compito è quello di prendersi carico degli eventi generati dal server COM al quale verrà connesso. Questo server in automatico ogni volta che viene innescato un evento va a richiamare la funzione Invoke() del'event sink connesso; la quale a seconda dei parametri che gli sono stati passati riconoscerà l'evento ed eseguira un determinato codice.

Ecco la definizione dell'Event Sink:

TEventSink = class(TInterfacedObject, IUnknown,IDispatch)
 private
FController: TFormServer;
{IUknown methods}
function QueryInterface(const IID: TGUID; out Obj):HResult;stdcall;
{Idispatch}
function GetTypeInfoCount(out Count: Integer): HResult; stdcall;
function GetTypeInfo(Index, LocaleID: Integer; out TypeInfo): HResult; stdcall;
function GetIDsOfNames(const IID: TGUID; Names: Pointer;
NameCount, LocaleID: Integer; DispIDs: Pointer): HResult; stdcall;
function Invoke(DispID: Integer; const IID: TGUID; LocaleID: Integer;
Flags: Word; var Params; VarResult, ExcepInfo, ArgErr: Pointer): HResult; stdcall;
public
constructor Create(Controller: TFormServer);
end;

Le parti in rosso sono state inserite per tenere traccia della form all'interno della classe TEventSink. La maggior parte di questi metodi non hanno bisogno di una particolare implementazione; basterà che il risultato delle funzioni sia il valore S_OK (=0). Le uniche funzioni che dovremo personalizzare sono QueryInterface() e Invoke(). Questi metodi sono quelli chiamati dal server per ottenere le interfacce e chiamare il gestore degli eventi.

function TEventSink.QueryInterface(const IID: TGUID; out Obj):HResult;stdcall;
begin
if GetInterFace(IID,Obj) then
Result := S_OK
else if IsEqualIID(IID,IMioServerComEvents) then
Result := QueryInterface(IDispatch,Obj)
else
Result := E_NOINTERFACE;
end;

Questo metodo si occupa delle sue interfacce IDispatch e IUnknown , ed entra in azione per trovare l'interfaccia in uscita IMioServerComEvents quando questa viene richiesta.

function TEventSink.Invoke(DispID: integer; const IID: TGUID; LocaleID: integer; 
Flags: Word; var Params; VarResult,ExcepInfo,ArgErr:Pointer): HResult;
begin
Result := S_OK;
case DispID of
201: FController.OnEventFired;
end;
end;

Questa funzione viene chiamata dal server al momento in cui si manifesta l'evento. Tra i vari parametri , DispID identifica l'evento sull'interfaccia di uscita e deve essere lo stesso di quello dichiarato nel file Server_TLB. Se avessimo avuto due o più eventi allora avremo dovuto inserire lo stesso numero di righe nella dichiarazione di case.

Notare che la funzione Invoke() richiama una procedura OnEventFired utilizzando il riferimento locale (FormController) alla form della applicazione (FormClient). Questo riferimento locale alla for viene gestito durante la creazione della classe:

constructor TEventSink.Create(Controller: TFormClient);
begin
inherited Create;
FController := Controller;
end;

Una volta creata la classe vediamo di usarla. Quello che occorre fare è creare l'event sink, collegarsi al server COM e connettere ad esso l'event sink; faremo tutto questo gestore dell'evento OnCreate del client:

procedure TFormClient.FormCreate(Sender: TObject);
begin
FServer := CoMioServerCom.Create;
FEventSink := TEventSink.Create(FormClient);
InterfaceConnect(FServer, IMioServerComEvents,FEventSink,FconnectionToken);
end;

Ricordate che l' FEventSink è un interfaccia IUnknown, che noi stiamo recuperando dal constructor della classe  TEventSink, quindi non sarà più un riferimento standard e questo ci permetterà di usare la funzione Advise per la connessione ad esso.

Quindi in primo luogo, creiamo il nostro server utilizzando la sua coclasse. Dopo creiamo un istanza del nostro Event sink. Infine useremo il metodo InterfaceConnect() per collegare l'event sink al server; questa funzione è molto comoda, basta passare come parametri:

  • l'istanza del server creato in precedenza
  • la sua interfaccia in uscita alla quale vogliamo connetterci
  • l'istanza dell'event sink da connettere al server
  • un numero intero che tiene traccia della connessione creata.

Occorre conservare questo numero dato che serve usarlo nella disconnessione dal server; infatti è possibile disconnettersi semplicemente usando la funzione  InterfaceDisconnect():

InterfaceDisconnect(FServer,IMioServerCom,FConnectionToken);

Dato che abbiamo creato la connessione con l'evento Oncreate, effettueremo la disconnessione nell'evento OnDestroy.

procedure TFormClient.FormDestroy(Sender: TObject);
begin
InterfaceDisconnect(FServer,IMioServerCom,FConnectionToken);
end;

Le ultime cose che ci rimangono sono è invocare il metodo CallServer dal pulsante sulla form.

procedure TFormClient.Button1Click(Sender: TObject);
begin
Memo1.Lines.Add('Richiesta verso il server');
FServer.CallServer;
end;

e l'implementazione della funzione che si occuperà di gestire l'evento, cioè che verrà chiamata ogni volta che il server innesca l'evento:

procedure TFormClient.OnEventFired;
begin
Memo1.Lines.Add('Evento ricevuto')
end;

Il codice sia del client che del server verranno riportati al termine dell'articolo.

Adesso non rimane che compilare e lanciare il client; quello che dovreste vedere è che premendo il pulsante sulla form del client dovrebbe apparire un messaggio che attesta l'avvenuta ricezzione dell'evento del server.

Ora a voler essere critici possiamo analizzare alcuni scenari:

  • Ci sono due applicazioni server attive: i client si connetteranno sempre e solo alla prima.
  • Ci sono più client connessi al server: ogni client funziona bene, ma dato che l'evento sul server dovrebbe essere unico, se un client innesca l'evento, il risultato dovrebbe interessare contemporaneamente anche tutti gli altri client connessi. Invece questo non accade, perchè per ogni client connesso al server viene creato un automation object che funziona da server COM. Ognuno di questi oggetti non tiene conto degli altri e risponde solo al proprio client.
    Solitamnte questo comportamento non è proprio consigliabile perchè va contro la logica degli eventi multicasting e perchè consuma memoria.

Codice server

unit UnitServer;

{$WARN SYMBOL_PLATFORM OFF}

interface

uses
ComObj, ActiveX, AxCtrls, Classes, Server_TLB, StdVcl;

type
TMioServerCom = class(TAutoObject, IConnectionPointContainer, IMioServerCom)
private
FConnectionPoints: TConnectionPoints;
FConnectionPoint: TConnectionPoint;
FEvents: IMioServerComEvents;
{ note: FEvents maintains a *single* event sink. For access to more
than one event sink, use FConnectionPoint.SinkList, and iterate
through the list of sinks. }
public
procedure Initialize; override;
protected
procedure CallServer; safecall;
property ConnectionPoints: TConnectionPoints read FConnectionPoints
implements IConnectionPointContainer;
procedure EventSinkChanged(const EventSink: IUnknown); override;
end;

implementation

uses ComServ,UnitForm;

procedure TMioServerCom.EventSinkChanged(const EventSink: IUnknown);
begin
FEvents := EventSink as IMioServerComEvents;
end;

procedure TMioServerCom.Initialize;
begin
inherited Initialize;
FConnectionPoints := TConnectionPoints.Create(Self);
if AutoFactory.EventTypeInfo <> nil then
FConnectionPoint := FConnectionPoints.CreateConnectionPoint(
AutoFactory.EventIID, ckSingle, EventConnect)
else FConnectionPoint := nil;
end;

procedure TMioServerCom.CallServer;
begin
FormServer.Memo1.Lines.Add('Richiesta del client');
if FEvents <> nil then
begin
FEvents.EventFired;
FormServer.Memo1.Lines.Add('Evento innescato');
end;
end;

initialization
TAutoObjectFactory.Create(ComServer, TMioServerCom, Class_MioServerCom,
ciMultiInstance, tmApartment);
end.

Codice client

unit UnitClient;

interface

uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, StdCtrls, server_TLB, ActiveX, ComObj;

type
TFormClient = class(TForm)
Memo1: TMemo;
Button1: TButton;
procedure Button1Click(Sender: TObject);
procedure FormCreate(Sender: TObject);
procedure FormDestroy(Sender: TObject);
private
FServer: IMioServerCom;
FEventSink: IUnknown;
FConnectionToken: integer;
public
procedure OnEventFired;
end;

TEventSink = class(TInterfacedObject, IUnknown,IDispatch)
private
FController: TFormClient;
{IUknown methods}
function QueryInterface(const IID: TGUID; out Obj):HResult;stdcall;
{Idispatch}
function GetTypeInfoCount(out Count: Integer): HResult; stdcall;
function GetTypeInfo(Index, LocaleID: Integer; out TypeInfo): HResult; stdcall;
function GetIDsOfNames(const IID: TGUID; Names: Pointer;
NameCount, LocaleID: Integer; DispIDs: Pointer): HResult; stdcall;
function Invoke(DispID: Integer; const IID: TGUID; LocaleID: Integer;
Flags: Word; var Params; VarResult, ExcepInfo, ArgErr: Pointer): HResult; stdcall;
public
constructor Create(Controller: TFormClient);
end;

var
FormClient: TFormClient;

implementation

{$R *.dfm}

procedure TFormClient.Button1Click(Sender: TObject);
begin
Memo1.Lines.Add('Richiesta verso il server');
FServer.CallServer;
end;

procedure TFormClient.FormCreate(Sender: TObject);
begin
FServer := CoMioServerCom.Create;
FEventSink := TEventSink.Create(FormClient);
InterfaceConnect(FServer,IMioServerComEvents,FEventSink,FConnectionToken);
end;

procedure TFormClient.FormDestroy(Sender: TObject);
begin
InterfaceDisconnect(FServer,IMioServerCom,FConnectionToken);
end;

procedure TFormClient.OnEventFired;
begin
Memo1.Lines.Add('Evento ricevuto')
end;

{ TEventSink }

constructor TEventSink.Create(Controller: TFormClient);
begin
inherited Create;
FController:=Controller;
end;

function TEventSink.GetIDsOfNames(const IID: TGUID; Names: Pointer; NameCount,
LocaleID: Integer; DispIDs: Pointer): HResult;
begin
Result:=S_OK;
end;

function TEventSink.GetTypeInfo(Index, LocaleID: Integer;out TypeInfo): HResult;
begin
Result:=S_OK;
end;

function TEventSink.GetTypeInfoCount(out Count: Integer): HResult;
begin
Result:=S_OK;
end;

function TEventSink.Invoke(DispID: Integer; const IID: TGUID; LocaleID: Integer;
Flags: Word; var Params; VarResult, ExcepInfo, ArgErr: Pointer): HResult;
begin
Result:=S_OK;
case DispID of
201: FController.OnEventFired;
end;
end;

function TEventSink.QueryInterface(const IID: TGUID; out Obj): HResult;
begin
if GetInterFace(IID,Obj) then
Result := S_OK
else if IsEqualIID(IID,IMioServerComEvents) then
Result := QueryInterface(IDispatch,Obj)
else
Result := E_NOINTERFACE;
end;

end.

 



Ultimo aggiornamento ( Sabato 31 Luglio 2010 10:21 )  
Loading

Login




Chiudi