Designing Components for Scripting Clients

[Previous] [Next]

Up to this point, we've focused on clients that use type information obtained from type libraries. These clients know how to access custom vTables and are compiled along with CLSIDs and IIDs. But scripting clients are much different. They never know or care about your type library. All they know about is your ProgIDs and the names of your methods and properties.

Designing components for scripting clients poses several limitations that make it impossible to use some of the techniques that we discussed earlier in this chapter. You shouldn't design using UDTs because most scripting languages don't offer the syntax to access UDT members. You can define methods using enumerations because the scripting client sees an enumeration type as an integer. However, enumerations aren't nearly as handy when the client can't get their definitions out of a type library.

When you design a method that has ByRef parameters, you should define each of these parameters as a Variant instead of using a more precise type because most scripting hosts can't execute methods with strongly typed output parameters. For example, VBScript can deal with ByRef parameters only of type Variant. A VBScript client will fail with a "Type mismatch" error if it attempts to call a method with a ByRef parameter based on a data type such as Integer, Double, or String.

Scripting Clients and User-Defined Interfaces

Many COM programmers have worked hard to get up to speed on the concepts and practice of interface-based programming. User-defined interfaces require extra work when you design and write an application, but the benefits they provide in larger projects are easy to measure.

When you define your interfaces separately from your creatable components, you can achieve higher levels of reuse, maintainability, and extensibility. You can create polymorphic designs based on plug-compatible components. Your client applications can adapt to any version of a component by conducting runtime tests to see whether an object supports a certain interface. You can also create a robust versioning scheme that makes it possible to safely upgrade a component or client application without disturbing any of the other components or client applications already in production.

Once you grasp its key concepts, interface-based programming becomes addictive. Many Visual Basic programmers have fallen in love with user-defined interfaces and try to use them wherever possible. However, there's an unfortunate problem: Once you create a component that implements user-defined interfaces, it's difficult or impossible to use it directly from a scripting client. That's a problem if you're creating components that will be used by scripting clients such as Web sites built with Active Server Pages (ASP).

What's the problem?

The problem is rooted in the fact that scripting clients are written in typeless languages. For example, when you write VBScript in an ASP page, all your variables and parameters are defined as Variants. When you establish a connection to a COM object, you can't specify which interface you want to use. Instead, a VBScript client is always connected to a COM object through its default interface. Also, the default interface must be an IDispatch-style interface or a dual interface. IDispatch allows a scripting client to access the object through late binding.

In COM, clients navigate from one interface to another by calling QueryInterface. C++ clients can call QueryInterface directly. Visual Basic clients can call QueryInterface indirectly by assigning object references to variables and parameters that have the desired interface type. However, scripting clients can't call QueryInterface either directly or indirectly. They can't navigate to a secondary interface. In essence, they're stuck with the default interface to which they were originally connected. As you can see, scripting clients simply don't provide the required support to take advantage of components that implement more than one interface.

Another problem related to Visual Basic makes things even worse. You can't configure Visual Basic to use a user-defined interface as the default interface behind a MultiUse class. This is true of user-defined interfaces defined in PublicNotCreatable class modules as well as interfaces defined in IDL.

The only way you can define the default interface for a MulitUse class is by adding public methods to the class module itself. For example, let's say you've defined a dual interface named IDog in IDL that looks like this:

 Interface IDog : IDispatch {     HRESULT Bark();     HRESULT RollOver([in, out] short* Rolls); }; 

