Title Connectable COM Objects
Author Andre v.d. Merwe dart@iafrica.com
Published March 1998 - Pinnacle Delphi Developer
Copyright Reprinted with permission from Pinnacle Publishing, Inc. http://www.pinpub.com
Downloads
  Back to the Zen Home Page



Introduction:

A connectable COM object is one that supports events. This article shows how to implement connectable COM objects in Delphi 3. The specification for connectable COM objects is well defined by Microsoft. Implementing events in the fashion described in this article will ensure that your COM servers / clients will work seamlessly with other servers / clients. This article will show how to use COM classes new to Delphi 3 to greatly simplify creating COM objects.

 

What is a "Connectable COM Object"?:

A connectable COM object, or source, is a COM object that supports events. In other words it can fire events in a client. Think of an ActiveX control, say a button. When you click the button an event is fired and the container is notified. This is accomplished with connectable COM objects. This article is not about ActiveX, instead I'll create two very simple examples to demonstrate this concept.

 

Client and Server:

When dealing with connectable COM objects there are two distinct objects, the client and the server. The client connects to the server. When some condition is met the server fires and event and the client responds. This can be likened to a Delphi component. The application (client) is connected to the server (component). When some condition is met in the component (server) an event is fired and the client responds.

 

Connection Points and Connection Point Containers

From here on the OLE technology starts to kick in… So take a sip of your favourite cafinated drink and enjoy! Before I get started I'd just like to mention that OLE does not stand for anything anymore. It is just OLE.

To make this discussion a little less abstract I'll explain with the aid of an example.

First an event is needed. As with all COM objects an interface is used to define function(s)/procedure(s) that will be implemented. Here I define an interface, IMyEvents, with one method (event) OnMyEvent.

IMyEvents = interface

['{blablabla}']

procedure OnMyEvent; stdcall;

end;

The blablabla part is where you would place a CLSID, which is generated by pressing ctrl-alt-g while in the Delphi 3 IDE.

The client implements OnMyEvent, and the server causes this event to be fired (executed). Again think of a Delphi component. The component fires and event, which causes code defined by the user (client) to be executed. This is important, the server fires the event but it is the client that implements it.

In COM each 'event interface', as above, is contained in a connection point. A connection point is defined by the IConnectionPoint interface. The connection point's responsibilities include keeping a list of client that are connected to it. In other words multiple clients can be connected to a single connection point.

A server can have multiple connection points, and thus support many 'event interfaces'. A connection point container contains these connection points. IConnectionPointContainer is the interface that defines a connection point container.

See Figure 1 for a depiction of this relation.

Figure 1. Conceptual layout of the client/server relation.

 

 

By now you might well be asking whether all this complexity is really necessary for something as simple as events. The reason is that COM is designed to be extensible, and thus the most generic approach is chosen. It is important to remember that the client and server may be written by different people, in fact that is probably the norm. So the client and server COM objects must be able to function with absolutely no knowledge of how the other one is implemented, other than to know that it is a connectable COM client / server.

Having said this I honestly believe that understanding this particular COM technology is not that difficult. Once you've implemented it your self it should make perfect sense.

 

Delphi:

If you have used connectable COM objects in C/C++ you will know that there is quite a bit of overhead that must be dealt with. For instance another responsibility of the IConnectionPointContainer is to enumerate all contained connection points.

Thankfully Delphi 3 hides must of the drudgery from you. IConnectionPointContainer is implemented by TConnectionPoints, IConnectionPoint by TConnectionPoint. Making use of these classes greatly simplifies COM programming. For other useful COM classes take a look at AxControls.pas in the Delphi 3 source directory (Professional and C/S versions of Delphi only).

 

The Example:

Now that the basics are covered I will go through the steps of creating both the client and server objects. The server has one callable method - GetPinNumber. When this method is called the server sets the given window handle's caption to a randomly selected number. If any clients are connected to the server the server will fire the OnMyEvent event in each of them.

The server is implemented as a DLL, the client as a EXE.

I will now explain each unit of the server then of the client. I will first give an overview of what must be done for each unit as well as an overview for each function. The overview for each function will be followed by an explanation of what the source code is doing, in point form.

Before continuing, download this months subscriber downloads from the pinnacle web site. Print the following files, which together make up the example that I'm about to start explaining.

 

Server Files

PinServer.dpr

PinServer_U1.pas

PinServer_Def.pas

Client

PinClient_U1.pas

Once you've printed the source you can refer to it and see exactly how I implement the section that I'm discussing.

 

 

Server DLL: - .DPR

Start a new DLL project (File-New DLL), this will be the server DLL.

