Retrofitting a Class to Interoperate with COM






Retrofitting a Class to Interoperate with COM

problem

An existing C# class needs to be usable by a COM object or will need to be usable sometime in the future. You need to make your class work seamlessly with COM.

Solution

Microsoft has made COM interop quite easy. In fact, you really have to complete only two minor steps to make your code visible to COM:

  1. Set the Register for COM interop field in the project properties to true. This produces a type library that can be used by a COM client.

  2. Use the Regasm.exe command-line tool to register the class. For example, to register the type library for the ClassLibrary1.dll, you would do the following:

    	regasm ClassLibrary1.dll /tlb:ClassLibrary1.tlb
    

By default, this tool will make many decisions for you. For example, new GUIDs are created for your classes and interfaces unless you specify a particular GUID to use. This can be a bad thing; it is usually a good idea to explicitly specify which GUIDs your classes and interfaces are to use. To take control of how your C# code is viewed and used from a COM client, you need to use a few attributes. Figure contains a list of attributes and their descriptions that can be used to control these things.

Attributes to control how a COM client views and uses your C# code

Attribute name

Description

GuidAttribute

Places a GUID on an assembly, class, struct, interface, enum, or delegate. Prevents the Tlbimp (the type library converter tool, which converts a COM type library into the equivalent metadata) from creating a new GUID for this target.

ClassInterfaceAttribute

Defines the class interface type that will be applied to an assembly or class. Valid interface types are:


AutoDispatch

An IDispatch interface will be exposed for this type. The interface will support only late binding. This is the default.


AutoDual

The interface will support both early and late binding.


None

An interface will not be explicitly provided. Therefore, only late-bound access is allowed through an IDispatch interface. Note that this is when other explicit interfaces are not defined.

InterfaceTypeAttribute

Defines how an interface is exposed to COM clients. This attribute may be used only on interfaces. Valid interface types are:


InterfaceIsDual

The interface will be exposed as a dual interface.


InterfaceIsIDispatch

The interface will be exposed as a dispinterface.


InterfaceIsIUnknown

The interface will be exposed as deriving from IUnknown.

If this attribute is not used, the interface will default to being exposed as a dual interface.

ProgIdAttribute

Force the ProgId of a class to a defined string. An automatically generated ProgId consists of the namespace and type name. If your ProgId may exceed 39 characters (i.e., your namespace is equal to or greater than 39 characters), you should use this attribute to manually set a ProgId that is 39 characters or less. By default the ProgId is generated from the full namespace and type name (e.g., Namespace1.Namespace2. TypeName).

ComVisibleAttribute

Allows fine-grained control over which C# code is visible to a COM client. To limit the exposed types, set the ComVisibleAttribute to false at the assembly level:

	[assembly: ComVisibleAttribute(false)]

and then set each type and/or member's visibility individually using the following syntax:

	[ComVisibleAttribute(true)]
	public class Foo {…}

The ComVisibleAttribute was not specified by default in projects created in Visual Studio 2003, but in Visual Studio 2005, the attribute is specified as false in the AssemblyInfo.cs file. This hides all types in the assembly from COM by default.


These attributes are used in conjunction with the previous two steps mentioned to create and register the assembly's classes. Several other COM interop attributes exist in the FCL, but the ones mentioned here provide the most basic control over how your assembly is viewed and used by COM clients.

Discussion

The Foo class, defined within the Chapter_Code namespace, shows how these attributes are applied:

	using System;

	namespace Chapter_Code
	{
	    public class Foo
	    {
	         public Foo( ) {}

	         private int state = 100;
	         public string PrintMe( )
	         {
	              return("TEST SUCCESS");
	         }

	         public int ShowState( )
	         {
	              return (state);
	         }

	         public void SetState(int newState)
	         {
	              state = newState;
	         }
	    }
	}

To allow the Foo type to be exposed to a COM client, you would first add an interface, IFoo, describing the members of Foo that are to be exposed. Adding an interface in this manner is optional, especially if you are exposing classes to scripting clients. However, it is recommended since COM is interface-based and you will be able to explicitly control how it is exported. If the AutoDual interface type is used with the ClassInterfaceAttribute, early-bound clients will not need this interface either. Even though it is optional, it is still a good idea to use an interface in this manner.