Once you compile your IDL into a type library, you can register the type library on your development workstation. (You'll see how to do this in the next chapter.) Once you register the type library, you can reference it in an ActiveX DLL project and implement your interface in a MultiUse class named CDingo, like this:

 Option Explicit Implements IDog Private Sub IDog_Bark()     ' Your implementation End Sub Private Sub IDog_RollOver(ByRef Rolls As Integer)     ' Your implementation End Sub 

Even though IDog is the only interface you want your class to support, it's not marked as the default interface. In fact, there's nothing you can do in Visual Basic to make IDog the default interface. If you reverse-engineer the type library that Visual Basic builds into the DLL, you can see the following definitions, which illustrate the problem.

 interface _CDingo : IDispatch {     // Empty because there are no public methods in CDingo }; coclass CDingo {     [default] interface _CDingo;     interface IDog; }; 

Visual Basic always defines a dual interface from the public members of a MultiUse class and marks it as the default—even when the class doesn't have any public methods. Any user-defined interface that you implement using the Implements keyword is always a secondary, nondefault interface. This means that a scripting client can't navigate to the methods in your interface.

What are the implications of not being able to navigate to a secondary interface? It means when you create components with Visual Basic, your scripting clients can get to only the public methods in your creatable classes. It means you can't define your interfaces separately from the classes that hold your implementations. It also means that you can't really benefit from the principles of interface-based programming when you deal with scripting clients.

So, after all this hard work in the design and coding phase, you end up with a sticky problem: Clients that can call QueryInterface can access the user-defined interfaces behind your components, but scripting clients can't. How do you deal with this situation? Let's start by discussing what not to do.

Hacking away at the problem

The first technique we'll look at is for programmers who love a challenge. Scripting clients can't navigate to a secondary interface on their own. However, you can give them a little help. Look at the following code in a MultiUse class and notice the new method GetIDog:

 Option Explicit Implements IDog Private Sub IDog_Bark()     ' Your implementation End Sub Private Sub IDog_RollOver(ByRef Rolls As Integer)     ' Your implementation End Sub ' Entry point for a scripting client Public Function GetIDog() As IDog     Set GetIDog = Me End Function 

GetIDog is a public method in the default interface and is therefore accessible to a scripting client. This method has a return type of IDog, which results in an implicit call to QueryInterface. By calling GetIDog, a scripting client can navigate from the default interface to a secondary interface. Here's an example of some VBScript code that connects to IDog and calls the Bark method:

 Dim ProgID, Ref1, Ref2 ProgID = "DogServer.CDingo" Set Ref1 = Server.CreateObject(ProgID) Set Ref2 = Ref1.GetIDog() Ref2.Bark Ref2.RollOver 3 Set Ref1 = Nothing Set Ref2 = Nothing 

As you can see, this technique allows a scripting client to access a user-defined interface such as IDog. Note that you should define IDog as a dual interface that derives from IDispatch as opposed to a custom interface that derives from IUnknown. A dual interface allows a scripting client to use late binding when calling methods such as Bark and RollOver.

On the surface, this approach seems to provide a nice solution. It allows you to design an application with scripting clients that benefits from the principles of interface-based programming. However, this approach has a significant problem: It doesn't work when the client and the object run in different processes. In fact, it doesn't even work when the client and object run on different threads inside the same process.

The problem with this approach is that it breaks COM's remoting layer. Every connection between a client and an object is based on a specific IID. For instance, there should be one proxy/stub pair for the default interface behind CDingo and another proxy/stub pair for IDog. However, in some situations the second required proxy/stub pair for IDog never gets created.

When a scripting client calls the GetIDog method on a remote object, things don't work correctly because COM's remoting layer attempts an optimization. It sees that there's already one IDispatch connection, so it doesn't attempt to create a second IDispatch connection. Even though each dual interface has a different IID, the typeless scripting client always asks for IDispatch. As a result, a call to GetIDog returns a redundant reference to the default interface. The scripting client doesn't navigate from one interface to another in the intended manner. When the scripting client attempts to call Bark, the call fails because the default interface behind CDingo doesn't support Bark.

So maybe this technique isn't so great after all. It works when the client and the object don't need a proxy/stub layer between them, but it breaks in all other cases. When will this get you in trouble? Let's say that you create a component and an ASP script client that use this technique. You install the component in a COM+ library application on the same machine as the ASP code. Things will work just fine at first, but what happens if the system administrator decides to reconfigure your component to run in a COM+ server application for reasons related to security or fault tolerance? Your code won't work anymore because you created a dependency that broke when the component was reconfigured. This type of dependency is something you probably want to avoid.

Don't implement IDispatch more than once

Scripting clients know about only one interface, IDispatch. When you implement two dual interfaces in one component, you create an ambiguity because you implement IDispatch more than once. As you've seen, this ambiguity can be problematic. From a design perspective, a component should expose only one IDispatch implementation. COM's designers made this assumption when they designed their remoting layer.

Recall that Visual Basic components always expose the default interface as a dual interface. This is the interface that's built from the class's public methods. This means that a Visual Basic MultiUse class already has an IDispatch implementation before you implement any secondary interfaces. All secondary, user-defined interfaces you implement should derive from IUnknown instead of IDispatch. Following this rule will ensure that your components don't provide multiple implementations of IDispatch. (You'll learn how to define such an interface using IDL in Chapter 5.)

So, we still haven't solved the problem. If you have a component that implements user-defined interfaces, how do you access those interfaces from a scripting client? The solution is to flatten out all the user-defined interfaces into a single IDispatch-style interface. This interface will be a superset of all methods in all other interfaces. You implement it by using public methods in a MultiUse class.

Let's extend our example. Assume that you have a MultiUse class that implements two different user-defined interfaces, IDog and IWonderDog. Also assume that these two interfaces derive from IUnknown instead of IDispatch. Look at the MultiUse class definition in Listing 4-1. The public methods in the MultiUse class module serve as an entry point for a scripting client. Each method of each interface has a corresponding public method that can be called by a scripting client. The public method simply forwards the call to the interface method. Although adding scripting client support like this can be somewhat tedious, it does solve our problem. A scripting client can call any method that the component implements.

Listing 4-1 You can add public methods to a MultiUse class to map calls made by scripting clients to the private methods of user-defined interfaces.

 Implements IDog Implements IWonderDog Private Sub IDog_Bark()     ' Your implementation End Sub Private Sub IDog_RollOver(ByRef Rolls As Integer)     ' Your implementation End Sub Private Sub IWonderDog_FetchSlippers()     ' Your implementation End Sub ' Scripting support added to default interface Public Sub Bark()     Call IDog_Bark ' Forward call. End Sub Public Sub RollOver(ByRef Rolls As Variant)     Dim iRolls As Integer     iRolls = Rolls     Call IDog_RollOver(iRolls) ' Forward call.     Rolls = iRolls End Sub Public Sub FetchSlippers()     Call IWonderDog_FetchSlippers ' Forward call. End Sub 

Creating wrapper components for scripting clients

While the approach just described gets the job done, at times you might need another technique that doesn't require modifying the original component—such as when you're dealing with a component that implements user-defined interfaces and you can't modify its source code. Instead of adding public methods to the original component, you can create a complementary wrapper component that provides access for scripting clients. This wrapper component is an adapter that plays the role of an "impedance matcher" between a scripting engine and a component that's not script-friendly.

A wrapper component typically creates an object from the original component and maps its public methods to methods in all the user-defined interfaces. Look at the code for the CDingo component defined in Listing 4-2. A scripting client can't access it because it exposes all its functionality through user-defined interfaces.

Listing 4-3 shows the code for a wrapper component CDingoWrapper. This wrapper component uses the Class_Initialize event handler to create a CDingo object. During its initialization, the wrapper component acquires a separate connection for each user-defined interface. The wrapper component uses these connections to forward public method calls to interface method implementations.

Once you flatten out all the user-defined interfaces into one big default interface, a scripting client can access every method. The scripting client simply instantiates an object using the wrapper component and calls methods directly. Here's an example of some VBScript code in an ASP page.

 Dim ProgID, Dog ProgID = "DogServer.CDingoWrapper" Set Dog = Server.CreateObject(ProgID) Dog.Bark Dog.RollOver 3 Dog.FetchSlippers Set Dog = Nothing 

When you define and implement the methods in the wrapper component, you must forward parameters passed from the scripting client to the component you're wrapping. In many cases, you can forward the parameters without any casting or conversion. In some cases, however, you might need to cast the parameters to types that are compatible with whatever scripting clients you're using.

Note that you're responsible for modifying your wrapper component whenever anyone extends the original component to support another interface. You should also realize (and be thankful) that your versioning concerns aren't as complicated as they could be because the wrapper component supports only scripting clients. We'll get into all the details associated with component versioning in the next chapter.

Listing 4-2 This class definition produces a component that scripting clients can't access directly.

 ' MultiUse class CDingo  Implements IDog Implements IWonderDog Private Sub IDog_Bark()     ' Your implementation End Sub Private Sub IDog_RollOver(ByRef Rolls As Integer)     ' Your implementation End Sub Private Sub IWonderDog_FetchSlippers()     ' Your implementation End Sub 

Listing 4-3 This wrapper class allows scripting clients to access the interfaces implemented by the CDingo class.

 ' MultiUse class CDingoWrapper ' CDingo wrapper component for scripting clients Private Ref1 As IDog Private Ref2 As IWonderDog Private Sub Class_Initialize()     Set Ref1 = New CDingo    ' Create object and     Set Ref2 = Ref1          ' establish connections. End Sub Public Sub Bark()     Ref1.Bark ' Forward call. End Sub Public Sub RollOver(ByRef Rolls As Variant)     Dim iRolls As Integer     iRolls = Rolls     Ref1.RollOver(iRolls) ' Forward call.     Rolls = iRolls End Sub Public Sub FetchSlippers()    Ref2.FetchSlippers ' Forward call. End Sub Private Sub Class_Terminate()     Set Ref1 = Nothing     Set Ref2 = Nothing End Sub 

Observations About Scripting Clients

So, scripting clients take something as elegant as interface-based programming and make it less than elegant. When you decide to create a component with user-defined interfaces, you should assume that the component will be accessed exclusively by clients that can call QueryInterface. Such a component won't support scripting clients directly.

The good news is that you can still program in terms of abstract and concrete classes when scripting client support isn't required. For instance, you can create a set of user-defined interfaces that define the manner in which your business logic components interact with the components that contain your data access code. This approach works great as long you can assume that all your business components will be created with Visual Basic or C++.

If you're creating a component that will support scripting clients, you're usually better off avoiding user-defined interfaces. Simply create your components using public methods in MultiUse classes. Scripting clients can call any method directly.

In an imperfect world, you can't always plan your fate so carefully. For instance, if you're creating a Web site in which an ASP client must access a vendor-provided component that implements user-defined interfaces, you have to do a little extra work. One of the best approaches is to create a wrapper component like the one shown in this chapter. Its purpose is to flatten out all the user-defined interfaces into a single interface that is accessible to scripting clients. Your scripting clients can thus get up and running using the path of least resistance.



Programming Distributed Applications with COM+ and Microsoft Visual Basic 6.0
Programming Distributed Applications with Com and Microsoft Visual Basic 6.0 (Programming/Visual Basic)
ISBN: 1572319615
EAN: 2147483647
Year: 2000
Pages: 70
Authors: Ted Pattison

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net