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 |
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; } }; |
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:
Private Sub Command1_Click() Dim myRef As New Component.InsideCOM MsgBox "myRef.Sum(5, 6) returns " & myRef.Sum(5, 6) End Sub |
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:
Interface | Description |
---|---|
IUnknown | The Visual Basic Virtual Machine (VM) handles QueryInterface and reference counting for all COM+ objects. See Chapter 2. |
IDispatch | Provides Automation support. See Chapter 5. |
IProvideClassInfo | Provides a single method of accessing the type information for an object's coclass entry in its type library. See Chapter 9. |
ISupportErrorInfo | Ensures that error information can be correctly propagated up the call chain. See Chapter 6. |
IConnectionPoint and IConnectionPointContainer | Supports connection points for connectable objects. See Chapter 8. |
IExternalConnection | Manages 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.
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:
Private Function InsideCOM_Sum(ByVal x As Long, _ ByVal y As Long) As Long InsideCOM_Sum = x + y End Function |
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.