Using COM Callable Wrappers to Extend
Existing Visual Basic 6.0 Applications

by Jon Wojtowicz

Many large enterprises have a vast investment in applications written in Visual Basic 6.0. These applications can range from line of business to small departmental application. Typically the maintenance on these applications includes adding features to the existing applications. Ideally, these applications should be re-written in .Net. Many of these applications have been in production for several years and are very stable. To re-write the applications would be a considerable investment, an investment these organizations are not willing to make (understandably).


Performing certain operations, such as file IO and accessing web services, are much easier in .Net. To get the productivity benefits of .Net and still utilize the previous investment .Net has features that allow for calling .Net assemblies from COM client using a COM Callable Wrapper. A COM Callable Wrapper (CCW) exposes .Net objects to COM. This allows extension of Com based application without re-writing the entire application.
COM Callable Wrapper
COM Callable Wrapper
The Basics
The simplest form for allowing interoperability is as follows:
  1. Create a class library project in .Net. A default constructor is required.
  2. Open the project properties and go to Configuration Properties -> Build. Set Register for COM Interop to true. This will create a type library and register the assembly for use from COM clients.
  3. Build the assembly. Use the regasm utility to register the assembly when deploying to remote machines.
The assembly is now registered to be called from a COM client. Following is a simple example.
.Net class in an assembly called COMTest.
public class TestClass
{
public TestClass(){}
public string GetAuthenticationType()
{
return System.Security.Principal.WindowsIdentity.GetCurrent().AuthenticationType;
}
}
A test VB 6.0 project is created and a reference is added to the type library created earlier.
Visual Basic 6.0 Test Client
Private Sub Command1_Click()
Dim obj As COMTest.TestClass
Set obj = New COMTest.TestClass
MsgBox obj.GetAuthenticationType()
Set obj = Nothing
End Sub
In order to run the code the .Net assembly will have to be placed in the same folder as the executable. The other option is to provide a strong name to the assembly and place it in the GAC. It is recommended that the assemblies used for COM interoperability be strong named and placed in the GAC.
The default COM registration does not give Intellisense. The reason given is that the IDE uses regasm with the /tlb option to create the type library. By default the regasm utility creates a type library that exposes a non-dual, empty IDispach interface. This prevents early binding and Intellisense. This is a serious drawback to using the automated method.
The Visual Basic project with the reference will need to closed or the reference removed prior to replacing the type library (it locks the file and prevents replacement).
Controlling the COM Callable Wrapper– Do It Yourself
While the default behavior can allow for quick interop it has some serious deficiencies. This includes the lack of Intellisense and object browsing. To overcome these limitations control of the CCW is required. This means writing the interfaces as well as the interop classes. This requires using the attributes in the System.Runtime.InteropServices namespace.
While it is possible to use the InterfaceTypeAttribute to overcome the limitations of the automated type library generation it is not recommended. If the InterfaceType is set to InterfaceIsDual then any changes in the ordering of the methods will cause the COM client to break. Using an explicit interface gives the most control and flexibility. By specifying the DispId the ordering of the methods is no longer an issue. Specifying an IDispatch interface allows for late binding and Intellisense.
The re-written code is as follows.
[Guid("942e01d9-821f-4908-83d9-6e456dc9a3f0")]
[InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]
public interface _TestClass
{
[DispId(1)]
string GetAuthenticationType();
}

[Guid("794b4f11-1a70-42e6-ad4d-c627cc2fc89b")]
[ClassInterface(ClassInterfaceType.None)]
[ProgId("COMTest.TestClass")]
public class TestClass : _TestClass
{
public TestClass(){}
public string GetAuthenticationType()
{
return System.Security.Principal.WindowsIdentity.GetCurrent().AuthenticationType;
}
}
The steps taken for this example are:
  1. Adding the System.runtime.Interopservices namespace. This allows the use of the COM attributes.
  2. Explicitly creating the interface. This was done by adding _ to the class name.
  3. Creating the GUIDs for the interface and class. This allows changes to be made to the assembly and the classes without breaking client code. The developer must determine when the changes made will break client code. If it will then new GUIDs should be used. This is the same effect as binary compatibility in VB 6.0. GUIDs can be created by selecting Create GUID from the Tools menu. Select the registry format for attaching to the classes.
  4. Specify the interface to be InterfaceIsIDispatch by using the InterfaceType attribute. This only allows late binding and helps insulate the COM client from changes within the .Net assembly.
  5. In the interface place the properties and methods that will be exposed. Attach arbitrary DispIds to prevent changes from breaking the client code. This allows additions to the interface without effecting the client code as long as the previous DispIds are not changed.
  6. Set the InterfaceType to None for the class. This prevents the type exporter from exporting the class metadata.
  7. Set the ProgId so the class can be called using CreateObject.
  8. Modify the class to implement the interface.
  9. Build the class.
While this does take more time the benefit of having Intellisense and the .Net classes displayed in the VB 6.0 Object Browser make the effort worthwhile.
A Gotcha
One item I noticed in working with the CCW is the behavior with arrays. For demonstration a Concat method will be added that takes a string array.
[DispId(2)]
string Concat(string[] strArray);
Rebuilding the project and making a call from the VB 6.0 client using the following.
Private Sub Command1_Click()
Dim obj As COMTest.TestClass
Set obj = New COMTest.TestClass
Dim strArray(4) As String
strArray(0) = "Hello "
strArray(2) = "from "
strArray(3) = "COM "
strArray(4) = "Interop."
MsgBox obj.Concat(strArray)
Set obj = Nothing
End Sub
Trying to compile this project will generate the following error.
Function or interface marked as restricted, or the function uses an Automation type not supported in Visual Basic.
This is a confusing error that is caused by the marshaller not being able to handle the array. To work around this issue the array must be passed by ref. By adding the ref keyword to the method we can alleviate the issue. The following modified code compiles and runs successfully.
[DispId(2)]
string Concat(ref string[] strArray);
Summary
Using a CCW can help extend life into existing Visual Basic 6.0 applications. This can save considerable time, effort and cost in upgrading applications. When creating CCWs I would recommend the following:
  • Do explicitly create the interface. This provides the greatest control and robustness of the component. Using the _ as the interface prefix follows the pattern used by VB 6.0 for COM object creation.
  • Do explicitly set the GUID on the class. This will allows for control of the versioning as seen by COM.
  • Do mark classes that you do not want to visible to COM using the ClassInterfaceType.None. This is cleaner than using the ComVisible(false) attribute. Classes are visible to COM by default.
  • Do pass arrays by ref. This will prevent cryptic marshalling errors.
  • Do register the assemblies in the GAC. This allows for more flexibility than placing the assembly in the same directory as the exe. It can save future headaches when upgrading assemblies shared by multiple applications.
  • Do not expose complex types through COM, keeping the types exposed as simple as possible prevents having to deal with marshalling issues. Custom types can be exposed but keep the contained data simple.
  • Do use the full power of .Net to extend the functionality of VB 6.0 applications.
Jon Wojtowicz is a C# MVP and a Systems Analyst at a large insurance company in Chattanooga, TN where he currently provides developer support and internal training. He has worked as a consultant working with Microsoft Technologies. This includes ASP, COM, VB6 and .Net, both C# and VB.Net since Beta 1. He has been an MCSD since 1999 and an MCT since 2000. Prior to getting a degree in Computer science he worked as a process engineer focusing on process automation, programmable controllers and equipment installations. In his spare time he likes woodworking and gardening.
Article Discussion: