What's wrong with ComClassAttribute?

Normally I spend most of my time in C#, but every now and again I switch to VB.NET, and sometimes interesting discoveries are made. One of the more recent discoveries was ComClassAttribute in the Microsoft.VisualBasic assembly. Essentially it's an easy way to expose a .NET type to COM. Just attribute up your class with three GUIDs, one for the class, one for the interface, and one for the event interface, then compile and register with COM, and hey presto one COM object.

Here's an example that's pretty contrived, but you get the idea:

Imports System
Imports System.Runtime.InteropServices
Imports Microsoft.VisualBasic

<ComVisible(True), _
ComClass(MyComClass.ClassGuid, MyComClass.InterfaceGuid, MyComClass.EventInterfaceGuid)> _
Public Class MyComClass

    Private _prefix As String = "Hello "

    Public Const ClassGuid As String = "7b636a10-f549-4a48-979d-ea5284960254"
    Public Const InterfaceGuid As String = "6585a923-1a2a-4891-a7b1-8df07089f13c"
    Public Const EventInterfaceGuid As String = "68b36e6f-2c1c-4471-a62a-1e4e3c489306"

    Public Property Prefix() As String
        Get
            Return _prefix
        End Get
        Set(ByVal value As String)
            _prefix = value
        End Set
    End Property

    Public Function DoSomeWork(ByVal input As String) As String
        Dim work As String = _prefix + input
        OnWorkDone(work)
        Return work
    End Function

    Public Event WorkDone(ByVal work As String)

    Protected Overridable Sub OnWorkDone(ByVal work As String)
        RaiseEvent WorkDone(work)
    End Sub

End Class

So what does this end up looking like in the COM world? Whenever I work with .NET exposed as COM I always like to check what the COM type library looks like when you export the types from .NET. This helps ensure that you are getting what you really intend in the COM world. It's pretty easy to do, just use TLBEXP to export a type library from the .NET assembly, and then OleView to view the type library in IDL. The type library for the above VB.NET class looks like this:

[
  uuid(AA9CD477-1A75-49B4-9621-EBE57B1F9584),
  version(1.0),
  custom(90883F05-3D28-11D2-8F17-00A0C9A6186D, "Example.ComInterop, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null")

]
library Example_ComInterop
{
    // TLib :     // TLib : mscorlib.dll : {BED7F4EA-1A96-11D2-8F08-00A0C9A6186D}
    importlib("mscorlib.tlb");
    // TLib : OLE Automation : {00020430-0000-0000-C000-000000000046}
    importlib("stdole2.tlb");

    // Forward declare all types defined in this typelib
    interface _MyComClass;
    dispinterface __MyComClass;

    [
      uuid(7B636A10-F549-4A48-979D-EA5284960254),
      version(1.0),
      custom(0F21F359-AB84-41E8-9A78-36D110E6D2F9, "Example.ComInterop.MyComClass")
    ]
    coclass MyComClass {
        interface _Object;
        [default] interface _MyComClass;
        [default, source] dispinterface __MyComClass;
    };

    [
      odl,
      uuid(6585A923-1A2A-4891-A7B1-8DF07089F13C),
      version(1.0),
      dual,
      oleautomation,
      custom(0F21F359-AB84-41E8-9A78-36D110E6D2F9, "Example.ComInterop.MyComClass+_MyComClass")    

    ]
    interface _MyComClass : IDispatch {
        [id(0x00000001), propget]
        HRESULT Prefix([out, retval] BSTR* pRetVal);
        [id(0x00000001), propput]
        HRESULT Prefix([in] BSTR pRetVal);
        [id(0x00000002)]
        HRESULT DoSomeWork(
                        [in] BSTR input, 
                        [out, retval] BSTR* pRetVal);
    };

    [
      uuid(68B36E6F-2C1C-4471-A62A-1E4E3C489306),
      version(1.0),
      custom(0F21F359-AB84-41E8-9A78-36D110E6D2F9, "Example.ComInterop.MyComClass+__MyComClass")    

    ]
    dispinterface __MyComClass {
        properties:
        methods:
            [id(0x00000001)]
            void WorkDone([in] BSTR work);
    };
};