Next, an unchanging GUID is added to the IFoo interface and the Foo class using the GuidAttribute. The assembly.cs file contains a Guid attribute attached to the assembly with a new GUID. A ProgId is also added to the Foo class. Finally, the class interface type is defined as an AutoDispatch interface, using the ClassInterfaceAttribute. The new code is shown here with the changes highlighted:

	using System;
	using System.Runtime.InteropServices;

	namespace Chapter_Code
	{
	     [GuidAttribute("1C6CD700-A37B-4295-9CC9-D7392FDD425D")]
	     public interface IFoo
	     {
	          string PrintMe( );
	          int ShowState( );
	          void SetState(int newState);
	     }

	     [GuidAttribute("C09E2DD6-03EE-4fef-BB84-05D3422DD3D9")]
	     [ClassInterfaceAttribute(ClassInterfaceType.AutoDispatch)]
	     [ProgIdAttribute("Chapter_Code.Foo")]
	     public class Foo : IFoo
	     {
	          public Foo( ) {}
	          private int state = 100;

	          public string PrintMe( )
	          {
	               return("TEST SUCCESS");
	          }

	          public int ShowState( )
	          {
	               return (state);
	          }

	          public void SetState(int newState)
	          {
	               state = newState;
	          }
	     }
	}

The code to use the exposed C# code from VB6 code using COM interop is shown here:

	Sub TestCOMInterop( )
	    'ClassLibrary1 was created using Regasm in the Solution section
	    'of this recipe
	    Dim x As New ClassLibrary1.Foo

	    MsgBox ("Current State: " & x.ShowState( ))
	    x.SetState (-1)
	    MsgBox ("Current State: " & x.ShowState( ))
	    MsgBox ("Print String: " & x.PrintMe( ))
	End Sub

The first Dim statement creates a new instance of the Foo type that is usable from the VB6 code. The rest of the VB6 code exercises the exposed members of the Foo type.

There are some things to keep in mind when exposing C# types to COM clients:

  • Only public members or explicit interface member implementations can be exposed to COM clients. Explicit interface member implementations are not public, but if the interface itself is public, it may be seen by a COM client.

  • Constant fields are not exposed to COM clients.

  • You must provide a default constructor in your exposed C# type.

  • Parameterized constructors are not exposed to COM clients.

  • Static members are not exposed to COM clients.

  • Interop flattens the inheritance hierarchy so that your exposed type and its base class members are all available to the COM client. For example, the methods ToString( ) and GetHashCode( ), defined in the base Object class, are also available to VB6 code:

    	Sub TestCOMInterop( )
    	    Dim x As New ClassLibrary1.Foo
    	    MsgBox (x.ToString( ))
    	    MsgBox (x.GetHashCode( ))
    	End Sub
    

  • It is a good idea to explicitly state the GUIDs for any types exposed to COM clients, including any exposed interfaces, through the use of the GuidAttribute. This prevents Tlbexp/Regasm from creating new GUIDs every time your interface changes. A new GUID is created by the Regasm tool every time you choose the Build Rebuild Solution or Build Rebuild ProjectName menu item. These actions cause the date/time of the module (dll or exe), as well as the version number for your assembly, to change which, in turn, can cause a different GUID to be calculated. A new GUID will be calculated for a rebuilt assembly even if no code changes within that assembly. Not explicitly adding a GUID to your exposed types will cause your registry to greatly expand during the development stage as more new GUIDs are added to it.

  • It is also a good idea to limit the visibility of your types/members through judicial use of the ComVisibleAttribute. This can prevent unauthorized use of specific types/members that could possibly corrupt data or be used to create a security hole by malicious code.

  • Exposed types should implement an interface (for example, IFoo) that allows you to specify exactly what members of that type are exposed to COM. If such an explicit interface is not implemented, the compiler will default to exposing what it can of the type.

See Also

See the "Assembly Registration Tool (Regasm.exe)," "Type Library Exporter (Tlbexp.exe)," "Type Library Importer (Tlbimp.exe)," and "Assembly to Type Library Conversion Summary" topics in the MSDN documentation.



 Python   SQL   Java   php   Perl 
 game development   web development   internet   *nix   graphics   hardware 
 telecommunications   C++ 
 Flash   Active Directory   Windows