|Delphi Clinic||C++Builder Gate||Training & Consultancy||Delphi Notes Weblog||Dr.Bob's Webshop|
Delphi & COM (2) - OLE Automation
In this second article about Delphi and COM, we'll cover OLE Automation. Last time, we build an in-process COM Objects called Euro, used the Type Library Editor to add methods to the IEuro interface, registered the COM Object, and finally called CreateCOMObject to use the COM Server in a client application. This time, I want to move from using "plain" COM Objects to OLE Automation. Besides that, which doesn't involve that much work, I want to add some more useful methods to the IEuro interface, and finally I want to show no less than three ways in which we can use these OLE Automation COM Objects in our client applications.
In-Process COM Server
Let's start by building an OLE Automation Object first. Like last time, we only focus on in-process COM Servers. This means that the COM Object (or OLE Automation Object) resides in a DLL, which gets loaded in the same process as the calling (client) application. Hence the name in-process. The alternative is called out-of-process, and involves a COM Object inside an executable. For examples of out-of-process COM Objects I would like to refer to DataSnap middle-ware executables, which can contain remote data modules - COM Objects that implement the IAppServer interface.
Anyway, for an in-process COM Server, we need to start with an ActiveX Library (instead of a regular executable), so do File | New - Other, and select the ActiveX Library icon from the ActiveX tab of the Object Repository. This will start a new ActiveX Library project, which should be saved in Auto42.dpr this time.
Now that we have an ActiveX Library, we can add things to it. Last time, we added a regular COM Object to it, but this time we need to select an Automation Object. Do File | New - Other and double-click on the Automation Object icon in the ActiveX page of the Object Repository:
This will present you with the Automation Object Wizard, in which the minimum amount of information that we have to specify consists of the CoClass Name of the new Automation Object. In our case, let's call it Euro42 (last time we used the Euro COM Object, so let's create an Euro42 Automation Object now).
Instancing and Threading Model was explained last time (and should generally be left on their default values). The last option can be used to generate event support code, something which we won't be using, so leave that one unchecked as well.
Type Library Editor
The result of the new Automation Object Wizard is a new unit, which can be saved in file Euro.pas, containing the definition of class TEuro42, derived from TAutoObject and implementing the IEuro42 interface. Note that Automation Objects descend from TAutoObject, while regular COM Objects (last time) descend from TTypedComObject. This difference can be explained a bit further if you take a look at the Type Library editor, where the IEuro42 interface is defined.
As you can see from Figure 3, the IEuro42 interface is now derived from the IDispatch parent interface, where a regular COM interface (again like last time) is derived from IUnknown. What's the difference between IDispatch and IUnknown? Actually, if you search inside System.pas, you'll find that IDispatch is derived directly from IUnknown, adding four functions to the interface definition, including a method called Invoke. Obviously, these methods are implemented by the new type TAutoObject which is the parent class of our TEuro42 class (so we don't have to worry about implementing them), but it should be clear that IDispatch can do something more than a "simple" IUnknown can do - as we'll see later in this article, when we try to use the Automation Object from a client application.
If you take another look at the Type Library Editor of Figure 3, then you must have noticed two new methods (EuroToGuilder and GuilderToEuro) that were already added by me. You can add new methods using the button with the green arrow (the same arrow that appears on the left of the method name).
Of course, these two methods EuroToGuilder and GuilderToEuro won't do any good without arguments. But before I can tell you how to add the arguments, we must first make sure we're talking the same language. By that I mean the same language inside the Type Library Editor - which can be Pascal or IDL. I actually prefer IDL, since that's the Interface Description Language native to COM, and more likely to be useful if you ever want to communicate with COM Objects (servers or clients) written in other environments. The place to check this setting is the Type Library page of the Tools | Environment Options dialog:
Now then, once you've made sure it's set to IDL (which should be the default), you can return to the Type Library Editor. Click on the EuroToGuilder method, and move to the Arguments tab of the Type Library Editor. Here, we can add the arguments. We need to arguments for the EuroToGuilder. The first one is called Euro and is of type Currency, with the Modifier set to [in]. This last bit means that the argument can be used as input, but nothing can be written to it. The second parameter has the name Guilder, is also of type Currency, and must be an [out] modifier. If you try to specify that, you'll get an error message, since [out] parameters must be pointers. As a result, just add a * to the currency type, as you can see in figure 5:
We have to perform a similar set of steps for the GuilderToEuro method, but this time the Guilder should be the in parameter, and the Euro the out parameter. Actually, it's very easy to repeat this, since we can move to the Text tab and see the representation of the EuroToGuilder function as follows:
[id(0x00000001)] HRESULT _stdcall EuroToGuilder([in] CURRENCY Euro, [out] CURRENCY * Guilder );This looks good, and we can copy the arguments from this text and move them over to the Text tab for the GuilderToEuro method. Make sure to change the Euro to Guilder and vice versa, and you end with the following text representation of the GuilderToEuro function:
[id(0x00000002)] HRESULT _stdcall GuilderToEuro([in] CURRENCY Guilder, [out] CURRENCY * Euro );The best thing is: writing text here will automatically update the definition in the Type Library Editor itself. We only have to click on the Refresh button in order to make sure the Type Library import unit (and corresponding unit for the TEuro42 class definition) is updated.
The class TEuro42, defined inside unit Euro.pas, now contains two empty method definitions - including the skeletons to implement them. Of course, the implementation is a simple matter of multiplying and dividing with the right constant, as you can see in the following listing:
const GuilderPerEuro = 2.20371; procedure TEuro42.EuroToGuilder(Euro: Currency; out Guilder: Currency); begin Guilder := Euro * GuilderPerEuro end; procedure TEuro42.GuilderToEuro(Guilder: Currency; out Euro: Currency); begin Euro := Guilder / GuilderPerEuro end;Surely, this is more interesting than the simple About message of last time (which you can still add, if you want to). Once you've finished adding methods to the IEuro42 interface and implementing them in the TEuro42 class, we should compile and register our OLE Automation server. This is again done using Run | Register ActiveX Server.
Using Automation Objects
And now we get to the really interesting part of the article: there is more than one way by which we can use this Euro42 Automation object. In fact, there are at least three ways: using variants (the most flexible, but most dangerous as well), using dispinterfaces, and finally using interfaces as we saw last time.
Why would we want to use anything but interfaces in the first place? Well, mainly because when using Variants we are performing "late binding" to the Automation Object, meaning you don't have to worry about type libraries or import units, you just call the methods and pass arguments that you think are correct. At run-time (the late binding), the methods and arguments will be resolved and invoked. Or an error message will be shown (if the method is unknown, or the arguments are invalid). Quick and easy. In fact, any registered Automation Object on your machine can be used in the same way (without the need for dozens if not hundreds of type library import units), which can be really convenient.
The code to use the Automation Object using variants is as follows:
procedure TForm1.Button1Click(Sender: TObject); var Euro: Variant; Guilder: Currency; begin try Euro := GetActiveOleObject(EuroClassName) except Euro := CreateOleObject(EuroClassName) end; Euro.EuroToGuilder(StrToFloat(edtEuro.Text),Guilder); // NO Code Insight!! edtGuilder.Text := FloatToStr(Guilder) end;How do Variants do their job? This is done using the IDispatch interface, which contains two special methods - called GetIDOfNames and Invoke - that do the dirt work behind the scenes. GetIDOfNames is taking a method name and translates it into a dispatch id. The Invoke then calls this method, based on the dispatch id. Obviously, searching for the dispatch id takes time, and can result in an error if the method cannot be found.
So, although convenient in use, the big danger of using Automation Objects by means of Variants is the fact that it's easy to make a typing mistake, and you won't find out until you hit that point at run-time. There's not even Code Insight support, all simply because neither the methods nor their arguments are known at design-time due to the late-binding mechanism.
Dispinterfaces are another example of late binding, but this time they show some of the characteristics of early binding as well - including Code Insight support! The difference is due to the fact that a dispinterface is listing the methods with their dispatch id already! So when calling these methods - using late binding - the GetIDOfNames doesn't have to be used anymore, only the Invoke has to be called with the predefined dispatch id. The code to use the Automation Object using dispinterfaces is as follows:
procedure TForm1.Button2Click(Sender: TObject); var Euro: IEuro42Disp; Guilder: Currency; begin Euro := CoEuro42.Create AS IEuro42Disp; Euro.EuroToGuilder(StrToFloat(edtEuro.Text),Guilder); // Code Insight! edtGuilder.Text := FloatToStr(Guilder) end;Personally, I find this a better way than using variants. We still use the dispatch interface, but this time with a generated dispinterface, so it will be impossible to make typing mistakes (the feeling of early-binding when in fact we're using late binding).
The final usage example is the true early-binding using plain interfaces. This is the same as we saw last time, when we examined "plain" COM Objects, and is implemented as follows:
procedure TForm1.Button3Click(Sender: TObject); var Euro: IEuro42; Guilder: Currency; begin Euro := CoEuro42.Create; Euro.EuroToGuilder(StrToFloat(edtEuro.Text),Guilder); // Code Insight! edtGuilder.Text := FloatToStr(Guilder) end;Now that we've seen all three different ways, you may ask yourself: which technique is the best to use? That depends actually, on the situation at hand. Variants can be a convenient way to start and use an Automation Object for which you don't have (or want to use) the type library import unit. People have been using WinWord in this way for years. Oh the other hand, the calls to GetIDOfNames and Invoke take their toll (and execution time) as well, rendering the late binding ways slower than the plain interfaces (early binding) way.
Talking about more clients, next time we will take a look at Microsoft's Transaction Server with MTS and COM+. A bit more complex again, but more powerful as well. So stay tuned for more...