Active objects
In this chapter I will discuss how to share an automation object among
several clients. We will meet the Running Object Table, take a closer look how a
client connects its eventsinks to a connectable object, and see how an automation
class can implement multiple interfaces.
This story has been inspired by some threads in the Borland
newsgroup on automation. Special credit should be given to Greg Lorriman on whose ROTviewer
code I have based the ROTviewer presented here.
GetObject and the ROT
Well known automatable applications like Word and Excel register at startup
their main (application) object in the so-called Running Object Table
(ROT). Instead of starting a new instance an automation client can connect to
the running instance of the application
using the GetObject method in VBA:
Dim MyManager As Excel.Application
On Error GoTo CreateNew
Set MyExcel = GetObject(, "Excel.Application")
GoTo Created
CreateNew:
Set MyExcel = New Excel.Application
Created:
MyExcel.WorkBooks.Open("MyFile")
GetObject will raise an exception if there is no running instance of Excel, I
will have to use VBA error handling to handle
that. If GetObject cannot find a running instance I will create a new
instance. And when Excel is running one way or the other, my spreadsheet file can be opened.
To work in Delphi with automation objects in the ROT you can use the
GetActiveObject function. When I have added the Word typelibrary to my project I
can try to connect to Word :
var Unknown : Iunknown;
if (GetActiveObject(Word_TLB.CLASS_WordApplication, nil, Unknown) = MK_E_UNAVAILABLE) then
fWordApplication := Word_TLB.CoWordApplication.Create
else
Unknown.QueryInterface(Word_TLB._Application, fWordApplication);
GetActiveobject is passed the GUID of the desired class, a reserved nil pointer and an
out param to hold a resulting interface. If there is no Word application in the ROT the
function will return MK_E_UNAVAILABLE and I have to create the
Word.Application object myself. If the call to GetActiveObject was successfull I
use QueryInterface to obtain the Word.Application
object from the returned interface.
Register your own object in the ROT
Now we have seen how easy it seems to connect to an automation object in the
ROT it would be quite nice to register an own object in the ROT so that
multiple clients can attach to it. I will take the FileManager
demo app again and will try to register that in the ROT. The API functions RegisterActiveObject
and RevokeActiveObject can be used to manipulate the ROT. In a first
attempt I will Register the FileManager in it's AfterConstruction
method.
procedure TFileZapper.AfterConstruction;
begin
...
RegisterActiveObject(self as IfileZapper, CLASS_FileZapper, ACTIVEOBJECT_WEAK, cookie);
CoLockObjectExternal(self as IfileZapper, true, true); }
RegisterActiveObject takes as parameters the filezapper object to register, the ID of the
Filezapper CoClass, a flag and an out parameter to recieve a cookie. Two values
for the flag are available ACTIVEOBJECT_WEAK
and ACTIVEOBJECT_STRONG. A strong registration will add an extra
reference to the object, preventing an early unload of the object. It is advised
to register weak, to prevent premature unloading the manager is locked using the
API function CoLockObjectExternal.
The cookie will be needed to remove the object from the ROT again, which will be
done in the BeforeDestruction method.
procedure TFileZapper.BeforeDestruction;
begin
...
CoLockObjectExternal(self as IfileZapper, false, true);
RevokeActiveObject(cookie, nil);
CoDisconnectObject(self as IfileZapper, 0); }
First the lock is released by a new call to CoLockObjectExternal.
After which I can pass the cookie of the object to be removed to RevokeActiveObject. Finally a call to CoDisconnectObject
will disconnect all clients from the FileManager object.
An in-process object in the ROT ?
At first sight everything seems to work. I can start a Delphi client app
which uses the FileManager after which I will start Word which will
connect to this same instance. All seems to work well and both client
apps respond to events in the FileManager. I can
close Word or start other apps which will connect to the object. But the moment I
shut down the first client (the Delphi app) all clients lose their FileManager
object.
To understand why we will have to understand what was going on. The Delphi
client created the original object. To do so it loaded the FileManager
DLL which registered the object in the ROT. Word and all later clients will find this
active object in the ROT and start communicating with it. They are talking to the FileManager
object in the DLL which was loaded by the Delphi app. And while doing so they
are crossing process boundaries. The DLL was an in process
server but it is hosted by another process. So now the FileManager
has become an out of process server to all except the first client. This shows
very nicely how the access to the object is fully transparent to the client. The
moment the first client shuts down it will unload the FileManager DLL and
the object inside will be gone with it. The other clients will get an "RPC
not available" error.
So an object which wants to join the ROT has to reside in an out of process
EXE. Clients cannot ask each other to stay alive because they are using that
clients object as well.
Instead of rebuilding my server to a standalone EXE I will take another
approach. A small proxy executable will create a FileManager
object and this proxy will register its interface to this object in the ROT.
fManager:= CoFileZapper.Create;
RegisterActiveObject(fManager, CLASS_FileZapper, ACTIVEOBJECT_WEAK, cookie);
CoLockObjectExternal(fManager, true, true);
Before closing down the proxy will do the clean up.
CoLockObjectExternal(fManager, false, true);
RevokeActiveObject(cookie, nil);
CoDisconnectObject(fManager, 0);
And now I am safe. As long as the proxy lives it will house a FileManager
object which will be in the ROT. The moment the proxy closes it will remove the
object from the ROT.
Now I have my FileManager in the ROT and I want to share it among several
clients. One of these clients will be written in Delphi. The FileManager
does sink events, to sink these to a Delphi client will take some effort. In my
first chapter on events I briefly introduced the EventsinkImp
tool which generates Delphi implementations of eventsinks. Let's
take another look at this handy tool. As a sidestep I will dive deeper into the
code it generates. If you get puzzled on the way it is enough to know that we
will end up with classes which hide all complexities and are simple to use.
Instead of creating a component I will use the tool to generate a plain
tObject. This can be set in the options :

