What's wrong with ComClassAttribute?
- Tuesday 20 May 2008 21:12 by Rupert Benbrook
- 2 Comments
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
Bill Wilson
Thanks Rupert. Funny how things "to make your life easy" (Like ComClassAttribute) always seem to end up making it harder!
Rupert Benbrook
O how true that is! I've got more on ComClassAttribute to come, so watch this space.