From COM to COM+
Overview
For about 10 years, starting soon after the release of Windows 3.0, Microsoft has been promising that its operating system and its API would be based on a real object model instead of functions. According to the speculations, Windows 95 (and later Windows 2000) should have been based on this revolutionary approach. Nothing like this happened, but Microsoft kept pushing COM (Component Object Model), built the Windows 95 shell on top of it, pushed applications integration with COM and derivative technologies (such as Automation), and reached the peak by introducing COM+ with Windows 2000.
Now, soon after the release of the complete foundation required for high-level COM programming, Microsoft has decided to switch to a new core technology, part of the .NET initiative. My impression is that COM wasn't really suited for the integration of fine-grained objects, although it succeeded in providing an architecture for integrating applications or large objects.
In this chapter, you'll build your first COM object; I'll stick to the basic elements to let you understand the role of this technology without delving heavily into the details. We'll continue by discussing Automation and the role of type libraries, and you'll see how to work with Delphi data types in Automation servers and clients.
In the final part of the chapter, we'll explore the use of embedded objects, with the OleContainer component, and the development of ActiveX controls. I'll also introduce stateless COM (MTS and COM+) technologies and a few other advanced ideas including the .NET integration support offered by Delphi 7.
A Short History of OLE and COM
Part of the confusion related to COM technology comes from the fact that Microsoft used different names for it in the early years for marketing reasons. Everything began with Object Linking and Embedding (OLE, for short), which was an extension of the DDE (Dynamic Data Exchange) model. Using the Clipboard allows you to copy raw data, and using DDE allows you to connect parts of two documents. OLE lets you copy data from a server application to a client application, along with information regarding the server or a reference to information stored in the Windows Registry. The raw data might be copied along with the link (object embedding) or kept in the original file (object linking). OLE documents are now called active documents.
Microsoft updated OLE to OLE 2 re-implementing it not to be just an extension to DDE and began adding new features, such as OLE Automation and OLE Controls. The next step was to build the Windows 95 shell using OLE technology and interfaces and then to rename the OLE Controls (previously known also as OCX) as ActiveX controls, changing the specification to allow for lightweight controls suitable for distribution over the Internet. For a while, Microsoft promoted ActiveX controls as suitable for the Internet, but the idea was never fully accepted by the development community—certainly not as "suitable" for Internet development.
As this technology was extended and became increasingly important to the Windows platform, Microsoft changed the name back to OLE, and then to COM, and finally to COM+ for Windows 2000. These changes in naming were only partially related to technological changes and were driven to a large extent by marketing purposes.
What, then, is COM? Basically, the Component Object Model is a technology that defines a standard way for a client module and a server module to communicate through a specific interface. Here, module indicates an application or a library (a DLL); the two modules may execute on the same computer or on different machines connected via a network. Many interfaces are possible, depending on the role of the client and server, and you can add new interfaces for specific purposes. These interfaces are implemented by server objects. A server object usually implements more than one interface, and all the server objects have a few common capabilities, because they must all implement the IUnknown interface (which corresponds to the Delphi-specific IInterface I introduced in Chapter 2, "The Delphi Programming Language").
The good news is that Delphi is fully compliant with COM. When Delphi 3 came out its COM implementation was much easier and integrated in the language than than C++ or other languages were at that time, up to the point that even programmers on the Windows R&D team commented "we should have done COM the way Delphi does COM." This simplicity mainly derives from the incorporation of interface types into the Delphi language. (Interfaces are also used in a similar way to integrate Java with COM on the Windows platform.)
As I've mentioned, the purpose of COM interfaces is to communicate between software modules, which can be executable files or DLLs. Implementing COM objects in DLLs is generally simpler, because in Win32, a program and the DLL it uses reside in the same memory address space. This means that if the program passes a memory address to the DLL, the address remains valid. When you use two executable files, COM has a lot of work to do behind the scenes to let the two applications communicate. This mechanism is called marshaling (which, to be accurate, is required also by DLLs if the client is multi-threaded). Note that a DLL implementing COM objects is described as an in-process server, whereas when the server is a separate executable, it is called an out-of-process server. However, when DLLs are executing on another machine (DCOM) or inside a host environment (MTS), they are also out-of-process.
Implementing IUnknown
Before we begin looking at an example of COM development, I'll introduce a few COM basics. Every COM object must implement the IUnknown interface, also dubbed IInterface in Delphi for non-COM usage of interfaces (as you saw in Chapter 2). This is the base interface from which every Delphi interface inherits, and Delphi provides a couple of different classes with ready-to-use implementations of IUnknown/IInterface, including TInterfacedObject and TComObject. The first can be used to create an internal object unrelated to COM, and the second is used to create objects that can be exported by servers. As you'll see later in this chapter, several other classes inherit from TComObject and provide support for more interfaces, which are required by Automation servers or ActiveX controls.
As mentioned in Chapter 2, the IUnknown interface has three methods: _AddRef, _Release, and QueryInterface. Here is the definition of the IUnknown interface (extracted from the System unit):
type IUnknown = interface ['{00000000-0000-0000-C000-000000000046}'] function QueryInterface(const IID: TGUID; out Obj): Integer; stdcall; function _AddRef: Integer; stdcall; function _Release: Integer; stdcall; end;
The _AddRef and _Release methods are used to implement reference counting. The QueryInterface method handles the type information and type compatibility of the objects.
Note |
In the previous code, you can see an example of an out parameter, a parameter passed back from the method to the calling program but without an initial value passed by the calling program to the method. The out parameters have been added to the Delphi language to support COM, but they can as well be used in a normal application, as in certain circumstances this makes parameters passing more efficient (significant cases are those of interfaces, strings, and dynamic arrays). It's also important to note that although Delphi's language definition for the interface type is designed for compatibility with COM, Delphi interfaces do not require COM. This was already highlighted in Chapter 2, where I built an interface-based example with no COM support. |
You don't usually need to implement these methods, because you can inherit from one of the Delphi classes already supporting them. The most important class is TComObject, defined in the ComObj unit. When you build a COM server, you'll generally inherit from this class.
TComObject implements the IUnknown interface (mapping its methods to ObjAddRef, ObjQuery Interface, and ObjRelease) and the ISupportErrorInfo interface (through the InterfaceSupports-ErrorInfo method). Notice that the implementation of reference counting for the TComObject class is thread-safe, because it uses the InterlockedIncrement and InterlockedDecrement API functions instead of the plain Inc and Dec procedures.
As you would expect if you remember the discussion of reference counting in Chapter 2, the _Release method of TInterfacedObject destroys the object when there are no more references to it. The TComObject class does the same. Also keep also in mind that Delphi automatically adds the reference-counting calls to the compiled code when you use interface-based variables, including COM variables.
Finally, notice that the role of the QueryInterface method is twofold:
- QueryInterface is used for type checking. The program can ask an object the following questions: Are you of the type I'm interested in? Do you implement the interface and the specific methods I want to call? If the answers are no, the program can look for another object, maybe asking another server.
- If the answers are yes, QueryInterface usually returns a pointer to the object, using its reference output parameter (Obj).
To understand the role of the QueryInterface method, it is important to keep in mind that a COM object can implement multiple interfaces, as the TComObject class does. When you call QueryInterface, you ask for one of the possible interfaces of the object, using the TGUID parameter.
In addition to the TComObject class, Delphi includes several other predefined COM classes. Here is a list of the most important COM classes of the Delphi VCL, which you'll use in the following sections:
- TTypedComObject, defined in the ComObj unit, inherits from TComObject and implements the IProvideClassInfo interface (in addition to the IUnknown and ISupportErrorInfo interfaces already implemented by the base class, TComObject).
- TAutoObject, defined in the ComObj unit, inherits from TTypedComObject and implements also the IDispatch interface.
- TActiveXControl, defined in the AxCtrls unit, inherits from TAutoObject and implements several interfaces (IPersistStreamInit, IPersistStorage, IOleObject, and IOleControl, to name just a few).
Globally Unique Identifiers
The QueryInterface method has a parameter of the TGUID type. This type represents a unique ID used to identify COM object classes (in which case the GUID is called CLSID), interfaces (in which case you'll see the term IID), and other COM and system entities. When you want to know whether an object supports a specific interface, you ask the object whether it implements the interface that has a given IID (which for the default COM interfaces is determined by Microsoft). Another ID is used to indicate a specific class or CLSID. The Windows Registry stores this CLSID with indications of the related DLL or executable file. The developers of a COM server define the class identifier.
Both of these IDs are known as GUIDs, or globally unique identifiers. If each developer uses a number to indicate its COM server, how can we be sure these values are not duplicated? The short answer is that we cannot. The real answer is that a GUID is such a long number (16 bytes, or 128 bits—a number with 38 digits!) that it is almost impossible to come up with two random numbers having the same value. Moreover, programmers should use the specific API call CoCreateGuid (directly or through their development environment) to come up with a valid GUID that reflects some system information.
GUIDs created on machines with network cards are guaranteed to be unique, because network cards contain unique serial numbers that form a base for the GUID creation. GUIDs created on machines with CPU IDs (such as the Pentium III) should also be guaranteed unique, even without a network card. With no unique hardware identifier, GUIDs are unlikely to ever be duplicated.
Warning |
Be careful not to copy the GUID from someone else's program (which can result in two different COM objects using the same GUID). You should also not make up your own ID by entering a casual sequence of numbers. To avoid any problem, press Ctrl+Shift+G in the Delphi editor, and you will get a new, properly defined, truly unique GUID. |
In Delphi, the TGUID type (defined in the System unit) is a record structure, which is quite odd but required by Windows. Thanks to some Delphi compiler magic, typically set up to help make more straightforward some tedious or time consuming task, you can assign a value to a GUID using the standard hexadecimal notation stored in a string, as in this code fragment:
const Class_ActiveForm1: TGUID = '{1AFA6D61-7B89-11D0-98D0-444553540000}';
You can also pass an interface identified by an IID where a GUID is required, and again Delphi will magically extract the referenced IID. If you need to generate a GUID manually and not in the Delphi environment, you can call the CoCreateGuid Windows API function, as demonstrated by the NewGuid example (see Figure 12.1). This example is so simple that I've decided not to list its code.
Figure 12.1: An example of the GUIDs generated by the NewGuid example. Values depend on my computer and the time I run this program.
To handle GUIDs, Delphi provides the GUIDToString function and the opposite StringToGUID function. You can also use the corresponding Windows API functions, such as StringFromGuid2, but in this case, you must use the WideString type instead of the string type. Any time COM is involved, you have to use the WideString type, unless you use Delphi functions that automatically do the required conversion for you. When you need to bypass Delphi functions that can call COM API functions directly, you can use the PWideChar type (pointer to null-terminated arrays of wide characters) or casting a WideString to PWideChar (exactly as you cast a string to the PChar type when calling a low-level Windows API.) does the trick.
The Role of Class Factories
When have registered the GUID of a COM object in the Registry, you can use a specific API function to create the object, such as the CreateComObject API:
function CreateComObject (const ClassID: TGUID): IUnknown;
This API function will look into the Registry, find the server registering the object with the given GUID, load it, and, if the server is a DLL, call the DLLGetClassObject method of the DLL. This is a function every in-process server must provide and export:
function DllGetClassObject (const CLSID, IID: TGUID; var Obj): HResult; stdcall;
This API function receives as parameters the requested class and interface, and it returns an object in its reference parameter. The object returned by this function is a class factory.
As the name suggests, a class factory is an object capable of creating other objects. Each server can have multiple objects. The server exposes a class factory for each of the COM objects it can create. One of the many advantages of the Delphi simplified approach to COM development is that the system can provide a class factory for you. For this reason, I didn't add a custom class factory to my example.
The call to the CreateComObject API doesn't stop at the creation of the class factory, however. After retrieving the class factory, CreateComObject calls the CreateInstance method of the IClassFactory interface. This method creates the requested object and returns it. If no error occurs, this object becomes the return value of the CreateComObject API.
By setting up this mechanism (including the class factory and the DLLGetClassObject call), Delphi makes it simple to create COM objects. At the same time, Window's CreateComObject is just a simple function call with complex behavior behind the scenes. What's great in Delphi is that many complex COM mechanisms are handled for you by the RTL. Let's begin looking in detail at how Delphi makes COM easy to master.
For each of the core VCL COM classes, Delphi also defines a class factory. The class factory classes form a hierarchy and include TComObjectFactory, TTypedComObjectFactory, TAutoObjectFactory, and TActiveXControlFactory. Class factories are important, and every COM server requires them. Usually Delphi programs use class factories by creating an object in the initialization section of the unit defining the corresponding server object class.
A First COM Server
There is no better way to understand COM than to build a simple COM server hosted by a COM server DLL. A library hosting a COM object is indicated in Delphi as an ActiveX library. For this reason, you can begin the development of this project by selecting File ® New ® Other, moving to the ActiveX page, and selecting the ActiveX Library option. Doing so generates a project file I saved as FirstCom among the book demos. Here is its complete source code:
library FirstCom; uses ComServ; exports DllGetClassObject, DllCanUnloadNow, DllRegisterServer, DllUnregisterServer; {$R *.RES} begin end.
The four functions exported by the DLL are required for COM compliance and are used by the system as follows:
- To access the class factory (DllGetClassObject)
- To check whether the server has destroyed all its objects and can be unloaded from memory (DllCanUnloadNow)
- To add or remove information about the server in the Windows Registry (DllRegisterServer and DllUnregisterServer)
You generally don't have to implement these functions, because Delphi provides a default implementation in the ComServ unit. For this reason, in the server code, you only need to export them.
COM Interfaces and Objects
Now that the structure of your COM server is in place, you can begin developing it. The first step is to write the code of the interface you want to implement in the server. Here is the code of a simple interface, which you should add to a separate unit (called NumIntf in the example):
type INumber = interface ['{B4131140-7C2F-11D0-98D0-444553540000}'] function GetValue: Integer; stdcall; procedure SetValue (New: Integer); stdcall; procedure Increase; stdcall; end;
After declaring the custom interface, you can add the object to the server. To accomplish this, you can use the COM Object Wizard (available in the ActiveX page of the File ® New ® Other dialog box). You can see this wizard's dialog box in Figure 12.2. Enter the name of the server's class and a description. I've disabled the generation of the type library (in which case the wizard disables the interface field in Delphi 7, different from what happened in Delphi 6) to avoid introducing too many topics at once. You should also choose an instancing and a threading model, as described in the related sidebar.
Figure 12.2: The COM Object Wizard
The code generated by the COM Object Wizard is quite simple. The interface contains the definition of the class to fill with methods and data:
type TNumber = class(TComObject, INumber) protected {Declare INumber methods here} end;
Beside the GUID for the server (saved in the Class_Number constant), there is also code in the initialization section of the unit, which uses most of the options you've set up in the wizard's dialog box:
initialization TComObjectFactory.Create(ComServer, TNumber, Class_Number, 'Number', 'Number Server', ciMultiInstance, tmApartment);
This code creates an object of the TComObjectFactory class, passing as parameters the global ComServer object, a class reference to the class you've just defined, the GUID for the class, the server name, the server description, and the instancing and threading models you want to use.
The global ComServer object, defined in the ComServ unit, is a manager of the class factories available in the server library. It uses its own ForEachFactory method to look for the class supporting a given COM object request, and it keeps track of the number of allocated objects. As you've already seen, the ComServ unit implements the functions required by the DLL to be a COM library.
Having examined the source code generated by the wizard, you can now complete it by adding to the TNumber class the methods required for implementing the INumber interface and write their code, and you'll have a working COM object in your server.
COM Instancing and Threading Models
When you create a COM server, you should choose a proper instancing and threading model, which can significantly affect the behavior of the COM server.
Instancing affects primarily out-of-process servers (any COM server in a separate executable file, rather than a DLL) and can assume three values:
Multiple Indicates that when several client applications require the COM object, the system starts multiple instances of the server
Single Indicates that, even when several client applications require the COM object, there is only one instance of the server application; it creates multiple internal objects to service the requests
Internal Indicates that the object can only be created inside the server; client applications cannot ask for one (this specific setting affects also in-process servers).
The second decision relates to the COM object's thread support, which is valid for in-process servers only (DLLs). The threading model is a joint decision of the client and the server application: If both sides agree on one model, it is used for the connection. If no agreement is found, COM can still set up a connection using marshaling, which can slow down the operations. Also keep in mind that a server must not only publish its threading model in the Registry (as a result of setting the option in the wizard); it must also follow the rules for that threading model in the code. Here are the key highlights of the various threading models:
Single Model No real support for threads. The requests reaching the COM server are serialized so the client can perform one operation at a time.
Apartment Model, or "Single-Threaded Apartment" Only the thread that created the object can call its methods. This means the requests for each server object are serialized, but other objects of the same server can receive requests at the same time. For this reason, the server object must take extra care to access only global data of the server (using critical sections, mutexes, or some other synchronization techniques). This is the threading model generally used for ActiveX controls inside Internet Explorer.
Free Model, or "Multithreaded Apartment" The client has no restrictions, which means multiple threads can use the same object at the same time. For this reason, every method of every object must protect itself and the nonlocal data it uses against multiple simultaneous calls. This threading model is more complex for a server to support than the Single and Apartment models, because even access to the object's own instance data must be handled with thread-safe care.
Both The server object supports both the Apartment model and the Free model.
Neutral Introduced in Windows 2000 and available only under COM+. This model indicates that multiple clients can call the object on different threads at the same time, but COM guarantees you that the same method is not invoked twice at the same time. Guarding for concurrent access to the object's data is required. Under COM, it is mapped to the Apartment model.
Initializing the COM Object
If you look at the definition of the TComObject class, you will notice it has a nonvirtual constructor. Actually it has multiple constructors, each calling the virtual Initialize method. For this reason, to set up a COM object properly, you should not define a new constructor (which will never be called), but instead override its Initialize method, as I've done in the TNumber class. Here is the final version of this class:
type TNumber = class(TComObject, INumber) private fValue: Integer; public function GetValue: Integer; virtual; stdcall; procedure SetValue (New: Integer); virtual; stdcall; procedure Increase; virtual; stdcall; procedure Initialize; override; destructor Destroy; override; end;
As you can see, I've also overridden the destructor of the class, because I wanted to test the automatic destruction of the COM objects provided by Delphi.
Testing the COM Server
Now that you've finished writing the COM server object, you can register and use it. Compile its code and then use the Run ® Register ActiveX Server menu command in Delphi. You do this to register the server on your own machine, updating the local Registry.
When you distribute this server, you should install it on the client computers. To accomplish this, you could write a REG file to install the server in the Registry. However, this is not the best approach, because the server already includes a function you can activate to register the server. This function can be activated by the Delphi environment, as you've seen, or in a few other ways:
- You can pass the COM server DLL as a command-line parameter to Microsoft's RegSvr32.exe program, found in the WindowsSystem directory.
- You can use the similar TRegSvr.exe demo program that ships with Delphi. (The compiled version is in the Bin directory, and its source code is in the DemosActiveX directory.)
- You can let an installation builder program call the server's registration function.
Having registered the server, you can now turn to the client side of the example. This time, the example is called TestCom, and is stored in a separate directory. The program loads the server DLL through the COM mechanism, thanks to the server information present in the Registry, so it's not necessary for the client to know which directory the server resides in.
The form displayed by this program is similar to the one you used to test some of the DLLs in Chapter 10, "Libraries and Packages." In the client program, you must include the source code file with the interface and redeclare the COM server GUID. The program starts with all the buttons disabled (at design time), and it enables them only after an object has been created. This way, if an exception is raised while creating one of the objects, the buttons related to the object won't be enabled:
procedure TForm1.FormCreate(Sender: TObject); begin // create first object Num1 := CreateComObject (Class_Number) as INumber; Num1.SetValue (SpinEdit1.Value); Label1.Caption := 'Num1: ' + IntToStr (Num1.GetValue); Button1.Enabled := True; Button2.Enabled := True; // create second object Num2 := CreateComObject (Class_Number) as INumber; Label2.Caption := 'Num2: ' + IntToStr (Num2.GetValue); Button3.Enabled := True; Button4.Enabled := True; end;
Notice in particular the call to CreateComObject and the following as cast. The API call starts the COM object-construction mechanism I've already described in detail. This call also dynamically loads the server DLL. The return value is an IUnknown object. This object must be converted to the proper interface type before it is assigned to the Num1 and Num2 fields, which now have the interface type INumber as their data type.
Warning |
To downcast an interface to the type, always use the as cast, which for interfaces performs a Query-Interface call behind the scenes. Alternatively, you can do a direct QueryInterface or Supports call. In the case of interfaces, the as cast (or a specific function call) is the only way to extract an interface from another interface. Casting an interface pointer to another interface pointer directly is an error—never do it. |
The program also has a button (toward the bottom of the form) with an event handler that creates a new COM object used to get the value of the number following 100. To see why I added this method to the example, click the button in the message showing the result. You'll see a second message indicating that the object has been destroyed. This demonstrates that letting an interface variable go out of scope invokes the object's _Release method, decreases the object's reference count, and destroys the object if its reference count reaches zero.
The same thing happens to the other two objects as soon as the program terminates. Even if the program doesn't explicitly do so, the two objects are indeed destroyed, as the message shown by their Destroy destructor clearly demonstrates. This happens because they were declared to be of an interface type, and Delphi will use reference counting for them. By the way, in case you want to destroy a COM object reference with an interface, you cannot call a Free method (interfaces don't have Free) but can assign nil to the interface variable; this causes the removal of the reference and possibly the destruction of the object.
Using Interface Properties
As a further small step, you can extend the example by adding a property to the INumber interface. When you add a property to an interface, you indicate the data type and then the read and write directives. You can have read-only or write-only properties, but the read and write clauses must always refer to a method because interfaces don't hold anything but methods.
Here is the updated interface, which is part of the PropCom example:
type INumberProp = interface ['{B36C5800-8E59-11D0-98D0-444553540000}'] function GetValue: Integer; stdcall; procedure SetValue (New: Integer); stdcall; property Value: Integer read GetValue write SetValue; procedure Increase; stdcall; end;
I've given this interface a new name and, even more important, a new interface ID. I could have inherited the new interface type from the previous one, but doing so would have provided no real advantage. COM by itself doesn't support inheritance, and from the perspective of COM, all interfaces are different because they have different interface IDs. Needless to say, in Delphi you can use inheritance to improve the structure of the code of the interfaces and of the server objects implementing them.
In the PropCom example, I've updated the server class declaration by referring to the new interface and providing a new server object ID. The client program (called TestProp) can now use the Value property instead of the SetValue and GetValue methods. Here is a small excerpt from the FormCreate method:
Num1 := CreateComObject (Class_NumPropServer) as INumberProp; Num1.Value := SpinEdit1.Value; Label1.Caption := 'Num1: ' + IntToStr (Num1.Value);
The difference between using methods and properties for an interface is only syntactical, because interface properties cannot access private data as Delphi class properties can. By using properties, you can make the code a little more readable.
Calling Virtual Methods
You've built a couple of examples based on COM, but you might still feel uncomfortable with the idea of a program calling methods of objects that are created within a DLL. How is this possible if those methods are not exported by the DLL? The COM server (the DLL) creates an object and returns it to the calling application. By doing this, the DLL creates an object with a virtual method table (VMT). To be more precise, the object has a VMT for its class plus virtual method tables for each of the interfaces it implements.
The main program receives back an interface variable with the virtual method table of the requested interface. This VMT can be used to invoke methods, but also can be used to query for other interfaces supported by the COM object (since the QueryInterface method is available as part of the IUnknown interface VMT).
The main program doesn't need to know the memory address of those methods, because the objects know it, exactly as they do with a polymorphic call. But COM is even more powerful than this: You don't have to know which programming language was used to create the object, provided its VMT follows the standard dictated by COM.
Tip |
The COM-compatible VMT implies a strange effect. The method names are not important, provided their address is in the proper position in the VMT. This is why you can map a method of an interface to a function implementing it. |
To sum things up, COM provides a language-independent binary standard for objects. The objects you share among modules are compiled, and their VMT has a particular structure determined by COM and not by the development environment you've used.
Automation
Up to now, you have seen that you can use COM to let an executable file and a library share objects. Most of the time, however, users want applications that can talk to each other. One of the approaches you can use for this goal is Automation (previously called OLE Automation). After presenting a couple of examples that use custom interfaces based on type libraries, I'll cover the development of Word and Excel Automation controllers, showing how to transfer database information to those applications.
Note |
The current Microsoft documentation uses the term Automation instead of OLE Automation, and uses the terms active document and compound document instead of OLE document. This book tends to use the new terminology, although the older "OLE" terminology is still indicated as it was probably more clear. |
In Windows, applications don't exist in separate worlds; users often want them to interact. The Clipboard and DDE offer a simple way for applications to interact, as users can copy and paste data between applications. However, more and more programs offer an Automation interface to let other programs drive them. Beyond the obvious advantage of programmed automation compared to manual user operations, these interfaces are completely language-neutral, so you can use Delphi, C++, Visual Basic, or a macro language to drive an Automation server regardless of the programming language used to write it. Automation is straightforward to implement in Delphi, thanks to the extensive work by the compiler and VCL to shield developers from its intricacies. To support Automation, Delphi provides a wizard and a powerful type-library editor, and it supports dual interfaces. When you use an in-process DLL, the client application can use the server and call its methods directly, because they are in the same address space. When you use Automation, the situation is more complex. The client (called the controller) and the server are generally two separate applications running in different address spaces. For this reason, the system must dispatch the method calls using a complex parameter passing mechanism called marshaling (something I won't cover in detail).
Technically, supporting Automation in COM implies implementing the IDispatch interface, declared in Delphi in the System unit as:
type IDispatch = interface(IUnknown) ['{00020400-0000-0000-C000-000000000046}'] 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; end;
The first two methods return type information; the last two are used to invoke an actual method of the Automation server. Actually the invocation is performed only by the last method, Invoke, while GetIDsOfNames is used to determine the dispatch id (required by Invoke) from the method name. When you create an Automation server in Delphi all you have to do is define a type library and implement its interface. Delphi provides everything else through compiler magic and VCL code (actually a portion of the VCL originally called DAX framework).
The role of IDispatch becomes more obvious when you consider that there are three ways a controller can call the methods exposed by an Automation server:
- It can ask for the execution of a method, passing its name in a string, in a way similar to the dynamic call to a DLL. This is what Delphi does when you use a variant (see the following note) to call the Automation server. This technique is easy to use, but it is rather slow and provides little compiler type-checking. It implies a call to GetIDsOfNames followed by one to Invoke.
- It can import the definition of a Delphi dispatch interface (dispinterface) for the object on the server and call its methods in a more direct way (dispatching a number, that is calling Invoke directly as the DispId of each method is known at compile time). This technique is based on interfaces and allows the compiler to check the types of the parameters and produces faster code, but it requires a little more effort from the programmer (namely the use of a type library). Also, you end up binding your controller application to a specific version of the server.
- It can call the interface directly, through the interface vtable, i.e. treating it as a normal COM object. This works in most cases as most Automation servers interfaces provide dual interfaces (that is support both IDispatch and a plain COM interface).
In the following examples, you'll use these techniques and compare them a little further.
Note |
You can use a variant to store a reference to an Automation object. In the Delphi language a variant is a type-variant data type, that is a variable that can assume different data types as its value. Variant data types include the basic ones (such as Integers, strings, characters, and Boolean values) but also the IDispatch interface type. Variants are type-checked at run time; this is why the compiler can compile the code even if it doesn't know about the methods of the Automation server, as we'll see later on. |
Dispatching an Automation Call
The most important difference between the two approaches is that the second generally requires a type library, one of the foundations of COM. A type library is basically a collection of type information, which is often found also in a COM object (with not dispatch support). This collection generally describes all the elements (objects, interfaces, and other type information) made available by a generic COM server oran Automation server. The key difference between a type library and other descriptions of these elements (such as C or Pascal code) is that a type library is language-independent. The type elements are defined by COM as a subset of the standard elements of programming languages, and any development tool can use them. Why do you need this information?
I've mentioned earlier that if you invoke a method of an Automation object using a variant, the Delphi compiler doesn't need to know about this method at compile time. A small code fragment using Word's old Automation interface, registered as Word.Basic, illustrates how simple it is for a programmer:
var VarW: Variant; begin VarW := CreateOleObject ('Word.Basic'); VarW.FileNew; VarW.Insert ('Mastering Delphi by Marco Cantù');
Note |
As you'll see later, recent versions of Word still register the Word.Basic interface, which corresponds to the internal WordBasic macro language, but they also register the new interface Word.Application, which corresponds to the VBA macro language. Delphi provides components that simplify the connection with Microsoft Office applications, introduced later on in this chapter. |
These three lines of code start Word (unless it was already running), create a new document, and add a few words to it. You can see the effect of this application in Figure 12.3.
Figure 12.3: The Word document is being created and composed by the WordTest Delphi application.
Unfortunately, the Delphi compiler has no way to check whether the methods exist. Doing all the type checks at run time is risky, because if you make even a minor spelling error in a function name, you get no warning about your error until you run the program and reach that line of code. For example, if you type VarW.Isnert, the compiler will not complain about the misspelling, but at run time, you'll get an error. Because it doesn't recognize the name, Word assumes the method does not exist.
Although the IDispatch interface supports the approach you've just seen, it is also possible—and safer—for a server to export the description of its interfaces and objects using a type library. This type library can then be converted by a specific tool (such as Delphi) into definitions written in the language you want to use to write your client or controller program (such as the Delphi language). This makes it possible for a compiler to check whether the code is correct and for you to use Code Completion and Code Parameters in the Delphi editor.
Once the compiler has done its checks, it can use either of two different techniques to send the request to the server. It can use a plain VTable (that is, an entry in an interface type declaration), or it can use a dispinterface (dispatch interface). You used an interface type declaration earlier in this chapter, so it should be familiar. A dispinterface is basically a way to map each entry in an interface to a number. Calls to the server can then be dispatched by number calling IDipatch.Invoke only, without the extra step of calling IDispatch.GetIDsOfNames. You can consider this an intermediate technique, in between dispatching by function name and using a direct call in the VTable.
Note |
The term dispinterface is a keyword. A dispinterface is automatically generated by the type-library editor for every interface. Along with dispinterface, Delphi uses other related keywords: dispid indicates the number to associate with each element; readonly and writeonly are optional specifiers for properties. |
The term used to describe this ability to connect to a server in two different ways, using a more dynamic or a more static approach, is dual interfaces. When writing a COM controller, you can choose to access the methods of a server two ways: you can use late binding and the mechanism provided by the dispinterface, or you can use early binding and the mechanism based on the VTables, the interface types.
It is important to keep in mind that (along with other considerations) different techniques result in faster or slower execution. Looking up a function by name (and doing the type checking at run time) is the slowest approach, using a dispinterface is much faster, and using the direct VTable call is the fastest approach. You'll do this kind of test in the TLibCli example, later in this chapter.
Writing an Automation Server
Let's begin by writing an Automation server. To create an Automation object, you can use Delphi's Automation Object Wizard. Begin with a new application, open the Object Repository by selecting File ® New ® Other, move to the ActiveX page, and choose Automation Object. You'll see the Automation Object Wizard:
In this wizard, enter the name of the class (without the initial T, because it will be added automatically for you to the Delphi implementing class) and click OK. Delphi will now open the type-library editor.
Tip |
Delphi can generate Automation servers that also export events. Select the corresponding check box in the Wizard, and Delphi will add the proper entries in the type library and in the source code it generates. |
The Type Library Editor
You can use the type-library editor to define a type library in Delphi. Figure 12.4 shows its window after I've added some elements to it. The type-library editor allows you to add methods and properties to the Automation server object you've just created or to a COM object that was created using the COM Object wizard. Once you do, it can generate both the type library (TLB) file and the corresponding Delphi language source code stored in a unit called a type library import unit.
Figure 12.4: The type-library editor, showing the details of an interface
I have two relevant suggestions to let you work better with Delphi's type-library editor. The first and most simple is that if you right-click on the toolbar and turn on the Text Labels option you'll see in each toolbar button a caption with its effect, making the editor easier to use. The second more relevant suggestion is to go to the Type Library page of Delphi's Environment Options dialog box and choose the Pascal language radio button over the IDL language radio button. This setting determines the notation used by the type-library editor to display methods and parameters, and even to edit the types of the parameters of a method or the type of a property. Unless you are used to writing COM code in C or C++, you'd probably prefer thinking in terms of Delphi rather than in terms of IDL.
Warning |
In this portion of the book I'll describe how to interact with the type-library editor when you have this setting, since also providing a description in terms of IDL would be both needlessly confusing and rather complex. |
To build a first example, you can add a property and a method to the server by using the editor's corresponding toolbar buttons and typing their names either in the Tree View control on the left side of the window or in the Name edit box on the right side. You add these two elements to an interface, which I've called IFirstServer.
For the procedure you'll be able to define the parameters in the Parameters page, for a function you'd also be able to set a return type in the same page. In this specific case the ChangeColor method has no parameters and its Delphi definition would be:
procedure ChangeColor; safecall;
Note |
The methods contained in Automation interfaces in Delphi generally use the safecall calling convention. It wraps a try/except block around each method and provides a default return value indicating error or success. It also sets up a COM rich error object containing the exception message, so interested clients (such as Delphi clients) can re-create the server exception on the client side. |
Now you can add a property to the interface by clicking the Property button on the type-library editor's toolbar. Again, you can type a name for it, such as Value, and select a data type in the Type combo box. Besides selecting one of the many types already listed, you can also enter other types directly, particularly interfaces of other objects.
The definition of the Value property of the example corresponds to the following elements of the Delphi interface:
function Get_Value: Integer; safecall; procedure Set_Value(Value: Integer); safecall; property Value: Integer read Get_Value write Set_Value;
Clicking the Refresh button on the type-library editor toolbar generates (or updates) the Delphi unit with the interface.
The Server Code
Now you can close the type-library editor and save the changes. This operation adds three items to the project: the type library file, a corresponding Delphi definition, and the declaration of the server object. The type library is connected to the project using a resource-inclusion statement, added to the source code of the project file:
{$R *.TLB}
You can always reopen the type-library editor by using the View ® Type Library command or by selecting the proper TLB file in Delphi's normal File Open dialog box.
As mentioned earlier, the type library is also converted into an interface definition and added to a new Delphi unit. This unit is quite long, so I've listed in the book only its key elements. The most important part is the new interface declaration:
type IFirstServer = interface(IDispatch) ['{89855B42-8EFE-11D0-98D0-444553540000}'] procedure ChangeColor; safecall; function Get_Value: Integer; safecall; procedure Set_Value(Value: Integer); safecall; property Value: Integer read Get_Value write Set_Value; end;
Then comes the dispinterface, which associates a number with each element of the IFirstServer interface:
type IFirstServerDisp = dispinterface ['{89855B42-8EFE-11D0-98D0-444553540000}'] procedure ChangeColor; dispid 1; property Value: Integer dispid 2; end;
The last portion of the file includes a creator class, which is used to create an object on the server (and for this reason used on the client side of the application, not on the server side):
type CoFirstServer = class class function Create: IFirstServer; class function CreateRemote(const MachineName: string): IFirstServer; end;
All the declarations in this file (I've skipped some others) can be considered internal, hidden implementation support. You don't need to understand them fully in order to write most Automation applications.
Finally, Delphi generates a file containing the implementation of your Automation object. This unit is added to the application and is the one you'll work on to finish the program. This unit declares the class of the server object, which must implement the interface you've just defined:
type TFirstServer = class(TAutoObject, IFirstServer) protected function Get_Value: Integer; safecall; procedure ChangeColor; safecall; procedure Set_Value(Value: Integer); safecall; end;
Delphi already provides the skeleton code for the methods, so you only need to complete the lines in between. In this case, the three methods refer to a property and two methods I've added to the form. In general, you should not add code related to the user interface inside the class of the server object. I've done it because I wanted to be able to change the Value property and have a visible side effect (displaying the value in an edit box). Here you can see this form at design time:
Registering the Automation Server
The unit containing the server object has one more statement, added by Delphi to the initialization section:
initialization TAutoObjectFactory.Create(ComServer, TFirstServer, Class_FirstServer, ciMultiInstance); end.
Note |
In this case, I've selected multiple instancing. For the various instancing styles possible in COM, see the sidebar "COM Instancing and Threading Models" earlier in this chapter. |
This is not very different from the creation of class factories you saw at the beginning of this chapter. The ComServer unit hooks the InitProc system function to register all COM objects as part of the COM server application startup. The execution of this code is triggered by the Application.Initialize call, which Delphi adds by default to the project source code of any program.
You can add the server information to the Windows Registry by running this application on the target machine (the computer where you want to install the Automation server), or by running it and passing to it the /regserver parameter on the command line. You can do this by selecting Start ® Run, by creating a shortcut in Explorer, or by running the program within Delphi after you've entered a command-line parameter (using the Run ® Parameters command). Another command-line parameter, /unregserver, is used to remove this server from the Registry.
Writing a Client for the Server
Now that you have built a server, you can prepare a client program to test it. This client can connect to the server either by using variants or by using the new type library. This second approach can be implemented manually or by using Delphi techniques for wrapping components around Automation servers. You'll try all these approaches.
Create a new application—I've called it TLibCli—and import the server's type library, using the Project ® Import type library menu command of the Delphi IDE. This command shows the Import Type Library dialog box, visible in Figure 12.5. This dialog lists registered COM servers having a type library in the upper portion. You can add other projects to this list pressing the Add button and browsing for the proper file module. The lower portion of the Import Type Library dialog box shows some details of the selected library (such as the list of server objects) and about the type library import unit this dialog box is going to produce as you press the Create Unit button (or the Install button).
Figure 12.5: Delphi's Type Library Import dialog box.
Warning |
Do not add the type library to the client application, because you are writing the Automation controller, not a server. A controller's Delphi project should not include the type library of the server it connects to. |
The type library import unit is named by Delphi after the type library, with an _TLB at the end. In this case the unit name is TlibdemoLib_TLB. I've already mentioned that one of the elements of this unit, generated also by the type library editor, is the creation class. I've shown you the interface of this class, but here is the implementation of the first of the two functions:
class function CoFirstServer.Create: IFirstServer; begin Result := CreateComObject(Class_FirstServer) as IFirstServer; end;
You can use it to create a server object (and possibly start the server application) on the same computer. As you can see in the code, the function is a shortcut for the CreateComObject call, which allows you to create an instance of a COM object if you know its GUID. As an alternative, you can use the CreateOleObject function, which requires as a parameter a ProgID, which is the registered name of the server. There is another difference between these two creation functions: CreateComObject returns an object of the IUnknown type, whereas CreateOleObject returns an object of the IDispatch type.
In this example, let's use the CoFirstServer.Create shorthand. When you create the server object, you get as a return value an IFirstServer interface. You can use it directly or store it in a variant variable. Here is an example of the first approach:
var MyServer: Variant; begin MyServer := CoFirstServer.Create; MyServer.ChangeColor;
This code, based on variants, is not very different from that of the first controller you built in this chapter (the one that used Microsoft Word). Here is the alternate code, which has the same effect:
var IMyServer: IFirstServer; begin IMyServer := CoFirstServer.Create; IMyServer.ChangeColor;
You've already seen how you can use the interface and the variant. What about the dispatch interface? You can declare a variable of the dispatch interface type, in this case:
var DMyServer: IFirstServerDisp;
Then you can use it to call the methods as usual, after you've assigned an object to it by casting the object returned by the creator class:
DMyServer := CoFirstServer.Create as IFirstServerDisp;
Interfaces, Variants, and Dispatch Interfaces: Testing the Speed Difference
As I mentioned in the section introducing type libraries, one of the differences between these approaches is speed. It is complicated to assess the exact performance of each technique because many factors are involved. I've added a simple test to the TLibCli example among the demos for this chapter, to give you an idea. The code for the test is a loop that accesses the Value of the server 100 times. The output of the program shows the timing, which is determined by calling the GetTickCount API function before and after executing the loop. (Two alternatives are to use Delphi's own time functions, which are slightly less precise, or to use the very precise timing functions of the multimedia support unit, MMSystem.)
With this program, you can roughly compare the output obtained by calling this method based on an interface, the corresponding version based on a variant, and a third version based on a dispatch interface. Looking at the timing of the example, you should see that interfaces are quicker and variants are slower, with dispatch interfaces falling in between but closer to interfaces.
The Scope of Automation Objects
Another important element to keep in mind is the scope of the Automation objects. Variants and interface objects use reference-counting techniques, so if a variable that is related to an interface object is declared locally in a method, then at the end of the method the object will be destroyed and the server may terminate (if all the objects created by the server have been destroyed). For example, writing a method with this code produces minimal effect:
procedure TClientForm.ChangeColor; var IMyServer: IFirstServer; begin IMyServer := CoFirstServer.Create; IMyServer.ChangeColor; end;
Unless the server is already active, a copy of the program is created and the color is changed, but then the server is immediately closed as the interface-typed object goes out of scope. The alternative approach I've used in the TLibCli example declares the object as a field of the form and creates the COM objects at startup, as in this procedure:
procedure TClientForm.FormCreate(Sender: TObject); begin IMyServer := CoFirstServer.Create; end;
With this code, as the client program starts, the server program is immediately activated. At program termination, the form field is destroyed and the server closes. A further alternative is to declare the object in the form, but then create it only when it is used, as in these two code fragments:
// MyServerBis: Variant; if varType (MyServerBis) = varEmpty then MyServerBis := CoFirstServer.Create; MyServerBis.ChangeColor; // IMyServerBis: IFirstServer; if not Assigned (IMyServerBis) then IMyServerBis := CoFirstServer.Create; IMyServerBis.ChangeColor;
Note |
A variant is initialized to the varEmpty type when it is created. If you instead assign the value null to the variant, its type becomes varNull. Both varEmpty and varNull represent variants with no value assigned, but they behave differently in expression evaluation. The varNull value always propagates through an expression (making it a null expression), whereas the varEmpty value quietly disappears. |
The Server in a Component
When creating a client program for your server or any other Automation server, you can use a better approach: wrapping a Delphi component around the COM server. If you look at the final portion of the TlibdemoLib_TLB file, you can find the declaration of a TFirstServer class inheriting from TOleServer. This is a component generated when importing the library, which the system registers in the unit's Register procedure.
If you add this unit to a package, the new server component will become available on the Delphi Component Palette (in the ActiveX page, by default). The generation of the code of this component is controlled by a check box at the bottom of the Import Type Library dialog box, already shown in Figure 12.5.
I've created a new package, PackAuto, available in a directory having the same name. In this package, I added the directive LIVE_SERVER_AT_DESIGN_TIME in the Directories/Conditionals page of the package's Project Options dialog box. This directive enables an extra feature that you don't get by default: At design time, the server component will have an extra property that lists as subitems all the properties of the Automation server:
Warning |
The LIVE_SERVER_AT_DESIGN_TIME directive should be used with care with the most complex Automation servers (including programs such as Word, Excel, PowerPoint, and Visio). Certain servers must be in a particular mode before you can use some properties of their automation interfaces. Because this feature is problematic at design time for many servers, it is not active by default. |
As you can see in the Object Inspector, the component has few properties. AutoConnect indicates when to activate the COM server. When the value is True the server object is loaded as soon as the wrapper component is created (both at runtime and design time). When the AutoConnect property is set to False, the Automation server is loaded only the first time one of its methods is called. Another property, ConnectKind, indicates how to establish the connection with the server. It can always start a new instance (ckNewInstance), use the running instance (ckRunningInstance, which shows an error message if the server is not already running), or select the current instance or start a new one if none is available (ckRunningOrNew). Finally, you can ask for a remote server with ckRemote and directly attach a server in the code after a manual connection with ckAttachToInterface.
Note |
To connect to an existing object, this needs to be registered in the Running Object Table (ROT). The registration must be performed by the server calling the RegisterActiveObject API function. Of course, only one instance for each COM server can be registered at a given time. |
COM Data Types
COM dispatching doesn't support all the data types available in Delphi. This is particularly important for Automation, because the client and the server are often executed in different address spaces, and the system must move (or marshal) the data from one side to the other. Also keep in mind that COM interfaces should be accessible by programs written in any language.
COM data types include basic data types such as Integer, SmallInt, Byte, Single, Double, WideString, Variant, and WordBool (but not Boolean).
In addition to the basic data types, you can use COM types for complex elements such as fonts, string lists, and bitmaps, using the IFontDisp, IStrings, and IPictureDisp interfaces. The following sections describe the details of a server that provides a list of strings and a font to a client.
Exposing Strings Lists and Fonts
The ListServ example is a practical demonstration of how you can expose two complex types, such as a list of strings and a font, from an Automation server written in Delphi. I've chosen these two specific types because they are both supported by Delphi.
The IFontDisp interface is provided by Windows and is available in the ActiveX unit. The AxCtrls Delphi unit extends this support by providing conversion methods like GetOleFont and SetOleFont. Delphi supports the IStrings interface in the StdVCL unit, and the AxCtrls unit provides conversion functions for this type (along with a third type I won't use here, TPicture).
Warning |
To run this and similar applications, you must install and register the StdVCL library on the client computer. On your computer, it is registered during Delphi's installation. |
The Set and Get methods of the complex types' properties copy information from the COM interfaces to the local data and from there to the form, and vice versa. The strings' two methods, for example, do this by calling the GetOleStrings and SetOleStrings Delphi functions. The client application used to demonstrate this feature is called ListCli. The two programs are complex; but rather than list their details here I've decided to leave the source code for your study, because Delphi programmers seldom use this advanced technique.
Using Office Programs
So far, you've built both the client and the server side of the Automation connection. If your aim is just to let two applications you've built cooperate, this is a useful technique, although it is not the only one. You've seen the use of memory-mapped files in Chapter 10. (Another technique not covered in this edition of the book is the use of the wm_CopyData message.) The real value of Automation is that it is a standard, so you can use it to integrate your Delphi programs with other applications your users own. A typical example is the integration of a program with Office applications, such as Microsoft Word and Microsoft Excel, or even with stand-alone applications, such as AutoCAD.
Integration with these applications provides a two-fold advantage:
- You can let your users work in an environment they know—for example, generating reports and memos from database data in a format they can easily manipulate.
- You can avoid implementing complex functionality from scratch, such as writing your own word-processing code inside a program. Instead of just reusing components, you can reuse complex applications.
This approach also has some drawbacks, which are worth mentioning:
- The user must own the application you plan to integrate with, and they may also need a recent version of it to support all the features you are using in your program.
- You have to learn a new programming architecture, often with limited documentation at hand. It is true that you are still using the Delphi language, but the code you write depends on the data types, the types introduced by the server, and in particular a collection of interrelated classes that are often difficult to understand.
- You might end up with a program that works only with a specific version of the server application, especially if you try to optimize the calls by using interfaces instead of variants. In particular, Microsoft does not attempt to maintain script compatibility between major releases of Word or other Office applications.
Delphi simplifies the use of Microsoft Office applications by preinstalling some ready-to-use components that wrap the Automation interface of these servers. These components, available in the Servers page of the Palette, are installed using the same technique I demonstrated in the last section. The real plus lies in the technique of creating components to wrap existing Automation servers, rather than in the availability of predefined server components. Notice also that these Office components exist in different versions depending on your version of the Microsoft suite: All components are installed, but only one set is registered at design time, according to your choice in the Delphi install program. You can change this setting later by removing the related component package and adding a new one.
You won't see an actual example in this section because it is very difficult to write a program that works with all of the different versions of Microsoft Office. You'll find same sample code and tips in Essential Delphi (see Appendix C for instructions on how to download this free ebook).
Using Compound Documents
Compound documents is Microsoft's name for the technology that allows in-place editing of a document within another document (for example, a picture in a Word document). This technology originated the term OLE, but its role is now definitely more limited than Microsoft envisioned when it was introduced in the early 1990s. Compound documents have two capabilities: object linking and embedding (hence the term OLE):
- Embedding an object in a compound document corresponds to a smart version of the copy and paste operations you perform with the Clipboard. The key difference is that when you copy an OLE object from a server application and paste it into a container application, you copy both the data and some information about the server (its GUID). This allows you to activate the server application from within the container to edit the data.
- Linking an object to a compound document copies only a reference to the data and the information about the server. You generally activate object linking by using the Clipboard and performing a Paste Link operation. When editing the data in the container application, you modify the original data, which is stored in a separate file.
Because the server program refers to an entire file (only part of which may be linked in the client document), the server will be activated in a stand-alone window, and it will act upon the entire original file, not just the data you've copied. When you have an embedded object, however, the container may support visual (or in-place) editing, which means you can modify the object in context inside the container's main window. The server and container application windows, their menus, and their toolbars are merged automatically, allowing the user to work in a single window on several different object types—and therefore with several different OLE servers—without leaving the window of the container application.
Another key difference between embedding and linking is that an embedded object's data is stored and managed by the container application. The container saves the embedded object in its own files. By contrast, a linked object physically resides in a separate file, which is handled by the server exclusively, even if the link refers only to a small portion of the file. In both cases, the container application doesn't have to know how to handle the object and its data—not even how to display it—without the help of the server. Considering the relative slowness of OLE and the amount of work necessary to develop COM servers, you can understand why this approach never took off.
Compound document containers can support COM in varying degrees. You can place an object in a container by inserting a new object, by pasting or paste-linking one from the Clipboard, by dragging one from another application, and so on. Once the object is placed in the container, you can then perform operations on it, using the server's available verbs, or actions. Usually the Edit verb is the default action—the action performed when you double-click on the object. For other objects, such as video or sound clips, Play is defined as the default action. You can typically see the list of actions supported by the current contained object by right-clicking it. The same information is available in many programs via the Edit ® Object menu item, which displays a submenu that lists the available verbs for the current object.
The Container Component
To create a COM container application in Delphi, place an OleContainer component in a form. Then select the component and right-click to activate its shortcut menu, which will include an Insert Object command. When you select this command, Delphi displays the standard OLE Insert Object dialog box. This dialog box allows you to choose from one of the server applications registered on the computer.
Once the COM object is inserted in the container, the control container component's shortcut menu will include several more custom menu items. The new menu items include commands to change the properties of the COM object, insert another object, copy the existing object, or remove the existing object. The list also includes the verbs (actions) of the object (such as Edit, Open, or Play). Once you have inserted a COM object in the container, the corresponding server will launch to let you edit the new object. As soon as you close the server application, Delphi updates the object in the container and displays it at design time in the form of the Delphi application you are developing.
If you look at the textual description of a form containing a component with an object inside, you'll notice a Data property, which contains the COM object's data. Although the client program stores the object's data, it doesn't know how to handle and show that data without the help of the proper server (which must be available on the computer where you run the program). This means the COM object is embedded.
To fully support compound documents, a program should provide a menu and a toolbar or panel. These extra components are important because in-place editing implies a merging of the client's user interface and the server program's user interface. When the COM object is activated in place, some of the
pull-down menus on the server application's menu bar are added to the container application's menu bar.
Menu merging is handled almost automatically by Delphi. You only need to set the proper indexes for the menu items of the container, using the GroupIndex property. Any menu item with an odd index number is replaced by the corresponding element of the active OLE object. Specifically, the File (0) and Window (4) pull-down menus belong to the container application. The Edit (1), View (3), and Help (5) pull-down menus (or the groups of pull-down menus with those indexes) are taken by the COM server. A sixth group, Object (2), can be used by the container to display another pull-down menu between the Edit and View groups, when the COM object is active. The OleCont demo program I've written to demonstrate these features allows a user to create a new object by calling the InsertObjectDialog method of the TOleContainer class.
Once a new object has been created, you can execute its primary verb using the DoVerb method. The program also displays a small toolbar with some bitmap buttons. I placed some TWinControl components in the form to let the user select them and thus disable the OleContainer. To keep this toolbar/panel visible while in-place editing is occurring, you should set its Locked property to True. This setting forces the panel to remain present in the application and not be replaced by a toolbar of the server.
To show what happens when you don't use this approach, I've added to the program a second panel with more buttons. Because I haven't set its Locked property, this new toolbar will be replaced with that of the active server. When in-place editing launches a server application that displays a toolbar, that server's toolbar replaces the container's toolbar, as you can see in the lower part of Figure 12.6.
Figure 12.6: The second toolbar of the OleCont example (above) is replaced by the server's toolbar (below).
Tip |
To make all the automatic resizing operations work smoothly, you should place the OLE container component in a panel component and align both of them to the client area of the form. |
Alternatively, you can create a COM object using the PasteSpecialDialog method, called in the example's PasteSpecial1Click event handler. Another standard COM dialog box, wrapped in a Delphi function, shows the properties of the object; this dialog box is activated with the Object Properties item in the Edit pull-down menu by calling the ObjectPropertiesDialog method of the OleContainer component.
The last feature of the OleCont program is support for files. This is one of the simplest additions you can make, because the OLE container component already provides file support.
Using the Internal Object
In the preceding program, the user determined the type of the internal object created by the program. In this case, there is little you can do to interact with the internal objects. Suppose, instead, that you want to embed a Word document in a Delphi application and then modify it by code. You can do this by using Automation with the embedded object, as demonstrated by the WordCont example (the name stands for Word container).
Warning |
Because the WordCont example includes an object of a specific type (a Microsoft Word document), it won't run if you don't have that application installed. Having a different version of the server might also create problems (I've tested the examples in this chapter only with Office 97). For other versions, you might have to rebuild the program following the same steps I used. |
In the example's form, I added an OleContainer component, set its AutoActivate property to aaManual (so that the only possible interaction is with my code), and added a toolbar with a couple of buttons. The code is straightforward, once you know that the embedded object corresponds to a Word document. Here is an example (you can see the effect of this code in Figure 12.7):
procedure TForm1.Button3Click(Sender: TObject); var Document, Paragraph: Variant; begin // activate if not running if not (OleContainer1.State = osRunning) then OleContainer1.Run; // get the document Document := OleContainer1.OleObject; // add paragraphs, getting the last one Document.Paragraphs.Add; Paragraph := Document.Paragraphs.Add; // add text to the paragraph, using random font size Paragraph.Range.Font.Size := 10 + Random (20); Paragraph.Range.Text := 'New text (' + IntToStr (Paragraph.Range.Font.Size) + ')'#13; end;
Figure 12.7: The WordCont example shows how to use Automation with an embedded object.
Introducing ActiveX Controls
Microsoft's Visual Basic was the first program development environment to introduce the idea of supplying software components to the mass market, even if the concept of reusable software components is older than Visual Basic—it's well rooted in the theories of object-oriented programming (OOP). The first technical standard promoted by Visual Basic was VBX, a 16-bit specification that was fully available in Delphi 1. In moving to the 32-bit platforms, Microsoft replaced the VBX standard with the more powerful and more open ActiveX controls.
Note |
ActiveX controls used to be called OLE controls (or OCX). The name change reflects a new marketing strategy from Microsoft rather than a technical innovation. Not surprisingly, then, ActiveX controls are usually saved in files with the .ocx extension. |
From a general perspective, an ActiveX control is not very different from a Windows control. The key difference is in the control's interface—the interaction between the control and the rest of the application. Typical Windows controls use a message-based interface; Automation objects and ActiveX controls use properties, methods, and events (like Delphi's own components).
Using COM jargon, an ActiveX control is a "compound document object which is implemented as an in-process server DLL and supports Automation, visual editing, and inside-out activation." Perfectly clear, right? Let's see what this definition means. COM servers can be implemented three ways:
- As stand-alone applications (for example, Microsoft Excel)
- As out-of-process servers—that is, executables files that cannot be run by themselves and can only be invoked by a server (for example, Microsoft Graph and similar applications)
- As in-process servers, such as DLLs loaded into the same memory space as the program using them
ActiveX controls can only be implemented using the last technique, which is also the fastest: as in-process servers. Furthermore, ActiveX controls are Automation servers. This means you can access properties of these objects and call their methods. You can see an ActiveX control in the application that is using it and interact with it directly in the container application window. This is the meaning of the term visual editing, or in-place activation. A single click activates the control rather than the double-click used by OLE documents, and the control is active whenever it is visible (which is what the term inside-out activation means) without your having to double-click it.
In an ActiveX control, properties can identify states, but they can also activate methods. Properties can refer to aggregate values, arrays, subobjects, and so on. Properties can also be dynamic (or read-only, to use the Delphi term). The properties of an ActiveX control are divided into groups: stock properties that most controls need to implement; ambient properties that offer information about the container (similar to the ParentColor and ParentFont properties in Delphi); extended properties managed by the container, such as the position of the object; and custom properties, which can be anything.
Events and methods are, well, events and methods. Events relate to a mouse click, a key press, the activation of a component, and other specific user actions. Methods are functions and procedures related to the control. There is no major difference between the ActiveX and Delphi concepts of events and methods.
ActiveX Controls Versus Delphi Components
Before I show you how to use and write ActiveX controls in Delphi, let's go over some of the differences between the two kinds of controls. ActiveX controls are DLL-based: When you use them, you need to distribute their code (the OCX file) along with the application using them. In Delphi, the components' code can be statically linked to the executable file or dynamically linked to it using a run-time package, so you can always choose whether to deploy a single large file or many smaller modules.
Having a separate file allows you to share code among different applications, as DLLs usually do. If two applications use the same control (or run-time package), you need only one copy of it on the hard disk and a single copy in memory. The drawback, however, is that if the two programs have to use two different versions (or builds) of the ActiveX control, some compatibility problems may arise. An advantage of having a self-contained executable file is that you will also have fewer installation problems.
The drawback of using Delphi components is not that there are fewer Delphi components than ActiveX controls, but that if you buy a Delphi component, you'll only be able to use it in Delphi and Borland C++Builder. If you buy an ActiveX control, on the other hand, you'll be able to use it in multiple development environments from multiple vendors. Even so, if you develop mainly in Delphi and find two similar components based on the two technologies, I suggest you buy the Delphi component—it will be more integrated with your environment, and therefore easier for you to use. Also, the native Delphi component will probably be better documented (from the Delphi perspective), and it will take advantage of Delphi and its language features not available in the general ActiveX interface, which is traditionally based on C and C++.
Note |
In the .NET world, this situation will change completely. Not only will you be able to use any system component in a more seamless way, but you'll also be able to make Delphi components available to other .NET programming languages and tools. |
Using ActiveX Controls in Delphi
Delphi comes with some preinstalled ActiveX controls, and you can buy and install more third-party ActiveX controls. After this description of how ActiveX controls work in general, I'll demonstrate one in an example.
The Delphi installation process is simple:
- Select Component ® Import ActiveX Control in the Delphi menu to open the Import ActiveX dialog box, where you can see the list of ActiveX control libraries registered in Windows.
- Choose one, and Delphi will read its type library, list its controls, and suggest a filename for its unit.
- If the information is correct, click the Create Unit button to view the Delphi language source code file created by the IDE as a wrapper for the ActiveX control.
- Click the Install button to add this new unit to a Delphi package and to the Component Palette.
Using the WebBrowser Control
To build an example, I've used a preinstalled ActiveX control available in Delphi. Unlike the third-party controls, it is not available in the ActiveX page of the palette, but in the Internet page. The control, called WebBrowser, is a wrapper around Microsoft's Internet Explorer engine. The WebDemo example is a very limited web browser; it has a TWebBrowser ActiveX control covering its client area, a control bar at the top, and a status bar at the bottom. To move to a given web page, a user can type a URL in the toolbar's combo box, select one of the visited URLs (saved in the combo box), or click the Open File button to select a local file. Figure 12.8 shows an example of this program.
Figure 12.8: The WebDemo program after choosing a page that's well known by Delphi developers
The implementation of the code used to select a web or local HTML file is in the GotoPage method:
procedure TForm1.GotoPage(ReqUrl: string); begin WebBrowser1.Navigate (ReqUrl, EmptyParam, EmptyParam, EmptyParam, EmptyParam); end;
EmptyParam is a predefined OleVariant you can use to pass a default value as a reference parameter. This is a handy shortcut you can use to avoid creating an empty OleVariant variable each time you need a similar parameter. The program calls the GotoPage method when the user presses the Open File button, or when the user presses the Enter key while in the combo box or clicks the Go button, as you can see in the source code. The program also handles four events of the WebBrowser control. When the download operations ends, the program updates the text of the status bar and also the combo box's drop-down list:
procedure TForm1.WebBrowser1DownloadComplete(Sender: TObject); var NewUrl: string; begin StatusBar1.Panels[0].Text := 'Done'; // add URL to combo box NewUrl := WebBrowser1.LocationURL; if (NewUrl <> '') and (ComboURL.Items.IndexOf (NewUrl) < 0) then ComboURL.Items.Add (NewUrl); end;
Other useful events are the OnTitleChange, used to update the caption with the title of the HTML document, and the OnStatusTextChange event, used to update the second part of the status bar. This code basically duplicates the information displayed in the first part of the status bar by the previous two event handlers.
Writing ActiveX Controls
Besides using existing ActiveX controls in Delphi, you can easily develop new ones, using one of two techniques:
- You can use the ActiveX Control Wizard to turn a VCL control into an ActiveX control. You begin from an existing VCL component, which must be a TWinControl descendant (and must not have inappropriate properties, in which case it is removed form the combo box of the Wizard), and Delphi wraps an ActiveX around it. During this step, Delphi adds a type library to the control. (Wrapping an ActiveX control around a Delphi component is the opposite of what you did to use an ActiveX control in Delphi.)
- You can create an ActiveForm, place several controls in it, and use the entire form (without borders) as an ActiveX control. This second technique was introduced to build Internet applications, but it is also a very good alternative for constructing an ActiveX control based on multiple Delphi controls or on Delphi components that do not descend from TWinControl.
In either case, you can optionally prepare a property page for the control, to use as a sort of property editor for setting the initial value of the control's properties in any development environment—an alternative to the Object Inspector in Delphi. Because most development environments allow only limited editing, it is more important to write a property page than it is to write a component or property editor for a Delphi control.
Building an ActiveX Arrow
As an example of the development of an ActiveX control, I've decided to take the Arrow component from Chapter 9, "Writing Delphi Components," and turn it into an ActiveX control. You cannot use that component directly, because it is a graphical control (a subclass of TGraphicControl). However, turning a graphical control into a window-based control is usually a straightforward operation.
In this case, you change the base class name to TCustomControl (and change the name of the control's class to TMdWArrow, as well, to avoid a name clash), as you can see in the source code files in the XArrow folder. After installing this component in Delphi, you are ready to begin developing the new example. To create a new ActiveX library, select File ® New ® Other, move to the ActiveX page, and choose ActiveX library. Delphi creates the bare skeleton of a DLL, as you saw at the beginning of this chapter. I've saved this library as XArrow, in a directory with the same name, as usual.
Now it is time to use the ActiveX Control Wizard, available in the ActiveX page of the Object Repository—Delphi's New dialog box:
In this wizard, you select the VCL class you are interested in, customize the names shown in the edit boxes, and click OK; Delphi then builds the complete source code of an ActiveX control for you.
The use of the three check boxes at the bottom of the ActiveX Control Wizard window may not be obvious. If you make the control licensed, Delphi will include a license key in the code and provide this same GUID in a separate .LIC file. This license file is necessary to use the control in a design environment without the proper license key for the control or use it within a web page. The second check box allows you to include version information for the ActiveX control in the OCX file. If the third check box is selected, the ActiveX Control Wizard automatically adds an About box to the control.
Look at the code the ActiveX Control Wizard generates. This wizard's key element is the generation of a type library and, of course, a corresponding type library import unit with the definition of an interface (dispinterface) and other types and constants. In this example, the import file is named XArrow_TLB.PAS: I suggest you study it to understand how Delphi defines an ActiveX control. The unit includes a GUID for the control, constants for the definition of values corresponding to the COM enumerated types used by properties of the Delphi control (like TxMdWArrowDir), and the declaration of the IMdWArrowX interface. The final part of the import unit includes the declaration of the TMdWArrowX class. This is a TOleControl-derived class you can use to install the control in Delphi, as you saw in the first part of this chapter. You don't need this class to build the ActiveX control; you need it to install the ActiveX control in Delphi.
The rest of the code, and the code you'll customize, is in the main unit, which in the XArrow example is called MdWArrowImpl1. This unit has the declaration of the ActiveX server object, TMdWArrowX, which inherits from TActiveXControl and implements the specific IMdWArrowX interface.
Before you customize this control, let's see how it works. Compile the ActiveX library and then register it using Delphi's Run ® Register ActiveX Server menu command. Now you can install the ActiveX control as you've done in the past, except you have to specify a different name for the new class to avoid a name clash. If you use this control, it doesn't look much different from the original VCL control, but the same component can now be installed in other development environments.
Adding New Properties
Once you've created an ActiveX control, adding new properties, events, or methods to it is—surprisingly—simpler than doing the same operation for a VCL component. Delphi, provides specific visual support for adding properties, methods, or events to an ActiveX control, but not for a VCL control. You can open the Delphi unit with the implementation of the ActiveX control, and choose Edit ® Add To Interface. As an alternative, you can use the same command from the editor's shortcut menu. Delphi opens the Add To Interface dialog box:
In the combo box, you can choose between a new property, method, or event. In the edit box, you can then type the declaration of this new interface element. If the Syntax Helper check box is activated, you'll get hints describing what you should type next and highlighting any errors. When you define a new ActiveX interface element, keep in mind that you are restricted to COM data types.
In the XArrow example, I've added two properties to the ActiveX control. Because the Pen and Brush properties of the original Delphi components are not accessible, I've made their color available. These are examples of what you can write in the Add To Interface dialog box's edit box (executing it twice):
property FillColor: Integer; property PenColor: Integer;
Note |
Because a TColor is a specific Delphi definition, it is not legal to use it. TColor is an integer subrange that defaults to integer size, so I've used the standard Integer type directly. |
The declarations you enter in the Add To Interface dialog box are automatically added to the control's type library (TLB) file, to its import library unit, and to its implementation unit. All you have to do to finish the ActiveX control is fill in the Get and Set methods of the implementation. If you now install this ActiveX control in Delphi once more, the two new properties will appear. The only problem with this property is that Delphi uses a plain integer editor, making it difficult to enter the value of a new color by hand. A program, by contrast, can easily use the RGB function to create the proper color value.
Adding a Property Page
As it stands, other development environments can do very little with your component, because you've prepared no property page—no property editor. A property page is fundamental so that programmers using the control can edit its attributes. However, adding a property page is not as simple as adding a form with a few controls. The property page, will integrate with the host development environment. The property page for your control will show up in a property page dialog of the host environment, which will provide the OK, Cancel, and Apply buttons, and the tabs for showing multiple property pages (some of which may be provided by the host environment).
The nice thing is that support for property pages is built into Delphi, so adding one takes little time. You open an ActiveX project, then open the usual New Items dialog box, move to the ActiveX page, and choose Property Page. What you get is not very different from a form—the TPropertyPage1 class (created by default) inherits from the TPropertyPage class of VCL, which in turn inherits from TCustomForm.
Tip |
Delphi provides four built-in property pages for colors, fonts, pictures, and strings. The GUIDs of these classes are indicated by the constants Class_DColorPropPage, Class_DFontPropPage, Class_DPicturePropPage, and Class_DStringPropPage in the AxCtrls unit. |
In the property page, you can add controls as in a normal Delphi form, and you can write code to let the controls interact. In the XArrow example, I've added to the property page a combo box with the possible values of the Direction property, a check box for the Filled property, an edit box with an UpDown control to set the ArrowHeight property, and two shapes with corresponding buttons for the colors. You can see this form in the Delphi IDE while working on the ActiveX control in Figure 12.9.
Figure 12.9: The XArrow ActiveX control and its property page, hosted by the Delphi environment
The only code added to the form relates to the two buttons used to change the color of the two shape components, which offer a preview of the ActiveX control's color. The button's OnClick event uses a ColorDialog component, as usual:
procedure TPropertyPage1.ButtonPenClick(Sender: TObject); begin with ColorDialog1 do begin Color := ShapePen.Brush.Color; if Execute then begin ShapePen.Brush.Color := Color; Modified; // enable Apply button! end; end; end;
It's important to notice in this code the call to the Modified method of the TPropertyPage class. This call is required to let the property page dialog box know you've modified one of the values and to enable the Apply button. When a user interacts with one of the other controls on the form, this Modified call is made automatically to the TPropertyPage class method that handles the internal cm_Changed message. As a user, you don't change the buttons for these controls, however, you need to add this line yourself.
Tip |
Another tip relates to the Caption of the property page form. It will be used in the host environment's property dialog box as the caption of the tab corresponding to the property page. |
The next step is to associate the property page's controls with the ActiveX control's properties. The property page class automatically has two methods for this functionality: UpdateOleObject and UpdatePropertyPage. As their names suggest, these methods copy data from the property page to the ActiveX control and vice versa, as you can see in the example code.
The final step is to connect the property page to the ActiveX control. When the control was created, the Delphi ActiveX Control Wizard automatically added a declaration for the Define-PropertyPages method to the implementation unit. In this method, you call the DefinePropertyPage method (this time the method name is singular) for each property page you want to add to the control. The parameter of this method is the GUID of the property page, which you can find in the corresponding unit:
procedure TMdWArrowX.DefinePropertyPages( DefinePropertyPage: TDefinePropertyPage); begin DefinePropertyPage(Class_PropertyPage1); end;
You've finished developing the property page. After recompiling and reregistering the ActiveX library, you can install the ActiveX control in a host development environment (including Delphi) and see how it looks, as I already did in Figure 12.9.
ActiveForms
As I've mentioned, Delphi provides an alternative to the use of the ActiveX Control Wizard to generate an ActiveX control. You can use an ActiveForm, which is an ActiveX control that is based on a form and can host one or more Delphi components. This technique is used in Visual Basic to build new controls, and it makes sense when you want to create a compound component.
In the XClock example, I've placed on an ActiveForm a label (a graphic control that cannot be used as a starting point for an ActiveX control) and a timer, and connected the two with a little code. The form/control becomes a container of other controls, which makes it easy to build compound components (easier than for a VCL compound component).
To build such a control, select the ActiveForm icon in the ActiveX page of the File ® New dialog box. Delphi will ask you for some information in the ActiveForm Wizard dialog box, which is similar to the ActiveX Control Wizard dialog box.
ActiveForm Internals
Before you continue with the example, let's look at the code generated by the ActiveForm Wizard. The key difference from a plain Delphi form is in the declaration of the new form class, which inherits from the TActiveForm class and implements a specific ActiveForm interface. The code generated for the active form class implements quite a few Set and Get methods, which change or return the corresponding properties of the Delphi form; this code also implements the events, which again are the events of the form.
The TForm events are set to the internal methods when the form is created. For example:
procedure TAXForm1.Initialize; begin OnActivate := ActivateEvent; ... end;
Each event then maps itself to the external ActiveX event, as in the following method:
procedure TAXForm1.ActivateEvent(Sender: TObject); begin if FEvents <> nil then FEvents.OnActivate; end;
Because of this mapping, you should not handle the form's events directly. Instead, you can either add code to these default handlers or override the TForm methods that end up calling the events. This mapping problem relates only to the events of the form itself, not to the events of the form's components. You can continue to handle the components' events as usual.
Note |
These problems (and possible solutions) are demonstrated by the XForm1 example. I won't discuss it in detail, but leave it for you as a self-study example. |
The XClock ActiveX Control
Now that I've covered some foundations, let's return to the development of the XClock example:
- Place on the form a timer and a label with a large font and centered text, aligned to the client area.
- Write an event handler for the timer's OnTimer event, so that the control updates the output of the label with the current time every second:
procedure TXClock.Timer1Timer(Sender: TObject); begin Label1.Caption := TimeToStr (Time); end;
- Compile this library, register it, and install it in a package to test it in the Delphi environment.
Notice the effect of the sunken border. This is controlled by the active form's AxBorderStyle property, one of the few properties of active forms that is not available for a plain form.
ActiveX in Web Pages
In the previous example, you used Delphi's ActiveForm technology to create a new ActiveX control. An ActiveForm is an ActiveX control based on a form. Borland documentation often implies that ActiveForms should be used in HTML pages, but you can use any ActiveX control on a web page. Basically, each time you create an ActiveX library, Delphi should enable the Project ® Web Deployment Options and Project ® Web Deploy menu items.
Warning |
Due to what I consider a bug, in Delphi 7 these commands are activated only for an ActiveForm. If they are disabled, you can use a trick: Add an ActiveForm to your current ActiveX library, which will enable the menu commands; then immediately remove the ActiveForm, and the menu items will still be available. The trouble is, you'll have to repeat this operation every time you reopen the project—that is, until Borland fixes the bug. |
The first command allows you to specify how and where to deliver the proper files. In this dialog box you can set the server directory for deploying the ActiveX component, the URL of this directory, and the server directory for deploying the HTML file (which will have a reference to the ActiveX library using the URL you provide).
You can also specify the use of a compressed CAB file, which can store the OCX file and other auxiliary files, such as packages, making it easier and faster to deliver the application to the user. A compressed file, means a faster download. I've generated the HTML file and CAB file for the XClock project in the same directory. Opening this HTML file in Internet Explorer produces the output shown in Figure 12.10. If all you get is a red X marker indicating a failure to download the control, there are various possible explanations for this problem: Internet Explorer doesn't allow the download of controls, it doesn't match the security level for the unsigned control, there is a mismatch in the control version number, and so on.
Figure 12.10: The XClock control in the sample HTML page
Notice that in the portion of the HTML file referring to the control, you can use the special param tag to customize the control's properties. For example, in the XArrow control's HTML file, I've modified the automatically generated HTML file (in the XArrowCust.htm file) with these three param tags:
Although this might seem to be a useful technique, it is important to consider the (limited) role of an ActiveX form placed in a web page. It corresponds to letting a user download and execute a custom Windows application, which raises many concerns about security. An ActiveX control can access the computer's system information, such as the user name, directory structure, and so on. I could continue, but my point is clear.
Introducing COM+
In addition to plain COM servers, Delphi also allows you to create enhanced COM objects, including stateless objects and transaction support. Microsoft first introduced this type of COM object with the MTS (Microsoft Transaction Server) acronym in Windows NT and 98, and later renamed it COM+ in Windows 2000/XP (I'll call it COM+, but I'm referring to both MTS and COM+).
Delphi supports building both standard stateless objects and DataSnap remote data modules based on stateless objects. In both cases, you begin development by using one of the available Delphi wizards, using the New Items dialog box and selecting the Transactional Object icon on the ActiveX page or the Transactional Data Module icon on the Multitier page. You must add these objects to an ActiveX library project, not to a plain application. Another icon, COM+ Event Object, is used to support
COM+ events.
COM+ provides a run-time environment supporting database transaction services, security, resource pooling, and an overall improvement in robustness for DCOM applications. The run-time environment manages objects called COM+ components. These are COM objects stored in an in-process server (that is, a DLL). Whereas other COM objects run directly in the client application, COM+ objects are handled by this run-time environment, in which you install the COM+ libraries. COM+ objects must support specific COM interfaces, starting with IObjectControl, which is the base interface (like IUnknown for a COM object).
Before getting into too many technical and low-level details, let's consider COM+ from a different perspective: the benefits of this approach. COM+ provides a few interesting features, including:
Role-Based Security The role assigned to a client determines whether it has the right to access the interface of a data module.
Reduced Database Resources You can reduce the number of database connections, because the middle tier logs on to the server and uses the same connections for multiple clients (although you cannot have more clients connected at once than you have licenses for the server).
Database Transactions COM+ transaction support includes operations on multiple databases, although few SQL servers other than Microsoft's support COM+ transactions.
Creating a COM+ Component
The starting point for creating a COM+ component is the creation of an ActiveX library project. Then, follow these steps:
- Select a new Transactional Object in the ActiveX page of the New Items dialog box.
- In the resulting dialog box (see Figure 12.11), enter the name of the new component (ComPlus1Object in my ComPlus1 example).
Figure 12.11: The New Trans-actional Object dialog box, used to create a COM+ object
The New Transactional Object dialog box allows you to enter a name for the class of the COM+ object, the threading model (because COM+ serializes all the requests, Single or Apartment will generally do), and a transactional model:
Requires a Transaction Indicates that each call from the client to the server is considered a transaction (unless the caller supplies an existing transaction context).
Requires a New Transaction Indicates that each call is considered a new transaction.
Supports Transactions Indicates that the client must explicitly provide a transaction context.
Does Not Support Transaction (The default choice, and the one I've used.) Indicates that the remote data module won't be involved in any transaction. This option prevents the object from being activated if the client calling it has a transaction.
Ignores Transactions Indicates that objects do not participate in transactions, but can be used regardless of whether the client has a transaction.
- As you close this dialog, Delphi adds a type library and an implementation unit to the project and opens the type-library editor, where you can define the interface of your new COM object. For this example, add a Value integer property, an Increase method having as its parameter an amount, and an AsText method returning a WideString with the formatted value.
- As you accept the edits in the type-library editor (by clicking the Refresh button or closing the window), Delphi shows the Implementation File Update Wizard, but only if you set the Display updates before refreshing option of the Type Library page of the Environment Options dialog box. This wizard asks for your confirmation before adding four methods to the class, including the get and set methods of the property. You can now write some code for the COM object, which in my example is quite trivial.
Once you've compiled an ActiveX library, or COM library, which hosts a COM+ component, you can use the Component Services administrative tool (shown in the Microsoft Management Console, or MMC) to install and configure the COM+ component. Even better, you can use the Delphi IDE to install the COM+ component using the Run ® Install COM+ Object menu command. In the subsequent dialog box, you can select the component to install (a library can host multiple components) and choose the COM+ application where you want to install the component:
A COM+ application is nothing more than a way to group COM+ components; it is not a program or anything like one (why they call it an application is not clear to me). So, in the Install COM+ Object dialog, you can select an existing application/group, choose the Install Into New Application page, and enter a name and description.
I've called the COM+ application Mastering Delphi Com+ Test, as you can see in Figure 12.12 in Microsoft's Component Services administration console. This is the front end you can use to fine-tune the behavior of your COM+ components, setting their activation model (just-in-time activation, object pooling, and so on), their transaction support, and the security and concurrency models you want to use. You can also use this console to monitor the objects and method calls (in case they take a long time to execute). In Figure 12.12, you can see that there are currently two active objects.
Figure 12.12: The newly installed COM+ component in a custom COM+ application (as shown by Microsoft's Com-ponent Services tool)
Warning |
Because you've created one or more objects, the COM library remains loaded in the COM+ environment and some of the objects may be kept in cache, even if no clients are connected to them. For this reason, you generally cannot recompile the COM library after using it, unless you use the MMC to shut it down or set a Transaction Timeout of 0 seconds in MMC. |
I've created a client program for the COM+ object, but it is like any other Delphi COM client. After importing the type library, which is automatically registered while installing the component, I created an interface-type variable referring to it and called its methods as usual.
Transactional Data Modules
The same types of features are available when you create a transactional data module—a remote data module within a COM+ component. Once you've created a transactional data module, you can build a Delphi DataSnap application (as you'll see in Chapter 16, "Multitier DataSnap Applications"). You can add one or more dataset components, add one or more providers, and export the provider(s). You can also add custom methods to the data module type library by editing the type library or using the Add To Interface command.
Within a COM+ component or transactional data module, you can also use specific methods that support transactions. These methods are technically provided, at a lower level, in the IObjectContext interface returned by the GetObjectContext method:
- SetComplete tells the COM+ environment the object has finished working and can be deactivated, so that the transaction can be committed.
- EnableCommit indicates that the object hasn't finished but the transaction should be committed.
- DisableCommit stops the commit operation, even if the method is done, disabling the object deactivation between method calls.
- SetAbort says the object has finished and can be activated but the transaction cannot be committed.
- IsInTransaction checks whether the object is part of a transaction.
Other methods of the IContextObject interface include CreateInstance, which creates another COM+ object in the same context and within the current transaction; IsCallerInRole, which checks if the object's caller is in a particular "security" role; and IsSecurityEnabled (whose name is self-explanatory).
Once you've built a transactional data module within a server library, you can install it as I showed earlier for a plain COM+ object. After the transactional data module has been installed, it will be directly available to other applications and visible in the management console.
An important feature of COM+ is that it becomes much easier to configure DCOM support using this environment. A client computer's COM+ environment can grab information from a server computer's COM+ environment, including registration information for the COM+ object you want to be able to call over a network. The same network configuration is much more complex if done with plain DCOM, without MTS or COM+.
Tip |
Even though COM+ configuration is much better than DCOM configuration, you are limited to computers with a recent version of the Windows operating system. Considering that even Microsoft is moving away from DCOM technology, before you build a large system based on it you should evaluate the alternative provided by SOAP (discussed in Chapter 22, "Using XML Technologies"). |
COM+ Events
Client applications that use traditional COM objects and Automation servers can call methods of those servers, but this is not an efficient way to check whether the server has updated data for the client. For this reason, a client can define a COM object that implements a callback interface, pass this object to the server, and let the server call it. Traditional COM events (which use the IConnectionPoint interface) are simplified by Delphi for Automation objects, but are still complex to handle.
COM+ introduces a simplified event model, in which the events are COM+ components and the COM+ environment manages the connections. In traditional COM callbacks, the server object has to keep track of the multiple clients it has to notify, something Delphi doesn't provide us automatically (the default Delphi event code is limited to a single client). To support COM callbacks for multiple clients you need to add the code to hold references to each of the clients. In COM+, the server calls into a single event interface, and the COM+ environment forwards the event to all clients that have expressed interest in it. This way, the client and the server are less coupled, making it possible for a client to receive notification from different servers without any change in its code.
Note |
Some critics say that Microsoft introduced this model only because it was difficult for Visual Basic developers to handle COM events in the traditional way. Windows 2000 provided a few operating-system features specifically intended for VB developers. |
To create a COM+ event, you should create a COM library (or ActiveX library) and use the COM+ Event Object wizard. The resulting project will contain a type library with the definition of the interface used to fire the events, plus some fake implementation code. The server that receives the notification of the events will provide the interface implementation. The fake code is there only to support Delphi's COM registration system.
While building the MdComEvents library, I added to the type library a single method with two parameters, resulting in the following code (in the interface definition file):
type IMdInform = interface(IDispatch) ['{202D2CC8-8E6C-4E96-9C14-1FAAE3920ECC}'] procedure Informs(Code: Integer; const Message: WideString); safecall; end;
The main unit includes the fake COM object (notice that the method is abstract, so it has no implementation) and its class factory, to let the server register itself. At this point, you can compile the library and install it in the COM+ environment, following these steps:
- In Microsoft's Component Services console, select a COM+ application, move to the Components folder, and use the shortcut menu to add a new component to it.
- In the COM Component Install Wizard, click the Install New Event Class button and select the library you've just compiled. Your COM+ event definition will be automatically installed.
To test whether it works, you'll have to build an implementation of this event interface and a client invoking it. The implementation can be added to another ActiveX library, hosting a plain COM object. Within Delphi's COM Object Wizard, you can select the interface to implement from the list that appears when you click the List button.
The resulting library, which in my example is called EvtSubscriber, exposes an Automation object: a COM object implementing the IDispatch interface (which is mandatory for COM+ events). The object has the following definition and code:
type TInformSubscriber = class(TAutoObject, IMdInform) protected procedure Informs(Code: Integer; const Message: WideString); safecall; end; procedure TInformSubscriber.Informs(Code: Integer; const Message: WideString); begin ShowMessage ('Message <' + IntToStr (Code) + '>: ' + Message); end;
After compiling this library, you can first install it into the COM+ environment, and then bind it to the event. This second step is accomplished in the Component Services management console by selecting the Subscriptions folder under the event object registration, and using the New ® Subscription shortcut menu. In the resulting wizard, choose the interface to implement (there is probably only one interface in your COM+ event library); you'll see a list of COM+ components that implement this interface. Selecting one or more of them sets up the subscription binding, which is listed under the Subscriptions folder. You can see an example of my configuration while building this example in Figure 12.13.
Figure 12.13: A COM+ event with two subscriptions in the Component Services management console
Finally, you can focus on the application that fires the event, which I've called Publisher (because it publishes the information other COM objects are interested in). This is the simplest step of the process, because it is a plain COM client that uses the event server. After importing the COM+ event type library, you can add to the publisher code like this:
var Inform: IMdInform; begin Inform := CoMdInform.Create; Inform.Informs (20, Edit1.Text);
My example creates the COM object in the FormCreate method to keep the reference around, but the effect is the same. Now the client program thinks it is calling the COM+ event object, but this object (provided by the COM+ environment) calls the method for each of the active subscribers. In this case you'll end up seeing a message box:
To make things more interesting, you can subscribe the same server twice to the event interface. The net effect is that without touching your client code, you'll get two message boxes, one for each of the subscribed servers. Obviously this effect becomes interesting when you have multiple different COM components that can handle the event, because you can easily enable and disable them in the management console, changing the COM+ environment without modifying any program code.
COM and NET in Delphi 7
While putting out the new .NET infrastructure, Microsoft has tried to help companies that are continuing their existing programs. One of these migration paths is represented by the compatibility of .NET objects with COM objects. You can use an existing COM object in a .NET application, although not in the realm of managed and safe code. You can also use .NET assemblies from Windows applications as if they were native COM objects. This functionality takes place thanks to wrappers provided by Microsoft.
Borland's claim to support COM/.NET interoperability in Delphi 7 is mainly a reference to the fact that COM objects compiled with Delphi won't create trouble for the .NET importer. In addition, Delphi's type library importer can work seamlessly with .NET assemblies as with standard COM libraries.
Having said this, unless you have an existing large investment in COM, I'd discourage you from following this path. If you want to bet on Microsoft technologies, the future lies in native .NET solutions. If you don't like Microsoft technologies or want a cross-platform solution, COM will still be a worse choice than .NET (in the future, we may have a .NET framework for other operating systems).
Tip |
The steps suggested here should work also in Delphi 6. Delphi 7 adds an automatic import system that at times has troubles with some of the code generated by the compiler of the Delphi for .NET Preview. |
To demonstrate .NET importing features, I've created a .NET library with an interface and a class implementing it. The interface and class resemble those of the FirstCom example discussed at the beginning of this chapter. Here is the code of the library, which must be compiled with the Delphi for .NET preview compiler. You must create an object, or the linker will remove almost everything from your compiled library (assembly, in .NET jargon):
library NetLibrary; uses NetNumberClass in 'NetNumberClass.pas'; {$R *.res} begin // create an object to link all of the code TNumber.Create; end.
The code is in the NetNumberClass unit, which defines an interface and a class implementing it:
type INumber = interface function GetValue: Integer; procedure SetValue (New: Integer); procedure Increase; end; TNumber = class(TObject, INumber) private fValue: Integer; public constructor Create; function GetValue: Integer; procedure SetValue (New: Integer); procedure Increase; end;
Notice that, unlike a COM server, the interface doesn't require a GUID, following .NET rules (although it can have one using an attribute of the GuidAttribute class). The system will generate one for you. After compiling this code (available in the NetImport folder of this chapter's code) with Delphi for .NET Preview (not with Delphi 7!), you need to perform two steps: First, run Microsoft's .NET Framework Assembly Registration Utility, regasm; second, run Borland's Type Library Importer, tlibimp. (In theory, you should be able to skip this step and directly use the Import Type Library dialog box, but with some libraries the use of the tlibimp program is required.)
In practice, go to the folder where you've compiled the library and type from the command line the two commands in bold (you should see the rest of the text I've captured here):
C:md7codeNetImport>regasm netlibrary.dll Microsoft (R) .NET Framework Assembly Registration Utility 1.0.3705.0 Copyright (C) Microsoft Corporation 1998-2001. All rights reserved. Types registered successfully C:md7codeNetImport>tlibimp netlibrary.dll Borland TLIBIMP Version 7.0 Copyright (c) 1997, 2002 Borland Software Corporation Type library loaded .... Created E:ooksmd7code12NetImportmscorlib_TLB.dcr Created E:ooksmd7code12NetImportmscorlib_TLB.pas Created E:ooksmd7code12NetImportNetLibrary_TLB.dcr Created E:ooksmd7code12NetImportNetLibrary_TLB.pas
The effect is to create a unit for the project's type library and a unit for the imported Microsoft .NET Core Library (mscorlib.dll). Now you can create a new Delphi 7 application (a standard Win32 program) and use the .NET objects as if they were COM objects. Here is the code from the NetImport example, shown in Figure 12.14:
Figure 12.14: The NetImport program uses a .NET object to sum numbers.
uses NetLibrary_TLB; procedure TForm1.btnAddClick(Sender: TObject); var num: INumber; begin num := CoTNumber.Create as INumber; num.Increase; ShowMessage (IntToStr (num.GetValue)); end;
What s Next?
In this chapter, I have discussed applications of Microsoft's COM technology, covering automation, documents, controls, and more. You've seen how Delphi makes the development of Automation servers and clients and ActiveX controls, reasonably simple. Delphi even enables you to wrap components around Automation servers, such as Word and Excel. I've also introduced elements of COM+ and discussed briefly the use of ActiveForms inside a browser. I've stated this is not a good approach to Internet web programming—a topic discussed later in the book.
As I mentioned earlier, if COM has a key role in Windows 2000/XP, future versions of Microsoft's operating systems will downplay its role to push the .NET infrastructure (including SOAP and XML). But you'll have to wait until Chapter 23 for a complete discussion of Delphi SOAP support.