As you can see, it's not too difficult to get a simple application up and running using MTS. And you don't have to do anything special to an ordinary ActiveX DLL to deploy it in the MTS run-time environment. However, if you really want your Visual Basic objects to take advantage of MTS, you must understand what's going on inside the MTS environment. You should begin by getting to know the central figure in this environment, the MTS Executive. You can think of the MTS Executive as the MTS run time.
The MTS run time plays the role of quarterback for every MTS application. You can also think of the MTS run time as a control-freak boss who always wants to know what everybody's doing at any given time. Let's look at how the MTS run time micromanages every MTS object starting at activation time.
As you'll recall from Chapter 3, a standard activation request in COM is routed from the SCM to a class factory object that has been loaded by its server. The server must therefore supply a class factory object for each CLSID that it supports. However, when the SCM attempts to activate an object in an MTS application, the MTS run time intercepts the request. It then locates the class factory object for the requested CLSID and calls upon it to create the object.
Here's where the plot thickens. While creating and binding the object, the MTS run time places a wiretap on the connection back to the client. The MTS run time sets up this wiretap by introducing a transparent layer between the client and the object called the context wrapper, as shown in Figure 9-3. The context wrapper is situated between the stub and the object. When the context wrapper receives a method call from the client, it forwards the call to the object that it's wrapping. When the object finishes processing the call, the control of execution passes back through the context wrapper before it's returned to the base client. The base client is totally oblivious to what's going on. It can't detect the presence of the context wrapper.
Figure 9-3. The MTS run time inserts a context wrapper between a base client and an MTS object. This transparent layer allows the MTS run time to monitor the connection and steal away control on a call-by-call basis.
This is a pretty neat little trick. The context wrapper allows the MTS run time to steal control of every inbound method call just before and just after the object executes its method implementation. This powerful object-oriented technique is known as interception. Interception allows the MTS run time to modify the behavior of object code with preprocessing and postprocessing on a call-by-call basis. MTS mixes interception and attribute-based programming to produce a powerful programming model. For example, you can drastically change the behavior of an object by changing its transaction support attribute. When you do this, the MTS run time modifies the object's behavior by changing the way it carries out its preprocessing and postprocessing of method calls.
In addition to providing a basis for interception, the context wrapper is used to store state information about the object and the object's creator. The context wrapper holds metadata about the CLSID and the coclass that represent the object's type. It also holds security information about the user who activated the object. The next few chapters explain other essential pieces of information that are stored in the context wrapper.
When you create an ActiveX DLL that is targeted for the MTS run-time environment, you should include a reference to the Transaction Server type library (mtxas.dll), as shown in Figure 9-4. This allows you to communicate directly with the MTS run time and take full advantage of MTS. When you program against the Transaction Server type library, it means that you're writing your ActiveX DLL with the intention of using it exclusively in the MTS run-time environment.
Figure 9-4. By referencing the MTS type library, you can communicate with the MTS run time.
After you reference this type library for the first time, you should examine its contents using the Object Browser. There are two important methods in the class AppServer named SafeRef and GetObjectContext. The AppServer class is marked in the type library as an AppObject. This means that you can call both SafeRef and GetObjectContext as global functions. Figure 9-5 shows what these two important methods return.
Figure 9-5. A call to SafeRef returns the outside view of the object as the MTS run time intends the object's base clients to see it. A call to GetObjectContext returns the inside view of an object as the MTS run time sees it in the context of the current method call.
SafeRef returns the outside world's view of an MTS object. That is, it returns a reference to the context wrapper instead of a reference to the object itself. Be careful not to pass to a client application a direct reference to an MTS object. If you do, the client application can make a call on the MTS object without going through the context wrapper. This defeats the interception scheme set up by the MTS run time.
Let's say that you're writing a method implementation for an MTS object. In the method, you must create a second object and set up bidirectional communication between the two by registering a callback like that shown in the section entitled "Using a Custom Callback Interface" in Chapter 6. After creating the second object, you must pass it a reference from your object to set up the callback. To do this, you would write the following code:
Dim Child As CChildClass Set Child = New CChildClass Child.Register Me
This code is incorrect under MTS, however. The child object can invoke method calls on your object without going through the context wrapper. You should never bypass the interception scheme set up by the MTS run time. Instead, you should pass a reference to your object as follows:
Dim Child As CChildClass Set Child = New CChildClass Child.Register SafeRef(Me)
By calling SafeRef, you allow the child object to establish a connection that passes through the context wrapper. This keeps things in line with what the MTS run time expects. Note that the Me keyword is the only valid parameter you can pass when calling SafeRef with Visual Basic.
The previous example uses the New operator to create objects. As you'll soon see, this can also result in incorrect code under MTS. Later in this chapter, we'll look at the proper techniques for creating objects in the MTS environment in a variety of situations.
While SafeRef provides a view of your object to the outside world, GetObjectContext provides your object with an internal view to the MTS run time. A call to GetObjectContext returns a reference to a new object that the MTS run time creates, as shown earlier in Figure 9-5. The object returned by GetObjectContext represents the calling context in which your object is executing. It's important not to confuse the object context and the context wrapper because they're very different. The object context is transient, while the context wrapper lives for the lifetime of the client's connection.
You use the ObjectContext interface to communicate with the MTS run time through the object context. The ObjectContext interface contains methods and properties that allow you to query different types of state information about the context in which your call is executing. Methods in this interface also allow you to tell the MTS run time to perform specific tasks on your behalf. For example, you can ask for assistance when you want to create additional MTS objects or you want to affect the outcome of the current transaction. Let's start our examination of the ObjectContext interface by seeing what kind of information it can give you.
The concept calling context can be elusive to new programmers. The object behind the ObjectContext interface can be seen as an aggregate. First the ObjectContext interface can provide information about the state of affairs inside the current application. For example, if you want to find out whether your object has been loaded into a secure MTS server process rather than into the process of a nonsecure client application, you can write the following code:
Dim ObjCtx As ObjectContext Set ObjCtx = GetObjectContext() Dim IsAppSecure As Boolean IsAppSecure = ObjCtx.IsSecurityEnabled()
You can also use the ObjectContext interface to obtain state information about the object that's executing the current call. When the MTS run time creates an object, it stores the user name of the creator inside the context wrapper. You can obtain the name of the creator by using the Security property of ObjectContext:
Dim ObjCtx As ObjectContext Set ObjCtx = GetObjectContext() Dim Creator As String Creator = ObjCtx.Security.GetOriginalCreator()
ObjectContext also provides information that is propagated across the wire with every method call. For example, the user who is initiating a call on the object might be different from the user who originally created the object. Distributed COM transmits the caller's identity across the wire in each method call. The ObjectContext interface allows you to retrieve the caller's name like this:
Dim ObjCtx As ObjectContext Set ObjCtx = GetObjectContext() Dim Caller As String Caller = ObjCtx.Security.GetOriginalCaller()
As you can see, many different bits of information make up the calling context for an object. The MTS run time makes it easy to deal with all this information by grouping it all together in a single easy-to-use object.
ObjectContext lets you do more than just retrieve information. You can also use this interface to tell the MTS run time to do things. Later in this chapter, you'll learn how to create other MTS objects and manage threads using ObjectContext. In Chapter 10, you'll learn how to use the ObjectContext interface to affect the outcome of a transaction.
The MTS run time is responsible for managing the life cycle of every MTS object. The life cycle of an MTS object typically includes four stages: creation, activation, deactivation, and destruction. As an MTS programmer, you must be aware of when the transitions between these stages occur so that you can take appropriate action.
A Visual Basic base client can create an object in the MTS environment using either the New operator or the CreateObject function. When a base client creates an MTS object, the MTS run time does two things. First it locates the class factory and creates the object. Next it binds the client to the context wrapper. However, the object isn't activated inside the context wrapper at this time. The client senses that the object has been activated, but in fact only the context wrapper has been activated. The MTS run time defers activating the object inside the context wrapper until the client invokes the first method call. After the first method call, the object remains in an active state until it's destroyed. An activated object is always deactivated before it's destroyed.
A Visual Basic class module provides an Initialize procedure. The code you write in Initialize is guaranteed to run when the Visual Basic object is created. However, Initialize always runs before the object has been activated. This means that you can't successfully call GetObjectContext in Initialize. A call to GetObjectContext in Initialize won't experience a run-time error. It will simply return a null reference. You will get a run-time error, however, when you try to access a property or a method using this null reference.
The Terminate procedure in a Visual Basic class is executed prior to the object's destruction and after the object has been deactivated. Terminate is similar to Initialize in that you can't successfully call GetObjectContext in this procedure. These two procedures supplied by Visual Basic don't give you all the control you need to manage your object's life cycle in the MTS environment. Fortunately, the MTS run time can help by notifying your object just after activation and once again just before deactivation.
Here's how it works. The MTS type library includes a definition for an interface named ObjectControl. When the MTS run time creates an MTS object, it calls QueryInterface to determine whether the object supports this interface. If an MTS object implements ObjectControl, the MTS run time calls methods in this interface to notify the object at important transition stages during its life cycle. This means that you should implement ObjectControl in every object that you think needs to receive these notifications. The ObjectControl interface contains the three methods listed below.
To receive these notifications, you should implement the ObjectControl interface in the MultiUse class modules in your ActiveX DLLs. Here's an example of a Visual Basic class module that implements this interface:
Implements ObjectControl Private Sub ObjectControl_Activate() ' Your code for initialization after activation End Sub Private Sub ObjectControl_Deactivate() ' Your code for cleanup before deactivation End Sub Private Function ObjectControl_CanBePooled() As Boolean ' Object pooling not yet supported in MTS 2.0 ObjectControl_CanBePooled = False End Function
The MTS run time calls Activate just before the execution of the first method call. This means that the object has been switched into the context wrapper and you can successfully call GetObjectContext from within this method. It's therefore usually best to put your object initialization code inside Activate instead of Initialize. Likewise, you should place your cleanup code in Deactivate instead of Terminate.
Note that a client must invoke at least one method call to place an object in the active state. If a base client creates an object and then releases it without calling a method, Activate and Deactivate are never called. In some situations, you might need to use Initialize and Terminate in addition to Activate and Deactivate.
What about the third method in the ObjectControl interface, CanBePooled? Unfortunately, at the time of this writing the method is useless. It was included to support a future feature called object pooling. When object pooling becomes available, the MTS run time will be able to recycle objects and thus save some processing cycles associated with object destruction and re-creation. After an object has been deactivated, the MTS run time will call CanBePooled to see whether the object indicates that it wants to be placed in the recycling pool. Objects that return a value of True from CanBePooled will be placed in a pool. Objects that return False will be destroyed. When the MTS run time services a creation request, it will look through the pool for one of the appropriate type.
Although MTS run time calls CanBePooled in MTS 2.0, the value returned by your objects is ignored. The MTS run time always destroys your object after calling CanBePooled. Many people advocate returning a value of True so that your object will automatically take advantage of object pooling when it becomes available. However, you shouldn't do this casually. Managing the life cycle of pooled objects is far more complex than managing the life cycle of nonpooled objects.
The bottom line is that you should never return True from CanBePooled unless you have carefully thought through the semantics of object pooling, including the differences between object construction and object reactivation and the differences between object destruction and object deactivation. In most cases, you will be required to add code to Initialize, Activate, Deactivate, CanBePooled, and Terminate. If you don't care to think through these issues to accommodate a feature that isn't presently supported, you should simply return False from CanBePooled and revisit your code when object pooling becomes available.