Recipe 3.24 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 do only two minor steps to make your code visible to COM:
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. 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 explicity 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. Table 3-4 contains a list of
attributes and their descriptions that can be used to control these
things.
Table 3-4. Attributes to control how a COM client views and is able to use your C# code|
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
-
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.
|
InterfaceTypeAttribute
|
Defines how an interface is exposed to COM clients. This attribute
may only be used 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 defaults 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 {...}
|
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
To show how these attributes are applied, we use the
Foo class, defined within the
Chapter_Code namespace:
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, we 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. 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 assembly, the
IFoo interface, and the Foo
class using the GuidAttribute. 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;
[assembly: GuidAttribute("D4E77B72-43C8-45f1-B0C0-D47685EC18C2")]
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 VBScript using COM interop
is shown here:
<script runat=server>
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
</script>
The first Dim statement creates a new instance of
the Foo type that is usable from the VBScript
code. The rest of the VBScript 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 are
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 VBScript 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) to change, as well as the version number
for your assembly, 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. 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.
|