Let's start by looking at the general concepts behind implementing components that will run within the MTS environment. The four major tasks are as follows:
All components that run in the MTS environment must be implemented as in-process components. Each component must provide a class factory for each COM class in the component, as well as a type library that describes all of the COM classes and their interfaces.
In addition to these requirements, MTS components should be written as single-user components. The MTS Executive, MTXEX.DLL, provides thread management services, so you don't need to be concerned about multithreading issues in your components. In fact, you should not create threads on your own within your components. You should just write your components as if they will be used by one caller at a time.
Recall from Chapter 2 that every in-process COM component has a ThreadingModel registry value associated with it that describes how the component's objects will be assigned to threads during execution. If you do not specify a ThreadingModel value, the component uses the single-threaded apartment (STA) model and all objects will execute on the main thread of the containing process. COM synchronizes access to all objects on the main thread, so this approach is not very scalable. In addition, single-threaded stateful objects are prone to deadlock. However, if you use nonreentrant libraries within your component, you might need to use the STA model. A better choice for MTS components is to use the apartment threading model. In the apartment threading model, each object is assigned to a single thread for its entire lifetime, but multiple threads can be used to run multiple objects. This approach improves scalability without overly complicating your component implementation. (You do need to protect global variables from concurrent updates.) It's not necessary to implement fully thread-safe components that support the free-threaded model; if you do, you must not create any threads on your own.
Fortunately, every COM programming tool currently available can generate, at the push of a button, skeleton code that meets these requirements. In the section "Implementing Data Objects" later in this chapter, we'll see what buttons to push in Visual Basic and Visual C++.
What About Java?
Java is a great language for implementing COM components. With the COM support provided by the Microsoft Virtual Machine for Java and Microsoft Visual J++, developers can easily create components from Java classes. MTS even provides a package to help developers access its features from Java: com.ms.mtx.
We won't discuss implementing components in Java in this book, mainly because the Island Hopper sample application doesn't include any Java code. But if you want to use Java to implement your components, the concepts discussed in this book are still applicable. The details of how you code your components will obviously be a little different. Paul Stafford's article "Writing Microsoft Transaction Server Components in Java" is highly recommended to anyone writing components in Java. This article is available on the Microsoft Developer Network (MSDN) Library.
The default behavior provided by today's COM programming tools is "good enough" for most components that you will need in a three-tier application. If you use the defaults, you can spend more time focusing on your application-specific logic. You should save hand-coding the basic COM component infrastructure for times when you cannot meet your performance requirements with the default code provided by your tool. Since COM is a binary standard, you can change the implementation of any component without affecting its clients as long as you don't change the public interface—even to the extent of changing implementation languages. In this book, we use the standard COM implementations provided by the Microsoft Visual Studio tools. If you need to hand-tune your components or just want to learn more about how COM works, many books are available that go into the details of writing COM components, especially for C++ developers. Some of these resources are listed in the bibliography.
Once you have the basic in-process component framework in place, you can add the implementations of COM classes to it. Each COM class exposes its behavior through one or more COM interfaces. Some programming tools, such as Visual Basic, let you expose methods and properties directly on the class and internally convert these methods and properties to a COM interface. You should try to avoid this practice and always define your COM interfaces explicitly. Recall from Chapter 2 that COM interfaces represent a contract between caller and callee, and once published they must not be changed. When your interface definition is mixed in with its implementation, it is far too easy for you to make changes without realizing the consequences. Defining interfaces separately is essential if you will have multiple implementations of the interface. Defining interfaces separately also helps when you have multiple developers working on an application. Developers can quickly create test objects that expose the interfaces, so they can verify that client code that relies on those interfaces works correctly. We'll see how to implement separately defined interfaces in COM classes when we walk through the example components in the section "Implementing Data Objects" later in this chapter.
There might be programming tool features that prevent you from defining interfaces separately. For example, Visual Basic 5.0 always defines a default dispatch-based interface using the methods and properties implemented directly on the class. Scripting languages and other typeless languages that understand only IDispatch will be able to access only this default interface. If you are writing objects in Visual Basic that need to be accessed from scripting languages, you'll have to implement the methods and properties directly in the class.
Within each interface method implementation are two things you need to do (in addition to the actual work of the method). First you need to let MTS know when you have finished with the object's state. Second you need to handle errors within your object and let MTS know whether the method was unsuccessful. You do this using the ObjectContext associated with each object. The IObjectContext SetComplete method indicates that your object has completed its work successfully. The IObjectContext SetAbort method indicates the object encountered an error.
When you call SetAbort, if the object is participating in a transaction, that transaction will fail and managed resources will be rolled back to their state prior to the transaction. This is an extraordinarily powerful mechanism for handling errors when you have many independently implemented objects working together to perform some higher-level functionality requested by a client. If any one object can't complete its work, the entire request fails.
Since most objects will not retain state across method calls, the basic structure of each method will look something like the following pseudocode:
MyMethod(…) On Error Goto ErrorHandler CtxObject = GetObjectContext() Do work If error condition then Raise error CtxObject.SetComplete Return success ErrorHandler: CtxObject.SetAbort Return error
Note that in addition to calling SetAbort when an error occurs, your method should return a normal COM error. Doing so gives clients the information they need to decide whether to continue doing work. SetAbort does not have any effect until the containing transaction ends.
These basic ideas apply to both data objects and business objects. With the basic component framework and a skeleton implementation of each class in place, we can turn our attention to the interesting part: implementing the application logic. For data objects, this process consists primarily of retrieving and storing persistent data.