Every COM DLL must export 4 functions. These functions are DllGetClassObject, DllCanUnloadNow, DllRegisterServer and DllUnregisterServer.

Add these 4 functions to the DLLs exports. Once again Delphi comes to the rescue and implements these functions for you. Just include ComServ in the uses clause.

Also add the {$R *.res} line. This is required for later when the server is registered. Ignore the {$R *.TLB} this will automatically be added by Delphi.

Now add two units to the project, one for the interface definitions and one for the COM server implementation. Your .DPR file should now look like PinServer.dpr

 

 

Server DLL: - Definition

Two interfaces are required for this example. The server's interface with one method - GetPinNumber, and the event interface with one event - OnPinEvent. You should generate new CLSIDs for these interfaces, you can do this by pressing ctrl-alt-g in the Delphi 3 IDE.

Two constants are also defined. These constants are GUIDs and are used to make referencing the required interface easier. Were it not for these constants you would have to type the whole GUID every time. IID_IPinServerEvents is the event interfaces GUID, CLSID_IPinServer is the servers GUID.

See PinServer_Def.pas

 

Server DLL: - Implementation

Now the last unit of the server, this is also the most complex of the units. Add ActiveX, ComServ, AxControls, and PinServer_Def to the units uses clause. PinServer_Def is the name of the definition unit, as explained above.

Refer to PinServer_U1.pas as I explain the class step by step. The first thing that is needed is a class for the COM object. The class is derived from TComObject which implements all the basic COM functions. The server must also support IPinServer so add it to the classes declaration. By doing this you are indication that the class will support all methods in the IPinServer interface. In this example there is only one method to support - GetPinNumber.

Add the Initialize and Destory methods. Destroy is the destructor, but Initialize is not the constructor. Do not use the constructor to do initialisation in a COM class, always use the Initialize method. Next add GetPinNumber and QueryInterface.

 

Server DLL: - Implementation - Initialize

  1. Call the base classes' Initialize method, so that all the necessary COM initialisation can take place.
  2. Create the connection point container. Remember that this is the class that will contain every connection point supported by the server. In this example there is only one connection point.
  3. Create the connection point. This is accomplished by calling the CreateConnectionPoint method of cpcContainer (TConnectionPoints).
    The first parameter is the CLSID of the supported interface, which in this case is IPinServerEvents. The second parameter ckMulti indicates that multiple clients can connect to this connection point.
    The final parameter, nil, specifies that no function is to be called when a client connects or disconnects to this connection point.

 

 

Server DLL: - Implementation - Destroy

  1. Indicate that cpEvents is no longer needed.
  2. Free the connection point container.
  3. Call the inherited destroy method.

 

Server DLL: - Implementation - QueryInterface

When a client wants to connect to a server it requests the server's IConnectionPointContainer interface. If you take a look at the definition of the TPinServer (PinServer_U1.pas) class you will see that the class does not implement this interface.

The class does however have a variable called cpcConnection which is a TConnectionPoints class, which is exactly what the client is looking for. For this reason you need to intercept all calls to the servers QueryInterface.

 

 

  1. Allow the default QueryInterface to look for the requested interface.
  2. If the interface was not found, Result <> S_OK, then…
  3. If the client is requesting IConnectionPointContainer then…
  4. Pass the request on to cpcContainer by calling its GetInterface method.
  5. If the interface is found return S_OK else return E_NOINTERFACE.

 

 

Server DLL: - Implementation - GetPinNumber

GetPinNumber is the procedure where most of the work takes place. This procedure has 2 main tasks. First it must set the given window's caption to a random integer. It must then enumerate through all connected clients and fire their OnPinEvent events.

Task 1)

  1. Generate random number.
  2. Set the window's caption

 

Task 2)

  1. If there is no connection point, don't bother continuing.
  2. Get the IConnectionPoint interface from cpEvents.
  3. Get the IEnum interface for this connection point, if it is not found exit.
  4. For every connected client do…
  5. Get the IPinServerEvents interface for the connection point and fire the OnPinEvent event.
  6. Finished with the connection point data.

Take a careful look at these steps again. There is not much code but quite a bit is accomplished, it is important that you understand what is going on.

 

To summarise task 2

  1. Get connection point
  2. Enumerate through all clients connected to this connection point, firing OnPinEvent for every one.

 

 

Server DLL: - Implementation - The Class Factory

A class factory is used to create an instance of the server. When you call CreateComObject(…) from the client, the servers class factory is used called on to create an instance of the class.

Yet again Delphi 3 greatly simplifies this task, all that you need to do is create an instance of TComObjectFatory. See PinServer_U1.pas to see how this is done.

 

