[Previous] [Contents] [Next]

COM+ Programming in Visual Basic

You can develop COM+ components quite successfully in Visual Basic. Visual Basic is perhaps best at creating the more complex varieties of COM+ components, namely ActiveX controls and ActiveX documents. Developing such components in raw C++ can be quite an undertaking. Visual Basic is also surprisingly flexible and easy to use when it comes to providing the fundamental aspects of COM+ programming.

Visual Basic automatically calls CoInitializeEx and CoUninitialize—without any intervention from the programmer. The New keyword in Visual Basic replaces CoCreateInstance(Ex) as the object instantiator. Visual Basic's automatic garbage collection algorithm calls IUnknown::Release on object references that go out of scope. You can also explicitly call Release by setting an object reference to Nothing, as shown here:

Set myRef = Nothing

While you can use the New keyword to instantiate coclasses in Visual Basic, its use is somewhat limited. First, if you use New to instantiate classes defined within the active project, Visual Basic does not use CoCreateInstance. Instead, it uses an internal object creation mechanism that does not involve COM+. Therefore, you should not instantiate such classes using the New keyword. Also, New instantiates only COM+ objects running on the local machine, unless specified otherwise by the registry settings.

To overcome these limitations, Visual Basic offers the CreateObject function as an alternative to the New keyword. This function always uses COM+ to instantiate the object. The first parameter of CreateObject specifies the name of the coclass to be instantiated. (Visual Basic retrieves the object's CLSID from the registry by passing its name to the CLSIDFromProgID function.) In addition, the CreateObject function takes an optional second argument called servername, which dynamically specifies the name of a remote machine where the object should be instantiated. You can set the servername parameter to the computer name of the remote machine or the IP (Internet Protocol) address, such as 199.34.57.30. When you specify a servername parameter, Visual Basic calls the CoCreateInstanceEx function to instantiate the object. (The CoCreateInstanceEx function is discussed further in Chapter 13.) A sample use of the CreateObject function is shown in boldface below:

Private Sub Form_Click()
    Dim myRef As Object

    ' Create object based on a ProgID.
    Set myRef = CreateObject("TestATL.InsideCOM", "MyServer")

    MsgBox myRef.Sum(5, 3)
End Sub

QueryInterface: The Visual Basic Way

Although a COM+ class can support multiple interfaces, Visual Basic tries to simplify things by assuming that the programmer wants access to the default interface; it therefore automatically calls QueryInterface to request that interface. This simplification makes sense, since most classes have a primary interface through which most operations are performed. Unfortunately, Visual Basic also hides the name of the default interface! If a default interface named IMyDefault is implemented by a COM+ class named MyClass, Visual Basic uses the name MyClass as an alias for the IMyDefault interface. This technique seems to work acceptably when you access a COM+ object from Visual Basic, but it can be rather confusing when you are trying to implement a COM+ interface in Visual Basic.

Using the Implements keyword, a Visual Basic class module can implement any COM+ interface described in a type library. When the desired interface happens to be the default, the code that implements the COM+ class instead of the desired interface can look mighty strange. If you need access to the other (nondefault) interfaces supported by an object, you can execute a QueryInterface using the Set statement. If needed, the Set statement calls the IUnknown::QueryInterface method to cast the rvalue interface pointer to the type of the lvalue reference. For example, the following code snippet shows two references—MyRef1 and MyRef2. MyRef1 is declared as an IUnknown interface pointer; MyRef2 is a pointer to the IMyInterface interface. When MyRef2 is set to MyRef1, a QueryInterface call that requests the IMyInterface interface pointer from the object pointed to by MyRef1 takes place.

Dim MyRef1 As IUnknown
Set MyRef1 = New MyObject

Dim MyRef2 As IMyInterface
Set MyRef2 = MyRef1 ' QueryInterface executed!

In the preceding code, MyRef1 is declared As IUnknown. This declaration might look odd to a Visual Basic programmer, but it is actually much more efficient than declaring references As Object. As Object actually uses the IDispatch (Automation) interface, which is much less efficient than a normal v-table-based interface.

In an IDL file, you indicate the default interface using the [default] attribute, shown below in boldface. If no interface in a coclass statement is declared as the default, the first interface listed is treated as the default. Here is an IDL file containing multiple interfaces that can be compiled into a type library and implemented in Visual Basic:

import "unknwn.idl";

[ object, uuid(00000000-0000-0000-0000-000000000001) ]
interface IDefaultInterface : IUnknown
{
    HRESULT ThingOne();
}

[ object, uuid(00000000-0000-0000-0000-000000000002) ]
interface ISecondaryInterface : IUnknown
{
    HRESULT ThingTwo();
}

[ uuid(00000000-0000-0000-0000-000000000003) ]
library Component
{
    importlib("stdole32.tlb");
    interface IDefaultInterface;
    interface ISecondaryInterface;

    [ uuid(00000000-0000-0000-0000-000000000004) ]
    coclass TheClass
    {
        [default] interface IDefaultInterface;
        interface ISecondaryInterface;
    }
};

The following Visual Basic code uses the previously defined type library, accompanied by C++-style pseudocode comments that explain what the code does in COM+ terms. Notice that the IDefaultInterface interface is hidden from the Visual Basic programmer and is instead referred to by the coclass name as TheClass.

Private Sub Command1_Click()
    ' IDefaultInterface* myRef1;
    ' CoCreateInstance(CLSID_TheClass, NULL, 
    '     CLSCTX_INPROC_SERVER, IID_IDefaultInterface, 
    '     (void**)&myRef1);
    Dim myRef1 As New Component.TheClass

    ' ISecondaryInterface* myRef2;
    Dim myRef2 As Component.ISecondaryInterface

    ' myRef1->ThingOne();
    myRef1.ThingOne

    ' myRef1->QueryInterface(IID_ISecondaryInterface, 
    '     (void**)&myRef2);
    Set myRef2 = myRef1

    ' myRef2->ThingTwo();
    myRef2.ThingTwo

    ' myRef1->Release();
    Set myRef1 = Nothing

    ' Garbage collector calls myRef2->Release();
End Sub

One solution to the hidden default interface problem in Visual Basic is to make IUnknown the default interface of a coclass in the IDL file, as shown in boldface in the following code. This technique tricks Visual Basic into showing all interfaces of a coclass; when Visual Basic thinks it is hiding the default interface from you, it is really hiding only IUnknown!

[ uuid(00000000-0000-0000-0000-000000000003) ]
library Component
{
    importlib("stdole32.tlb");
    interface IDefaultInterface;
    interface ISecondaryInterface;

    [ uuid(00000000-0000-0000-0000-000000000004) ]
    coclass TheClass
    {
        [default] IUnknown;    // IUnknown will be hidden.
        interface IDefaultInterface;
        interface ISecondaryInterface;
    }
};

Building a Client in Visual Basic

Earlier in the chapter, we built a COM+ component in C++ that contains a type library. Because the type library enables language integration in COM+, you can easily use this component from most other programming environments. Here are the steps required to build a client in Visual Basic:

  1. Open Visual Basic.
  2. Select the Standard EXE project, and click OK.
  3. Choose Project/References, and then select Inside COM+ Component Type Library.
  4. Click OK.
  5. Place a CommandButton control on Form1.
  6. Choose View/Code.
  7. From the Object list box, select Command1.
  8. Enter the following code shown in boldface:
  9. Private Sub Command1_Click()
        Dim myRef As New Component.InsideCOM
        MsgBox "myRef.Sum(5, 6) returns " & myRef.Sum(5, 6)
    End Sub
    

  10. Choose Run/Start.
  11. Test the component by clicking the Command1 button.

Implementing COM+ Interfaces in Visual Basic

In addition to creating client applications in Visual Basic, you can also use Visual Basic to build COM+ components. Visual Basic can produce in-process or executable components as well as components that run in the COM+ run-time environment. By default, the following interfaces are implemented automatically by in-process components built in Visual Basic:

InterfaceDescription
IUnknownThe Visual Basic Virtual Machine (VM) handles QueryInterface and reference counting for all COM+ objects. See Chapter 2.
IDispatchProvides Automation support. See Chapter 5.
IProvideClassInfoProvides a single method of accessing the type information for an object's coclass entry in its type library. See Chapter 9.
ISupportErrorInfoEnsures that error information can be correctly propagated up the call chain. See Chapter 6.
IConnectionPoint and IConnectionPointContainerSupports connection points for connectable objects. See Chapter 8.
IExternalConnectionManages an object's count of marshaled (external) references, enabling the object to detect when it has no external connections so it can shut down properly. See Chapter 13.

For each coclass, the Visual Basic Virtual Machine (msvbvm60.dll) automatically provides a class object that implements the IClassFactory interface so you can create instances of the coclass using standard mechanisms such as CoCreateInstance(Ex). Unfortunately, you cannot aggregate objects built in Visual Basic, nor can you aggregate another object from Visual Basic. For in-process components, Visual Basic exports the standard COM+ functions: DllGetClassObject, DllCanUnloadNow, DllRegisterServer, and DllUnregisterServer. The implementations of DllRegisterServer and DllUnregisterServer automatically provide all the code needed for the component to support self-registration. The project name supplied in the Project Properties dialog box in Visual Basic is combined with the Name property of a class module to produce the program identifier (ProgID). For example, a project named Zoo containing a class named Dog would be registered with a ProgID of Zoo.Dog.

To implement a COM+ interface in Visual Basic, you do not need interface definitions written in IDL because Visual Basic deduces the interface definition and creates the necessary type information automatically. If needed, however, you can use Visual Basic to implement a specific interface described in a type library. This technique is useful when you want to create a component in Visual Basic that will interoperate with a client that demands support for a particular custom interface with a well-known IID.

While each class module in Visual Basic defines only one class, a single class can implement multiple interfaces. The public properties, methods, and events of the class module define the default interface of the class. You can use the Implements keyword to implement additional interfaces in a class module. For example, a Math class might implement several different interfaces, as shown here:

Implements IArithmetic
Implements IGeometry

Private Function IArithmetic_Add(x As Integer, _
    y As Integer) As Integer
    ' Code here...
End Function
Private Function IArithmetic_Subtract(x As Integer, _ 
    y As Integer) As Integer
    ' Code here...
End Function

Private Function IGeometry_TriangleArea(base As Integer, _ 
    height As Integer) As Integer
    ' Code here...
End Function

A client program written in C++ would navigate among these interfaces using the IUnknown::QueryInterface method; in contrast, a Visual Basic client would use the Set keyword to perform the typecast, as described earlier in the section titled "QueryInterface: The Visual Basic Way." Notice that the name of each class module becomes the name of the coclass, but the name of the default interface defined by the class module is the class name prefixed with an underscore. For example, a Visual Basic class module named Math becomes a coclass named Math with a default interface named _Math.

Building a Component in Visual Basic

Earlier, we built a simple C++ client that uses a type library via the #import statement to invoke a component that supports the ISum interface. Now let's use Visual Basic to create a component that implements the ISum interface and is called by the C++ client. To do so, follow these steps:

  1. Open Visual Basic.
  2. Select the ActiveX DLL project, and click OK.
  3. Choose Project/References, and select Inside COM+ Component Type Library.
  4. Click OK.
  5. In the Code window, type Implements InsideCOM and press Enter.
  6. From the Object list box, select InsideCOM.
  7. In the code window, enter the following code shown in boldface:
  8. Private Function InsideCOM_Sum(ByVal x As Long, _
        ByVal y As Long) As Long
        InsideCOM_Sum = x + y
    End Function
    

  9. In the Properties window, set the Name property to VBInsideCOM.
  10. Choose Project/Project1 Properties.
  11. In the Project Name text box, type VBComponent.
  12. In the Project Description text box, type The VBInsideCOM Component.
  13. Select Apartment Threaded in the Threading Model section. This marks your component as ThreadingModel=Apartment in the registry. (For more on apartment threading, see Chapter 4.)
  14. Click OK to close the Project Properties dialog box.
  15. Choose File/Make VBComponent.dll, specify a folder for the DLL, and then click OK.
  16. Choose File/Save Project, and accept all the default names.
  17. Choose File/Exit.

To test this Visual Basic component from C++, use the easyclient.cpp program in Listing 3-3 as a starting point or use the code shown in Listing 3-5 below. Be sure to include the component.h file that was generated by the MIDL compiler in previous exercises so that the compiler can find the definition of ISum.

vbclient.cpp

#import "VBComponent.dll" no_namespace
#import "component.dll" no_namespace
#include <iostream.h>

void main()
{
    CoInitialize(NULL);
    ISumPtr myRef(__uuidof(VBInsideCOM));
    int result = myRef->Sum(5, 13);
    cout << "5 + 13 = " << result << endl;
    myRef = NULL;
    CoUninitialize();
}

Listing 3-5. A C++ client that uses a component created in Visual Basic.