| 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 |
 |
| The Basics |
The simplest form for allowing
interoperability is as follows:
-
Create a class library project in .Net. A default constructor is required.
-
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.
-
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:
-
Adding the System.runtime.Interopservices namespace. This allows the use of the
COM attributes.
-
Explicitly creating the interface. This was done by adding _ to the class name.
-
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.
-
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.
-
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.
-
Set the InterfaceType to None for the class. This prevents the type exporter
from exporting the class metadata.
-
Set the ProgId so the class can be called using CreateObject.
-
Modify the class to implement the interface.
-
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.
|