Server DLL: - Implementation - Register the Server

The COM server is now complete. It must be registered before it can be used though. There are two ways to do this. The easiest is to register from within Delphi, select "Register ActiveX Control" from the "Run" menu.

You can also use regserv32.exe which is found in the windows system directory. This is the method you would use to install a server on someone else's machine. To register a server using regserv32, give the path to the server DLL as parameter on. For example "c:\windows\system\regsvr32 PinServer.dll"

 

See PinServer_U1.pas

 

 

PinClient:

At this point you have a fully functional COM server, this is not much use without a client to use it though. So start a new application that will be used as the client.

Drop 3 buttons and an edit control on to the main form. See Figure 2 for the layout and component captions.

 

Figure 2. The client's main form

Add ActiveX, ComObj and PinServer_Def to the units uses clause. PinServer_Def is the unit that contains the servers interface definitions, and CLSID constants.

Since this is just a normal application at this stage the main form's class (TForm1) is derived from TForm. To convert this to a class capable of being the COM server's client, you need to add both IUnknown and IPinServerEvents.

Adding IPinServerEvents means that that this class will support the OnPinEvent event, which is to be fired by the server. When this client tries to connect to the server, the server will perform a QueryInterface on the client to ensure that it does indeed support this interface. IUnknown is needed so that this class can act as a COM client. Every COM object must support IUnknown, in fact that is what makes it a COM object!

Add the 3 IUnknown methods and the IPinServerEvent's OnPinEvent method. As with the server I'll explain each of these methods, using point form if necessary.

 

PinClient: - QueryInterface

Very simple method. Just call TForm's GetInterface to do the interface lookup. Return S_OK if the interface is found else return E_NOINTERFACE.

 

PinClient: - _AddRef and _Release

The client does not need to implement reference counting. The client object is contained within the main form class. This class is created when the application is stated and destroyed when the application ends. The client's lifetime is thus 'contained' by the main form's class and reference counting is therefore not needed.

Both _AddRef and _Release just return 1. Dont return 0 as Delphi will then try to free the running application. This would be a nasty bug! Return 1 and you can forget about these two methods.

 

PinClient: - OnPinEvent

This is the method that is called by the server when the event is fired.

Here I use ShowMessage to display the received number.

 

PinClient: - OnCreate

  1. Call CoInitialize to ensure that the COM libraries are loaded. Do not forget this call in your COM executables!
  2. Create an instance of the server. This is accomplished by calling CreateComObject and passing the CLSID of the COM object that you want created. CreateComObject returns a IUnknown interface, this is not much use so I immediately get the COM object's IPinServer interface.

 

PinClient: - "Get Number" Button

  1. Call the servers GetPinNumber method. Pass the handle of the edit control as parameter 1.

The server will respond regardless of whether the client is connected to it or not.

 

PinClient: - "Connect" Button

  1. Get the server's connection point container
  2. Find the IPinServerEvents connection point. In effect asking the server if it supports this event interface.
  3. If the connection point is found then…
  4. Connect to the server by calling the Advise method. Parameter 1 is the client to be connected, which is 'self'. The server will query this client to ensure that it supports the requested event interface. The second parameter, Cookie, is a DWORD variable. The server assigns a unique number to this variable, this is then used to uniquely identify this connection.

 

 

PinClient: - "Disconnect" Button

  1. If there is a valid connection point then…
  2. Disconnect by calling UnAdvise. Parameter 1 is the cookie value returned when the connection was established.

 

 

PinClient: - OnDestroy

  1. Disconnect
  2. Finished with the server
  3. Call CoUnInitialize since the application is finished with the COM libraries.

 

See PinClient_U1.pas

 

 

Overview

You now have a working COM server and client. I'm certain that after you've read through this article you'll agree that it was not too difficult. There is a bit of overhead code that must be written for each client and server, but the results are well worth it.

Having seen all the code you can now see how the client and server interact. Here is a very summarised version of this interaction

  1. Client starts and creates the server
  2. Client gets server's IConnectionPointContainer
  3. Client gets IConnectionPoint
  4. Client calls Advise so that it will be connected to the server
  5. Server calls client's QueryInterface to ensure that the client supports the necessary events.
  6. Server adds client to list of connected clients, and returns a unique cookie value.




 All information on these www pages is copyright (©) 1997 Andre .v.d. Merwe And may not be copied or mirrored without my permission.

="#ff0000">

 All information on these www pages is copyright (©) 1997 Andre .v.d. Merwe And may not be copied or mirrored without my permission.