Now this doesn't look too bad. The ComClassAttribute has created two interfaces for the class, one dual interface for the COM properties and methods, and one dispatch interface for the COM event sources. I'm not hugely pleased with the naming of the interfaces, but it'll do. So what's the point of this post you ask? What is so wrong with ComClassAttribute? Well, consider what you'd have to do if in the next version of this example you need to add an additional integer parameter to the DoSomeWork method. Not an unreasonable change. You could just pop the parameter on the method, compile and register. But this would change the COM definition of the interface too, and anything that was using this interface would also have to be updated. You could just increase the .NET assembly version, and thereby also increase the COM type library version. This time you'd be shouldered with the burden of having to have two versions deployed and maintained to support both the old and new clients of the type library. What you want to be able to do is only deploy the new updated interface, but also support the old interface as well. In the COM world this is usually achieved by introducing a new interface with the new method definition, maintaining the old interface, and have the class implement both interfaces. Unfortunately with ComClassAttribute you can't do this. Basically if you use ComClassAttribute now you're setting yourself up for a maintenance headache at some point in the future.

So what's the alternative? Well you need to be more explicit about the interfaces you want to expose to COM, and use the core COM interop attributes in the System.Runtime.InteropServices namespace. This gives you a huge amount more control over exactly what's exposed to COM, and I'd always favour any approach that gives me more control! Let's update the example to use these attributes. First we need an interface that will be exposed to COM:

Imports System.Runtime.InteropServices

<ComVisible(True), _
InterfaceType(ComInterfaceType.InterfaceIsDual), _
Guid("6585a923-1a2a-4891-a7b1-8df07089f13c")> _
Public Interface IMyComClass

    Property Prefix() As String

    Function DoSomeWork(ByVal input As String) As String

End Interface

Then we need an interface to provide the source for the COM events:

Imports System.Runtime.InteropServices

<ComVisible(True), _
InterfaceType(ComInterfaceType.InterfaceIsIDispatch), _
Guid("68b36e6f-2c1c-4471-a62a-1e4e3c489306")> _
Public Interface IMyComClassEvents

    Sub WorkDone(ByVal work As String)

End Interface

And finally, the class that brings together the two interfaces into a COM object:

Imports System.Runtime.InteropServices

<ComVisible(True), _
ClassInterface(ClassInterfaceType.None), _
ComSourceInterfaces(GetType(IMyComClassEvents)), _
Guid("7b636a10-f549-4a48-979d-ea5284960254")> _
Public Class MyComClass
    Implements IMyComClass

    Private _prefix As String = "Hello "

    Public Property Prefix() As String Implements IMyComClass.Prefix
        Get
            Return _prefix
        End Get
        Set(ByVal value As String)
            _prefix = value
        End Set
    End Property

    Public Function DoSomeWork(ByVal input As String) As String Implements IMyComClass.DoSomeWork
        Dim work As String = _prefix + input
        OnWorkDone(work)
        Return work
    End Function

    Public Event WorkDone(ByVal work As String)

    Protected Overridable Sub OnWorkDone(ByVal work As String)
        RaiseEvent WorkDone(work)
    End Sub

End Class

This results in almost the same type library IDL as we got with ComClassAttribute, albeit with some naming changes. So, now to introduce the new version of the DoSomeWork method that take an additional integer parameter. First we need a new interface for the new method signature:

Imports System.Runtime.InteropServices

<ComVisible(True), _
InterfaceType(ComInterfaceType.InterfaceIsDual), _
Guid("a6452c96-1601-4f40-b9a8-d6eeb9fefb64")> _
Public Interface IMyComClass2

    Function DoSomeWork(ByVal input As String, ByVal number As Integer) As String

End Interface

And then all we need to do is implement the new interface in the MyComClass class:

Public Class MyComClass
    Implements IMyComClass, IMyComClass2

    ...
    Public Function DoSomeWork(ByVal input As String, ByVal number As Integer) As String _
        Implements IMyComClass2.DoSomeWork

        Return DoSomeWork(input + number.ToString())
    End Function

    ...
End Class

You can see how this gives you much more control, and also a much more explicit separation between what's in .NET and what's exposed to COM. This ensures that whatever change you make, to either the .NET or the COM world, the change is explicit and you know which world is affected.

Happy Interop!

What others are saying

 avatar
Bill Wilson
Permalink to this comment 31 May 2008 16:17

Thanks Rupert. Funny how things "to make your life easy" (Like ComClassAttribute) always seem to end up making it harder!

rupert avatar
Rupert Benbrook
Permalink to this comment 02 June 2008 15:12

O how true that is! I've got more on ComClassAttribute to come, so watch this space.

Comments are closed for this post