When it's ready I have nice unit which wraps up all implementation
hassles of the sinks :

The unit has three classes. TFileManagerEventsBaseSink is the base
class and implements the two basic actions, connecting to and disconnecting from
the connectable object.
TFileManagerEventsBaseSink = class (TObject, IUnknown, IDispatch)
The base class implements Idispatch so it
can be used to implement the desired dispinterface.
When sinking events the invoke method of this dispinterface will be
called. This can get quite complex when the event has parameters. The generated
base class will sort these parameters out and make a call to the DoInvoke method.
This is an abstract method, we will get back to
it when we take a look at the implementation of an actual eventsink.
The base class also houses the essential variables of the connection.
FCookie : integer;
FCP : IConnectionPoint;
FSinkIID : TGUID;
A cookie needed to disconnect the sink, the connectionpoint itself and the ID of the
implemented sink. Connecting to an object involves passing the eventsink to the
object. The connect method will do just that
procedure TFileManagerEventsBaseSink.Connect (const ASource: IUnknown);
var
pcpc: IConnectionPointContainer;
begin
Assert (ASource <> nil);
Disconnect;
try
OleCheck ( ASource.QueryInterface ( IConnectionPointContainer, pcpc ) );
OleCheck ( pcpc.FindConnectionPoint ( FSinkIID, FCP ) );
OleCheck ( FCP.Advise ( Self, FCookie ) );
FSource := ASource;
except
raise Exception.Create (Format ('Unable to connect %s.'#13'%s', [ClassName, Exception (ExceptObject).Message] ));
end; { finally }
end;
The code will use QueryInterface to try to
obtain the IconnectionPointContainer
interface, when successful it will try to find the connectionpoint
for the sink class after which it can connect using Advise.
The Disconnect method will stop the sinking of events
procedure TFileManagerEventsBaseSink.Disconnect;
begin
if ( FSource = nil ) then Exit;
try
OleCheck ( FCP.Unadvise ( FCookie ) );
FCP := nil;
FSource := nil;
except
pointer (FCP) := nil;
pointer (FSource) := nil;
end; { except }
end;
This is straightforward, the connectionpoint's UnAdvise method is
called to disconnect.
My FileManager eventsinks are implemented as classes which inherit
from the events base class. For all event signatures
an appropriate type has been declared
TIFileZapperEventsOnSelectionChangedEvent = procedure (Sender: TObject) of object;
TIFileZapperEventsOnDirectoryChangedEvent = procedure (Sender: TObject; const DirName: WideString) of object;
For each event the class has a property so I can assign a method which will
actually handle the event. For each event there is a method which will actually
execute this eventhandler :
procedure TFileManagerIFileZapperEvents.DoOnDirectoryChanged(const DirName : WideString);
begin
if not Assigned ( OnDirectoryChanged ) then System.Exit;
OnDirectoryChanged (Self, DirName);
end;
The sinkclass has to implement the abstract DoInvoke method of the
baseclass.
function TFileManagerIFileZapperEvents.DoInvoke (DispID: Integer; const IID: TGUID; LocaleID: Integer; Flags: Word; var dps: TDispParams; pDispIds: PDispIdList; VarResult, ExcepInfo, ArgErr: Pointer): HResult;
type
POleVariant = ^OleVariant;
begin
Result := DISP_E_MEMBERNOTFOUND;
//SinkInvoke//
case DispId of
1 :
begin
DoOnSelectionChanged ();
Result := S_OK;
end;
2 :
begin
DoOnDirectoryChanged (dps.rgvarg^ [pDispIds^ [0]].bstrval);
Result := S_OK;
end;
end; { case }
//SinkInvokeEnd//
end;
The DoInvoke method has an enormous load of parameters, most of them are
not used here. They make up the whole set which was originally passed to the
dispinterface's Invoke method itself. The dispId identifies the
event in the sink, it is used in the case selector to fire the appropriate
event. When it comes to passing parameters, like in DoOnDirectoryChanged,
the parameter is extracted from the parameters record passed to DoInvoke.
All of this would be pretty complicated if you had to code it yourself.
Thanks to Binh Ly's tool we have a class for every eventsink supported. The
class hides all complexity
involved. The connect and a disconnect method of the class will start or stop
the sinking of events.
Inspecting the ROT
So far we have only indirectly worked with the ROT using the Get- Revoke- and
Register-
ActiveObject API functions. The ROT itself is accessible too. I will now
create a class which will inspect the ROT and can connect to any IFilezapper
instances found there. The sinking of its events will be handled by the classes
generated by the eventsink tool.
The ROT is described in the IRunningObjectTable
interface, the full declaration can be found in the VCL ActiveX.pas unit. The process
of getting objects from the ROT is called binding to an object. Many of the API
functions involved require a so-called binding context which is accessed through an IBindCtx
interface. This is an interface, declared in the VCL ActiveX.pas unit, to a global binding context.
I will create a new class, tROTmanager, which will wrap up the ROT, a
binding context and an interface to the FileManager with its events. The
sinking of events can be switched on and off. This leads to these declarations :
ROT : IRunningObjectTable;
BindCtx : IBindCtx;
fManager : IfileZapper;
fZapperEvents : TFileManagerIFileZapperEvents;
fSinkZapperEvents : boolean;
fCollectionEvents : TFileManagerISelectedFileCollectionEvents;
fSinkCollectionEvents : boolean;
The variables will be initialized in the AfterConstruction method.
procedure tROTmanager.AfterConstruction;
begin
inherited;
OleCheck(CreateBindCtx(0, BindCtx));
OleCheck(GetRunningObjectTable(0, ROT));
fZapperEvents:= TFileManagerIFileZapperEvents.Create;
fCollectionEvents:= TFileManagerISelectedFileCollectionEvents.Create; end;
The API function CreateBindCtx
will create a binding context and the GetRunningObject function will hand me
an interface to the ROT. The eventsink objects are created from the
generated code. They are at hand now to be connected to a IfileZapper
interface. When the ROTmanager object is finished I will have to free these
eventsinks :
procedure tROTmanager.BeforeDestruction;
begin
fZapperEvents.Free;
fCollectionEvents.Free;
inherited;
end;
Ok, now I have an interface to the ROT it would be very nice to be able to
show what is in the ROT. The ReportTo method will
show the ROT's contents in a tListView :
procedure ReportTo(aListView : tListView);
The ROT will be enumerated and for all objects found a listview item is created.
The running objects can be obtained by the IRunningObjectTable.EnumRunning
method. This will
result in a collection which can be traversed using the
standard Next method. All items returned are Imoniker interfaces.
Monikers are a world on itself, basically a moniker is some data with the
information how to create (or get to) an automation object out of these data. The easiest
to grasp are file-monikers. The file-name of Word document
combined with the ID of the Word.Document class is enough info to start Word and have
it open the file. I will use the moniker returned by the EnumRunning
method to get information about the object and to get to the running object
itself.
var
Enum : IEnumMoniker;
Fetched : integer;
RunningObj : IMoniker;
if ROT.EnumRunning(Enum) = S_OK then
begin
Enum.Next(1, RunningObj, @Fetched);
while RunningObj <> nil do
begin
I can enumerate the ROT and will get an Imoniker interface for every
object. To get a first idea I will ask every moniker for a display name
and will look in the registry to see if I can find some more info on the object
the moniker is describing.
reg : tRegistry;
reg:= tRegistry.create;
reg.Access:= KEY_READ;
reg.rootkey:=HKEY_CLASSES_ROOT;
The properties set indicate that I will only read from the registry and will start in classes root, that is where
all automation classes have written their info. I will store all info, including
the moniker itself in listitems. To provide a place for the moniker a tListItem
derived class is declared:
type tMonikerListItem = class(tListItem)
public
Moniker : Imoniker;
end;
It adds a placeholder for the Moniker to the standard tlistitem.
Now everything is ready to handle the objects found. The GetDisplayName
methods needs the binding context and will return something readable. This will
become the caption of the list item. Quite often the Class_ID
of the running object will be part of the displayname. The code below will try to extract a
classID and look it up in the CLSID subtree of CLASSES_ROOT in the registry. If
it finds that, it will read the registry keys for the ProgId
and the Inproc- or Local-Server. Note that the actual value of these keys is
always the default value under the key, in which case an empty string can be passed to reg.ReadString.
RunningObj.GetDisplayName( BindCtx, nil, Name );
NewItem:= tMonikerListItem.Create( ListView.Items );
ListView1.Items.AddItem( NewItem );
NewItem.Moniker:= RunningObj;
NewItem.Caption:= Name;
sp1:= pos( '{', Name );
sp2:= pos( '}', Name );
try
if ( sp1 > 0 ) and ( sp2 > 0 ) then
begin
ClassHive:= '\CLSID\' + Copy( Name, sp1, sp2-1 );
if reg.OpenKey( ClassHive, false ) then
begin
NewItem.SubItems.Add( reg.ReadString('') );
reg.CloseKey;
if reg.OpenKey( ClassHive + '\ProgId', false ) then
begin
NewItem.SubItems.Add( reg.ReadString('') );
reg.CloseKey;
end;
if reg.OpenKey( ClassHive + '\InProcServer32', false ) then
begin
NewItem.SubItems.Add( reg.ReadString('') );
reg.CloseKey;
end;
if reg.OpenKey( ClassHive + '\LocalServer32', false ) then
begin
NewItem.SubItems.Add( reg.ReadString('') );
reg.CloseKey;
end;
end;
end;
except
{It 's not a registerd CLSID }
end;
With the demoapplication I have included a ROTviewer which uses this
ROTmanager class to show the contents of the ROT. In the FormCreate fSharedManager,
a ROTmanager object is, constructed
procedure TForm1.FormCreate(Sender: TObject);
begin
fSharedManager:= tROTManager.Create;
A click on a button will now use the ReportTo method to show the contents of the ROT in
the form's listview.
procedure TForm1.Button3Click(Sender: TObject);
begin
fSharedManager.ReportTo(ListView1);
Now the contents of the ROT can be visualized :

When playing with the viewer some interesting things can be seen :
- Microsoft Office applications register a lot of objects. Word does register
Word.Application, Word basic, the default template normal.dot
and every document being editted.
- Multiple objects of the same class can be entered. I had started the FileManager
sharing proxy twice and both instances can now be found in the ROT.
- Objects will stay in the ROT until they are explicitly revoked or until
the machine reboots. When the application crashes before it revokes the
object the object will remain in the ROT. Logging on as a different user
will not clear the ROT, the objects will stay there until the machine is
rebooted.
Getting the FileManager from the ROT
Now I can see the FileManager objects in the ROT, I would like my
ROTmanager to connect to them and let the object sink its events into my
ROTmanager. In the second tab of the viewer there are some list- and check-
boxes as well as some eventhandlers.
procedure TForm1.EventHandler1(sender: tObject);
begin
TrackBar1.Position:= TrackBar1.Position + 1;
end;
procedure TForm1.EventHandler2(sender: tObject; const DirName : Widestring);
begin
ListBox1.Items.Add(DirName);
end;
procedure TForm1.EventHandler3(sender : tObject; const FileName : Widestring);
begin
ListBox2.Items.Add(FileName);
end;
In the formcreate these eventhandlers are bound to the various eventhandler
properties of the sinks
fSharedManager.ZapperEvents.OnSelectionChanged:= EventHandler1;
fSharedManager.ZapperEvents.OnDirectoryChanged:= EventHandler2;
fSharedManager.CollectionEvents.OnAdd:= EventHandler3;
To actually recieve the events I will have to connect the sinks. The
ROTmanager has a method ConnectSinks which will (dis-)connect the sinks.
procedure tROTmanager.ConnectSinks(sender: tObject);
begin
if fManager <> nil then
begin
if SinkZapperEvents then
fZapperEvents.Connect(fManager)
else
fZapperEvents.Disconnect;
if SinkCollectionEvents and (fManager.Selected <> nil) then
fCollectionEvents.Connect(fManager.Selected)
else
fCollectionEvents.DisConnect;
end;
end;
The method has to check if it has an interface to the filemanager
object. After which it will connect or disconnect the sinks according to the
values of the SinkxxxEvents properties. The values of these flags indicating the
wish to actually sink events are set
by a checkbox :
procedure TForm1.CheckBox1Click(Sender: TObject);
begin
fSharedManager.SinkZapperEvents:= (sender as tCheckBox).Checked;
fSharedManager.ConnectSinks(sender);
end;
After the checkbox'es value has changed the new value is passed to the manager
and the manager is asked to update the state of the sinks.
The only thing which is missing is the fManager itself. I will get ths from
the ROT. When I click a
listitem it is queried to see if it supports the IFileManager interface. If so I will assign
the associated object in the ROT to this fManager variable in my ROTmanager
object.
Each listitem had a moniker to an object in the ROT. I will create a listviewselectitem
handler which will use this Moniker to get the object :
procedure tROTmanager.ListViewSelectItem(Sender: TObject; Item: TListItem; Selected: Boolean);
var ppUnk : Iunknown;
begin
if not selected then exit;
try
OleCheck(ROT.GetObject((Item as tMonikerListItem).Moniker, ppUnk));
{ Try if it supports IFileZapper }
ppUnk.QueryInterface(IID_IfileZapper, fManager);
ConnectSinks(sender);
except
raise;
end;
end;
The selected listviewitem will be of type tMonkerListItem,
after a Typecast I can get to the moniker. This moniker is passed to
IRunningObjectTable.GetObject, which will pass back the running object as an
Iunknown. Using QueryInterface I can try to extract its IfileZapper interface.
The call to QueryInterface is not checked with the OleCheck
function. Not every object in the ROT will be an IfileZapper. If it is something else
this will result in fManager being set to nil. There is no need to propagate
this in an exception. A call to ConnectSinks will
check the state of the sinks.

And all works well. The viewer grabbed the FileManager from the ROT
and the filemanager now sinks its events to the viewer.
Introducing the SharedFileManager
The filemanager has been made available by a proxy. This
scenario has a small drawback. The moment the proxy starts it will create a FileManager object, which will be running in vain as long as no
clients are using it. It would be nice if the object was not created until
needed. To get this accomplished I will wrap up the filemanager object in a
sharedFileManager object and register this
sharedFileManager object in the ROT. The contained
filemanager will not be created until it is explicitly asked for.
The sharedfilemanager is also included with the demoapp.
The project contains the automationclass SharedZapper. It's typelibarary
will use the filemanager typelibrary. In the typelib editor the library has an uses tab.
Here I click the right mouse
button, choose "show all" and check the filemanager library

Now I can use all types defined in the filemanager typelibrary in this
typelibrary. The
sharedzapper class wraps up filezapper and its events. To publish this I have to add
the interfaces implemented by filezapper in the implements tab of the SharedFileZapper class

The IfileZapperEvents interface should be marked as source (of events) and as
default.
The fact that the SharedZapper implements all these interface is reflected in
its declaration in the unit:
type
TSharedZapper = class(TAutoObject, ISharedZapper, IFileZapper, IconnectionPointContainer)
SharedZapper will delegate the actual implementation of the IFileZapper
interfaces to properties using the implements keyword
protected
property Mananger : IFileZapper read GetManager implements IFileZapper;
property CpC : IconnectionPointContainer read GetContainer implements IconnectionPointContainer;
This states that every queryinterface on the object for an IfileZapper
interface will be redirected to the Manager property and every
queryinterface for an IconnectionPointContainer will be redirected to the CpC
property.
Bringing all of this together leads to :
type
TSharedZapper = class(TAutoObject, ISharedZapper, IFileZapper, IconnectionPointContainer)
private
fManager : IFileZapper;
cookie : integer;
function GetManager: IFileZapper;
function GetContainer: IconnectionPointContainer;
public
procedure BeforeDestruction; override;
protected
property Mananger : IFileZapper read GetManager implements IFileZapper;
property CpC : IconnectionPointContainer read GetContainer implements IconnectionPointContainer;
end;
The implementation of GetManager will initiate the original FileZapper
object and register it in the ROT in the by now well known way.
function TSharedZapper.GetManager: IFileZapper;
begin
if fManager = nil then
begin
fManager:= CoFileZapper.Create;
RegisterActiveObject(fManager, CLASS_FileZapper, ACTIVEOBJECT_WEAK, cookie);
CoLockObjectExternal(fManager, true, true);
end;
result:= fManager;
end;
GetContainer is needed when somebody wants to connect a sink to this object.
All GetContainer has to do is redirect the request to the contained fManager
object.
function TSharedZapper.GetContainer: IconnectionPointContainer;
begin
if fManager <> nil then
fManager.QueryInterface(IconnectionPointContainer, result);
end;
The implementation of the SharedZapper class is now complete. When the
sharedFileManager loads it will create a SharedZapper object and register
this in the ROT:
fSharedZapper:= CoSharedZapper.Create;
RegisterActiveObject(fSharedZapper, CLASS_SharedZapper, ACTIVEOBJECT_WEAK, cookie);
CoLockObjectExternal(fSharedZapper, true, true);
When the ROTmanager binds to the object in the ROT it will queryinterface
the obtained object for IfileZapper. And that
is exactly the moment the property-getter GetManager comes into action and
will create the actual FileZapper object. When a client tries to connect
a sink to the object it will queryinterface for
IConnectionPointContainer. And that is exactly the moment the
property-getter GetContainer will come into action.
A few words on MS-Word
As it seems I have now reached all my goals. With the SharedFilManager I can
share one instance of the FileManager among multiple clients and I
can wait with the creation of the object itself until the moment it is actually
needed. In the chapter on events I have used MS-Word
as an events sinking client.
For Word to work with the active FileManager object involves a small
change in the document open.
Private Sub Document_Open()
' Try to connect to running FileManager
On Error GoTo CreateNew
Set MyManager = GetObject(, "FileManager.FileZapper")
GoTo Created
CreateNew:
Set MyManager = New FileManager.FileZapper
Created:
MyManager.FileMask = "*.*"
Word will work with the sharedmanager and Word will recieve the same events
as the Delphi client. But Word does have a bug which will show when Word is shut
down before the SharedManager is shut down. To cite MS support : "[this
is] a
known issue with Word's shutdown routine if COM proxies are still Ref counted
when Word calls CoUninitialize. In this case COM will attempt to disconnect and
free the proxies, but Word has already forced VBA and oleaut32 to unload before
CoUnint is called, so the proxy will fault on the disconnect"

What is worse is that Word did not yet revoke its objects from the ROT when
crashing. After restarting Word the ROT will be filled with double entries :

And after closing Word there will remain Word entries in the ROT. I will
have to restart my machine to get rid of them.
Where are we ?
We have met the global ROT. This is a central place where all automation
clients can look for automation objects that are already running. The client can
use these objects and can connect its eventsinks to the object. An automation
object can implement multiple interfaces. Handing the implementation of an
interface to a property can postpone the initialization of the implementing code
to the moment the interface is actually requested.
What's next
|