Delphi Clinic C++Builder Gate Training & Consultancy Delphi Notes Weblog Dr.Bob's Webshop
Dr.Bob's Delphi Notes Dr.Bob's Delphi Clinics Dr.Bob's Delphi Courseware Manuals
 Dr.Bob Examines... #34
See Also: other Dr.Bob Examines columns or Delphi articles

An earlier version of this article originally appeared in Delphi Developer (July and August 2002). Copyright Pinnacle Publishing, Inc. All rights reserved.

Delphi & COM (3) - MTS and COM+
In this third article about Delphi and COM, we'll move on to MTS and COM+. In two previous Dr.Bob Examines articles we build an in-process COM Objects as well as Automation Objects, used the Type Library Editor to add methods to the interface, registered the COM and Automation Objects, and finally used the COM and Automation Servers in a client application.
This time, I want to move from using "plain" COM Objects to MTS (Microsoft Transaction Server) and COM+ Objects.

Microsoft Transaction Server
MTS consists of a set of enhancements and facilities for COM, especially useful in a distributed environment (or application), where a COM object is shared by many clients at the same time. The benefits can be great, as the MTS environment can host COM objects and provide load balancing, just-in-time activation, and more as we'll see in this article.

MTS and COM+
My first experience with MTS was with Windows NT4 and the NT4 Option Pack. As of Windows 2000 (and XP) it was part of the operating system itself and renamed to COM+ (although COM+ contains more than just MTS, as I'll explain in detail some other time). In this article I want to focus mainly on the MTS part of COM+, using Windows 2000 as operating system, so whenever you see a caption with COM+, it will be called MTS on Windows NT4 (but work just the same).

COM+ Components
Let's just get started and create a COM+ component in Delphi 6 now, so we can experience everything first hand. Start Delphi 6, close the default project, do File | New | Other, go to the ActiveX tab of the Object Repository and double-click on the Transactional Object icon. Last times, we saw that a COM Object or Automation Object should be hosted by either a regular application or an ActiveX Library (i.e. you had a choice). For a Transactional Object, however, you no longer have a choice, since MTS and COM+ Objects are all hosted in-process by the MTS or COM+ host environment. As a side-effect, a new ActiveX Library project will be created automatically for us when we want to create a new Transactional Object.

Transactional Object in Object Repository

The actual New Transactional Object dialog is a bit similar to the dialog box that we saw for the regular COM Object and the Automation Object - with some significant differences. The CoClass Name is still the same, and to keep up with the previous examples, I'll specify Euro42 as CoClass Name again. The Threading Model choice is also still present, but the Instancing choice is no longer available. That's because a Transactional Object runs inside MTS or COM+ environment. Instead of the Instancing choice, we should now specify the Transaction model. By default it's set to "Does not support transactions". Although it sounds strange, leave the default options, so only specify Euro42 as CoClass Name and click on the OK button to generate a new Transactional Object in the (also newly created) ActiveX Library, and open the Type Library Editor.

New Transactional Object Dialog

TMtsAutoObject
The first thing we should do now is save all files, using File | Save All. Save Unit1.pas in Euro42.pas (the unit with our IEuro42 interface and TEuro42 Transactional Object), and the project in eBob42.dpr. Now, if you take a look inside the Euro42.pas unit, you'll find the declaration of the TEuro42 class, derived from TMtsAutoObject this time. And as the name already indicates, it implements the IEuro42 interface which in turn is derived from IDispatch. Just as we saw last time with the Automation Objects!

Adding Methods
Now, use the Type Library Editor and add a few methods again to the IEuro42 interface: EuroToGuilder and GuilderToEuro. Like last time, they should each have two arguments of type CURRENCY, called Euro and Guilder (with "in" being the value that gets in, and "out" being the value that gets out - the out argument should be of type CURRENCY*, a pointer to CURRENCY).

Type Library editor for GuilderToEuro method

TEuro42
The class TEuro42, defined inside unit Euro42.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 (including a ShowMessage that will be useful as demonstration in a moment), as you can see in the following listing of the entire unit Euro42.pas:

  unit Euro42;
  {$WARN SYMBOL_PLATFORM OFF}
  interface
  uses
    ActiveX, Mtsobj, Mtx, ComObj, eBob42_TLB, StdVcl;

  type
    TEuro42 = class(TMtsAutoObject, IEuro42)
    protected
      procedure EuroToGuilder(Euro: Currency; out Guilder: Currency); safecall;
      procedure GuilderToEuro(Guilder: Currency; out Euro: Currency); safecall;
      { Protected declarations }
    end;

  implementation
  uses
    ComServ, SysUtils, Dialogs;

  const
    GuilderPerEuro = 2.20371;

  procedure TEuro42.EuroToGuilder(Euro: Currency; out Guilder: Currency);
  begin
    Guilder := Euro * GuilderPerEuro;
    ShowMessage(Format('%.2n euros = %.2n guilders',[Euro,Guilder]))
  end;

  procedure TEuro42.GuilderToEuro(Guilder: Currency; out Euro: Currency);
  begin
    Euro := Guilder / GuilderPerEuro;
    ShowMessage(Format('%.2n guilders = %.2n euros',[Guilder,Euro]))
  end;

  initialization
    TAutoObjectFactory.Create(ComServer, TEuro42, Class_Euro42, ciMultiInstance, tmApartment);
  end.

Installing Euro42
So far, you may have noticed very little difference between Transactional Objects and Automation Objects. However, this will change right now, since we are now ready to install and deploy the Euro42 object. Previously, this could be done with the Run | Register ActiveX Server. And although this still works (for the Euro42 Automation Object), we should now register the Euro42 transactional object using the Run | Install COM+ Objects dialog (note that on WinNT4 it will be the Run | Install MTS Objects dialog).

Install COM+ Objects

This Install COM+ Objects dialog lists all available Transactional Objects in our ActiveX Library project, in this case only Euro42. We must now click in the checkbox for each COM+ Object that we want to install into the COM+ host environment (again, everything is literally called MTS on WinNT4). For each click on a checkbox (in our case only Euro42), a new dialog will appear, see figure 5:

Install COM+ Objects

On the first tab, we can install the Euro42 Object into an existing application. The word application is here used to specify a COM+ package - we'll see some more of them in a moment, for example when opening up the drop-down combobox for the Application Name (see figure 6).

Install COM+ Object - Application Name

In most cases, however, you would want to install your COM+ Object into a new application, so go to the second tab. This gives you the option to specify the name of a new COM+ application, let's call it Delphi 6 Package, so it's clear what the purpose will be (a container for all our Delphi 6 COM+ Objects that we'll write for educational purposes).

Install COM+ Object - New Application

Now, finally click on OK in this dialog, followed by OK in the original Install COM+ Objects dialog (which will close after a few seconds, in which the COM+ Object is installed). You won't get an acknowledgement this time, but it's not hard to see the result of installing the COM+ Object.

Component Services
Using the Start Menu, Programs, Administrative Tools, you can start the Component Services application. In the treeview, open up the Component Services node, the Computers node, and then open up the My Computer node. This will show a number of subnodes, including the COM+ Applications running on My Computer.

Component Services

As you can see in figure 8, we now have a fifth COM+ Application available (compare to the four from the drop-down combobox in figure 6). We can see the different components inside the Delphi 6 Package, and even the interfaces (IEuro42) and methods (EuroToGuilder and GuilderToEuro).

Using COM+ Objects
OK, so registering and installing a COM+ Object is indeed different. How about using it? Well, if you've followed the earlier two articles on Delphi and COM, this will be easy again, using the same code as before. So, start Delphi 6, create a new application, save the form in ClientForm.pas and the project itself in Client.dpr (or something like that). Add the eBob42_TLB Type Library import unit (of the COM+ Object project) as well as the ComObj unit to the uses clause of the new main form.
Now, drop two edit buttons on the Client Form, call them edtEuro and edtGuilder, and drop two buttons, call them btnEuro and btnGuilder. Inside the OnClick event handler of btnEuro, write the following code:

  procedure TForm1.btnEuroClick(Sender: TObject);
  var
    Euro42: IEuro42;
    Euro: Currency;
  begin
    Euro42 := CreateCOMObject(Class_Euro42) as IEuro42;
    Euro42.GuilderToEuro(StrToFloatDef(edtGuilder.Text,0),Euro);
    edtEuro.Text := FloatToStr(Euro)
  end;
As you see, the CreateCOMObject is the same as we've used before. However, because I used a special call to ShowMessage inside the GuilderToEuro method (of the TEuro42 object), we now get a message dialog from the COM+ Object itself, see figure 9.

Euro42 Message Dialog

Not really earthshattering, you'd say. No, not really. But notice the caption of the dialog, which is dllhost (or mtx on WinNT4). And sure enough, our client application is called Client.exe and not dllhost. The reason is that our client application is talking via dllhost (the COM+ run-time environment) to our COM+ application with the eBob42.Euro42 component inside.

Idle Shutdown
And apart from that, there's something else that you may want to see. Don't close the "dllhost"-dialog of figure 9 (or if you closed it, just click on the Euro button again to allow the dialog to show again), and go to the Component Services again of figure 8. If you select the Components node of the Delphi 6 Package, you will see the eBob42.Euro42 component. And the icon is moving, to indicate that the COM+ Object is "alive and kicking" at this time.
The last button on the toolbar of the Component Services application gives the Status View. Click on it to get the status of all COM+ objects in this COM+ application in some more detail, with the number of objects, the activated objects, the ones that are currently "in" a call (waiting to complete), and the amount of time spend in that call (still waiting for us to click on the OK-button, so this time grows). Now, click on the OK button of the dllhost-dialog (to finish the GuilderToEuro request), which should stop the moving icon again. The component is no longer active now. However, if you move up to the COM+ Applications node in the treeview of the Component Services, you may see that the icon for the Delphi 6 Package is still moving. The eBob42.dll is still loaded (and you will get an error message if you try to recompile the eBob42.dll at this time), eventhough the Euro42 COM+ Object is no longer in use. By default it will take 3 minutes (of idle time) before the COM+ application is shutdown automatically. If in the meantime another client request (to any COM+ Object inside the COM+ application) occurs, then the idle counter is reset, of course. If you don't want to wait 3 minutes, you can right-click on the Delphi 6 Package and select Shut down to shut it down manually. Right-click on the Delphi 6 Package and select Properties to get a dialog in which the last tab (labelled Advanced) will allow you to specify the amount of minutes until idle shutdown. Having the COM+ Object remain loaded between client requests is an obvious load balancing feature, which helps to avoid unnecessary loading/unloading of the eBob42.dll in "busy" times.

We've only scratched the surface when it comes to MTS and COM+. I now want to discuss the importance of stateless components in a COM+ environment, demonstrate the just-in-time activation and next time finish with the demonstration of some debugging options and deployment steps (to other machines).

Stateful vs. Stateless
Let's start with the discussion about stateful and stateless components. The TEuro42 COM+ component we've made last month is a good example of a stateless component: it does not retain the state information of any of the clients who are using this COM+ object and are calling its GuilderToEuro or EuroToGuilder methods. This means that the methods TEuro42 COM+ object can be called by any number of clients simultaneously.
To illustrate the difference with a stateful component, let's now build an Account COM+ component - one that will maintain the balance of our bank account, with methods to deposit and withdraw, and to get the current balance, of course. For demonstration purposes, I will make this a stateful component first - one that will remember the current value of the balance. This will not be very useful, since there can be many clients working with the COM+ component at the same time, so we will then turn it into a stateless COM+ component again, and finally introduce you to transactions.

Account
If you want, you can use the eBob42.dpr project from last month (I will, so don't worry where the Euro42 interface came from). Once you've opened the project again, do File | New | Other and select the Transactional Object from the ActiveX tab of the Object Repository. In the New Transactional Object dialog, specify Account as CoClass Name, but leave all options set to their default values (this includes the "Does not support transactions" option for now).
When you click on OK, a new Transactional COM Object has been created, and added to your Type Library. Save the new (import) unit in file Account.pas, and use the Type Library Editor to add three (stateful) methods to the IAccount interface: Balance, Deposit and Withdraw. All get one argument called Amount. For Balance it's an out argument of type CURRENCY*, for the Deposit and Withdraw methods it's an in argument of type CURRENCY.

Type Library Editor

Finally, click on the Refresh Implementation button of the Type Library Editor to make sure the definition of these three methods is also added to the Account.pas type library import unit for the Account interface. The Account.pas unit should now contain the following code:
  unit Account;
  {$WARN SYMBOL_PLATFORM OFF}
  interface
  uses
    ActiveX, Mtsobj, Mtx, ComObj, eBob42_TLB, StdVcl;

  type
    TAccount = class(TMtsAutoObject, IAccount)
    protected
      procedure Balance(out Amount: Currency); safecall;
      procedure Deposit(Amount: Currency); safecall;
      procedure Withdraw(Amount: Currency); safecall;
    end;

  implementation
  uses
    ComServ;

  procedure TAccount.Balance(out Amount: Currency);
  begin

  end;

  procedure TAccount.Deposit(Amount: Currency);
  begin

  end;

  procedure TAccount.Withdraw(Amount: Currency);
  begin

  end;

  initialization
    TAutoObjectFactory.Create(ComServer, TAccount, Class_Account, ciMultiInstance, tmApartment);
  end.
Three methods are waiting their implementation, using a single private field FAmount to store (and hence maintain!) the current amount of money of my balance.
  // TAccount
  private
    FAmount: Currency;
  end;
The implementation is easy, of course:
  procedure TAccount.Balance(out Amount: Currency);
  begin
    Amount := FAmount;
  end;

  procedure TAccount.Deposit(Amount: Currency);
  begin
    FAmount := FAmount + Amount;
  end;

  procedure TAccount.Withdraw(Amount: Currency);
  begin
    FAmount := FAmount - Amount;
  end;
However, this Account object cannot be used by clients that are not working on the same account (and are in fact sharing that same account). So, while in theory it may be useful for this Account COM+ Object to be used by the accountant of a single company - working on one account only, it is less useful in the outside world. In fact, if you want to use transactions (and we are talking about a "transactional object" here, so I assume you want to use transactions), then your object must be stateless. Nothing else will work. But before we move on to transactions, let's first show how the Account object would work in a stateful world...

Registration & Installation
We can now recompile the eBob42.dpr project. Even if you installed it last time, you will again need to do Run | Install COM+ Objects, because a new COM+ Object has been added to the list: Account. Ideally, you would only need to click on the checkbox for the new Object Account, then click on OK to register Account (just as Euro42 is already registered). However, this may result in an error telling you that "the parameter is incorrect", and the fact that the Account object isn't registered.
The only workaround for this is to first unregister the Euro42 object (so nothing shows up as registered in the Delphi 6 Package), and then register both the Euro42 and the Account object at the same time. I'm not sure if this is a Delphi 6 Update 2 problem or a COM+ issue, but at least you can get it to work - in the end...

Stateful and working
Now, let's use our stateful Account object, which just has transactions supported (which should turn it into a stateless component). For this, we can open up the client application from last time, reuse the edtEuro editbox and drop three new buttons: one to retrieve the current value of the Account, one to deposit an amount, and one to withdraw an amount.
We also need a global instance of our Account object (so we don't have to recreate it - which would also automatically destroy it once we're done, loosing it's state information), and we retrieve an instance of the Account object in the FormCreate method. When the form is destroyed, the Account object will automatically be destroyed as well.

    // TForm1
    private
      Account: IAccount;
    end;

  procedure TForm1.FormCreate(Sender: TObject);
  begin
    Account := CreateCOMObject(Class_Account) as IAccount;
  end;
The OnClick event handlers of the three buttons are defined as follows:
  procedure TForm1.btnAccountClick(Sender: TObject);
  var
    Euro: Currency;
  begin
    Account.Balance(Euro);
    edtEuro.Text := FloatToStr(Euro)
  end;

  procedure TForm1.btnDepositClick(Sender: TObject);
  var
    Euro: Currency;
  begin
    Euro := StrToFloatDef(edtEuro.Text,0);
    Account.Deposit(Euro)
  end;

  procedure TForm1.btnWithdrawClick(Sender: TObject);
  var
    Euro: Currency;
  begin
    Euro := StrToFloatDef(edtEuro.Text,0);
    Account.Withdraw(Euro)
  end;
We can now start the application (which creates an instance of our COM+ Account object), add 100 euro to our account, withdraw 42 euros, and check the balance to see that there are 58 euros left. All this works, because the clients are all working on the same (shared) account. However, we will now add transaction support to this scheme, which will change the server implementation from being stateful to stateless (automatically - with all stateless side-effects that come with it).

Adding Transaction Support
In order to show you how this very simple example (of a stateful and hence "wrong" transactional object) will break, you need to select the Account object in the Type Library Editor, go to the COM+ tab, and in the Transaction Model drop-down combobox change the default "Does not support transactions" with the more COM+ like "Requires a new transaction":

Requires a new transaction

As a side-effect, the drop-down combobox with the value for the Call Synchronization option will move from None to Required.
Our Account object now contains transaction support. The three methods of Account must now make sure that a transaction is either successfully completed or (unsuccessfully) terminated. We'll get into more details later, but for now each of the three method must add one line (just before the end-statement of the method itself), namely:
  SetComplete
SetComplete is a method from the TMtsAutoObject, that uses the ObjectContext to call SetComplete. The alternative of SetComplete is SetAbort (see the unit Mtsobj.pas for more details).

Re-Registration?
We can now recompile the eBob42.dpr project. And we should even re-register it, since there's a big difference now: the Account object requires a new transaction for every method call. However, since registering Account took more time than anticipated in the first place, we can also right-click on the eBob42.Account object in the Component Services dialog, can start the properties dialog and go to the Transactions tab to set the "Requires New" option.

Requires New

Stateful and failing
Now, let's use our stateful Account object, which just has transactions supported (which should turn it into a stateless component). For this, we only need to run the client application again. You may be in for a surprise, since no matter how much you deposit (or even better: no matter how much you withdraw), the balance will remain the same, a nice big 0.
Since the Account object is now a stateless object, it is unable to retain its state any longer. And having a single variable at the client side pointing to the Account COM+ object doesn't matter: in between calls the server object will be removed (when no longer needed), or used by someone else, and it will be recreated - empty again - when needed.

Fixing Accounts
Obviously, we need to make sure that we no longer need to rely on the built-in FAmount field in the Account object, but rather store the account information in an external resource - such as a database or a .ini file - and use the Type Library Editor to add one more argument to each of the three Account methods: the account number of type BSTR (a WideString in Delphi). Make sure to click on the refresh button so the Type Library import unit as well as the definition of the TAccount object in the Account.pas unit is updated as well. For a quick-and-dirty (and small) implementation of the new Balance, Deposit and Withdraw methods, I'm using a .ini file at this time (for real-world use you would use a database of course (perhaps even one that also explicitly supports transactions, although database transactions and COM+ transactions are generally non-related).
The final implementation of the TAccount object in the Account.pas unit is as follows:

  unit Account;
  {$WARN SYMBOL_PLATFORM OFF}
  interface
  uses
    ActiveX, Mtsobj, Mtx, ComObj, eBob42_TLB, StdVcl;

  type
    TAccount = class(TMtsAutoObject, IAccount)
    protected
      procedure Balance(const Account: WideString; out Amount: Currency); safecall;
      procedure Deposit(const Account: WideString; Amount: Currency); safecall;
      procedure Withdraw(const Account: WideString; Amount: Currency); safecall;
    end;

  implementation
  uses
    ComServ, IniFiles, SysUtils, Dialogs;

  procedure TAccount.Balance(const Account: WideString; out Amount: Currency);
  begin
    with TIniFile.Create('.\bank.ini') do
    try
      Amount := StrToFloatDef(ReadString('Balance', Account, ''),0)
    finally
      Free;
      SetComplete
    end;
  //ShowMessage(Format('Balance: %.2N', [Amount]));
  end;

  procedure TAccount.Deposit(const Account: WideString; Amount: Currency);
  var
    Balance: Currency;
  begin
    with TIniFile.Create('.\bank.ini') do
    try
      Balance := StrToFloatDef(ReadString('Balance', Account, ''),0);
      Balance := Balance + Amount;
      WriteString('Balance', Account, FloatToStr(Balance))
    finally
      Free;
      SetComplete
    end;
  //ShowMessage(Format('Deposit: %.2N - New Amount: %.2N', [Amount, Balance]));
  end;

  procedure TAccount.Withdraw(const Account: WideString; Amount: Currency);
  var
    Balance: Currency;
  begin
    with TIniFile.Create('.\bank.ini') do
    try
      Balance := StrToFloatDef(ReadString('Balance', Account, ''),0);
      Balance := Balance - Amount;
      WriteString('Balance', Account, FloatToStr(Balance))
    finally
      Free;
      SetComplete
    end;
  //ShowMessage(Format('Withdraw: %.2N - New Amount: %.2N',  [Amount, Balance]));
  end;

  initialization
    TAutoObjectFactory.Create(ComServer, TAccount, Class_Account, ciMultiInstance, tmApartment);
  end.
Note that I've added a call to ShowMessage (in comments), which can help to see the server respond to different incoming client requests. Since we now add the account information itself, every request will start with an empty "state", and will have to retrieve the account balance first, before it can either return that balance, deposit or withdraw money from it.

New Client
Since we must now pass the account number information, we need to slightly modify the client application. We can still use the FAccount member field of type IAcount, but every call to a method (Balance, Deposit or Withdraw) now has one extra argument: the account number. Other than that, there's no change to the client code: an instance of the FAccount field will be made when the form is created, and will automatically be destroyed when the form is destroyed (because then the IAccount interface gets out of scope). And in between those two events (create and destroy), the server is available and ready to be used by us. Right?
Actually, that's what the client thinks, but behind the scenes, COM+ implements a just-in-time activation scheme, which is the last topic of this month...

Just-in-Time Activation
Stateless transactional objects have no state, and are also created when we need them (and no sooner), and destroyed after we've used them. However, unlike the client who can keep an instance to the server, the server may not be actually active at those time. In fact, they may be destroyed between two subsequent client calls, without your client being aware of it. This may sound scary, but the good thing is that the clients will not be aware of it, since the server will be created (and activated) when we need it: just-in-time.
To "demonstrate" this feature, you should add two methods to the public section of the TAccount class definition:

  procedure Initialize; override;
  destructor Destroy; override;
The implementation of these two can be simple (using ShowMessages) as follows:
  destructor TAccount.Destroy;
  begin
    inherited;
    ShowMessage('TAccount destroyed at '+TimeToStr(Time))
  end;

  procedure TAccount.Initialize;
  begin
    inherited;
    ShowMessage('TAccount created at '+TimeToStr(Time))
  end;
If you recompile the server, and then run the client, you'll notice that you get a messagebox telling you that TAccount has been created right at the start of the client. However, after you've called the first method (a press on the Balance, Deposit or Withdraw button), you get a message that the TAccount has been destroyed. And from that moment on, every method call (i.e. every button click) will result in a TAccount being created, used and immediately thereafter destroyed. The client still thinks it's using a form-global TAccount instance, while in fact MTS/COM+ is cleaning up the TAccount server instance as soon as it's no longer needed.
The benefit of Just-in-Time Activation is that multiple server instances can be destroyed if clients are not actually using them (which saves memory and resources on the server side), and then recreated when they are used. In fact, it should be possible to share or pool server instances for even greater performance benefits.

OnCompile Helpers
Have you ever written and debugged COM+ Applications? If so, you must have experiences the "problem" that you have to shut down the COM+ Application (using the Component Services) from time to time, especially when rebuilding the DLL or just before debugging. Because this can be a pain, I was glad to find a copy of OnCompile Helpers (the extended version of the COM+ Helpers) from elitedev.com.
OnCompile Helpers is a Delphi IDE AddIn wizard that shows up in the Project menu, and allows you - among others - to shut down your COM+ applications from the Delphi IDE. It also contains a Manage COM+ Applications dialog that lists all COM+ applications that are installed on our system. We can select which of these should be shut down automatically when we rebuild our project. As an additional feature, it also contains a button "Set as Debug Host" which will copy the appropriate parameters in the Run Parameters dialog. Only minor things perhaps, but handy if you frequently work with COM+ projects!
OnCompile Helpers runs on Windows 2000 or XP and is available for Delphi 5, 6 and 7. You can also download a fully functional trial edition from http://www.elitedev.com (with a splash screen and delay at startup, but otherwise fully functional).


This webpage © 2002-2010 by Bob Swart (aka Dr.Bob - www.drbob42.com). All Rights Reserved.