|
This article is a continuation of my first article,
COM Interop Exposed.
The emphasis of this article will be
explaining how to expose your .NET events to COM clients.
As we did in the first article, a review of how events are done in COM will help you understand how to expose
your .NET events to COM. Unfortunately, it's not as easy as simply creating a .NET event and let the
COM Callable Wrapper (CCW) do the rest. We'll have to apply a few attributes to get clean COM/.NET
integration with events.
A History of COM Events
Just like the first article, this history of COM events will focus on interfaces. That's because COM doesn't have native support for true events like .NET does. Instead, COM uses interfaces to simulate events.
Any "events" mechanism needs a few key features:
- An object that supports letting other objects know something has happened (an event). This is typically called the "source" object.
- An object that can "subscribe" to these events. This is typically called the "sink" object because it sinks events from the source.
- Sink objects talk to the source object to subscribe to the events.
- The source object supports more than one sink listening for events. When an event happens, all subscribed sinks will be notified of the event.
- Sink objects can subsequently unsubscribe from an event if they no longer want to be notified.
COM accomplishes the above mechanism through interfaces in a process know as "Connection Point Protocol".
The Source Object
In COM's Connection Point Protocol, when an object wants to be a source for events, it defines an interface with methods that will be used to represent the events. Let's say that last part again to make sure it's understood: It defines an interface with methods that will be used to represent the events.
Assume we have a simple VB6 object that contains a "Click" event and a "MouseOver" event.
We'll call this object "CButton". When compiled, the object will already have one interface it
implements - the default interface we talked about in the first article. VB6 will automatically create this
interface and call it "_CButton". A second interface would be created called "__CButton".
This defines the events that CButton exposes.
The VB6 code would look like this:
Public Event Click()
Public Event MouseOver(ByVal x As Integer, ByVal y As Integer)
Once compiled, the COM type library will show the CButton coclass as:
coclass CButton {
[default] interface _CButton;
[default, source] dispinterface __CButton;
};
The first interface, _CButton, is the default interface created by VB6. The second interface, __CButton, is the
default source interface - i.e. the events. It is also created automatically by VB6 and is defined in the
type library as:
dispinterface __CButton {
properties:
methods:
[id(0x00000001)]
void Click();
[id(0x00000002)]
void MouseOver(
[in] short x,
[in] short y);
};
Notice that the events defined in VB6 show up as methods of the source interface. That will be an important point to understand when we set up our .NET events to be exposed to COM.
This source object (CButton) also implements a special interface called IConnectionPoint (this doesn't show up in the type library). This COM interface enables other objects to subscribe and unsubscribe to events as well as get a list of events by getting a list of all source interfaces. That's how the VB6 IDE knows what events each object supports - it simply uses the IConnectionPoint interface to enumerate across all source interfaces.
The Sink Object
The object that wants to "capture" or "subscribe to" an event does so by implementing the
source interface. In the example above, a sink object would implement the __CButton interface and add event
handling code in the Click and MouseOver methods. The sink object will pass an instance of itself to the
source object through the IConnectionPoint interface. The source will add the reference to a list of
other sink objects that are listening for the events.
Once the source object wants to "raise" an event, it simply goes through the list of subscribers,
casts each one to the interface and calls into the sink object through the interface method for the event
(remember, the sink object implements the interface that represents all of the events). So in the case
of the click event, it would call the "Click" method on all of the sink objects that are contained
in its internal list of subscribers. Since the sink object implemented the __CButton interface, it will
have code for the Click method.
As you can see, COM's "Connection Point Protocol" is a fancy way of doing callbacks. Interfaces are
used to define the callbacks (i.e. the events). Various built-in COM interfaces provide the means to subscribe
to, unsubscribe and call back into the sink objects through the interfaces - thus defining an events mechanism.
Exposing .NET Events to COM
Events in .NET are done through delegates. It is beyond the scope of this article to explain delegates.
For more details on delegates, see the February 2003 issue of MSDN magazine for an article entitled
"A Primer on Creating Type-Safe References to Methods in Visual Basic .NET".
Let's first look at how a .NET event is exposed to COM without any extra help from us. We'll define a simple "Bug" class that exposes a "Hungry" event - raised when the bug becomes hungry - and a "Found" event - raised when the bug has found an object (it could be food, it could be a tree, etc
):
[VB.NET]
Option Strict On
Option Explicit On
Namespace BugVB
Public Class Bug
Public Delegate Sub HungryEventHandler()
Public Delegate Sub FoundEventHandler(ByVal item As String)
Public Event Hungry As HungryEventHandler
Public Event Found As FoundEventHandler
End Class
End Namespace
[C#]
using System;
namespace BugCS
{
public class Bug
{
public delegate void HungryEventHandler();
public delegate void FoundEventHandler(string item);
public event HungryEventHandler Hungry;
public event FoundEventHandler Found;
}
}
Note that I defined specific delegates for the events in the VB.NET version. This is not required
as VB.NET will do this for you automatically, but when I present both C# and VB.NET code that does
the same thing, I prefer that the code matches as much as possible and avoid using "helpful" compiler features.
If we use TLBEXP.EXE to export a COM type library for this object, you'll notice the type library
has a coclass for each delegate, but there is no mechanism for hooking up the events. There's no
source interface like we saw in the VB6 example earlier. And without a source interface, if you
add a reference to the generated type library in VB6, you won't see the events in the Object Browser.
For a COM object to subscribe to these events using COM's "Connection Point Protocol", a
COM object would want an interface that defines the "Hungry" and "Found" methods
which it could use to call back into the sink objects. To make our .NET/COM integration smooth, we'll
do just that!
Defining the Interface for Events
Let's start by defining a regular .NET interface which we'll expose as a COM source interface (the interface for events).
There are a few key aspects to point out here:
- As with other classes and interfaces to be exposed to COM, it needs a unique GUID (see my first article for more on GUID's and COM Interop).
- For these events to work in VB6, they must be a COM DispInterface type. We can get this by applying the InterfaceTypeAttribute to our interface.
- For each event, the method name and signature on the source interface method must match the name and signature of the event (not the delegate!) exactly.
- Each method in the source interface must have the "System.Runtime.InteropServices.DispId" attribute applied with a unique value greater than zero. By providing a unique DispId, COM can call into the method directly without having to do a late-bound call.
Here's our .NET interface we'll use as our source interface for the Bug object:
[VB.NET]
Imports System.Runtime.InteropServices
<Guid("A66356CF-7408-4bf5-B02E-17158FE30DA3"), _
InterfaceType(ComInterfaceType.InterfaceIsIDispatch)> _
Public Interface IBugEvents
<DispId(1)> _
Sub Hungry()
<DispId(2)> _
Sub Found(ByVal item As String)
End Interface
[C#]
using System.Runtime.InteropServices;
[Guid("A66356CF-7408-4bf5-B02E-17158FE30DA3")]
[InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]
public interface IBugEvents
{
[DispId(1)]
void Hungry();
[DispId(2)]
void Found(string item);
}
Now that we have our interface defined, we just need to tell TLBEXP.EXE that this will be our source interface for events. Our "Bug" class doesn't need to implement this interface - it's a "placeholder" used solely for COM interop. This is why it is important that your interface method signatures match your .NET events - since they're only a placeholder for TLBEXP.EXE, .NET will not complain if there is a mismatch. Use the "ComSourceInterfaces" attribute on the Bug class to let COM know what interface will be our source interface for event sinks to hook into. In this case, it will be the IBugEvents interface:
[VB.NET]
<ComSourceInterfaces(GetType(IBugEvents))> _
Public Class Bug
Public Delegate Sub HungryEventHandler()
Public Delegate Sub FoundEventHandler(ByVal item As String)
Public Event Hungry As HungryEventHandler
Public Event Found As FoundEventHandler
End Class
[C#]
[ComSourceInterfaces(typeof(IBugEvents))]
public class Bug
{
public delegate void HungryEventHandler();
public delegate void FoundEventHandler(string item);
public event HungryEventHandler Hungry;
public event FoundEventHandler Found;
}
Now let's look how this .NET class looks when exported to COM:
coclass Bug {
[default] interface _Bug;
interface _Object;
[default, source] dispinterface IBugEvents;
};
Perfect! The IBugEvents interface is now listed as our source interface. Please note that I didn't
do the extra legwork as described in my first article on COM Interop - I just wanted to cover the
COM eventing mechanism in this sample. Now if we look at this type library with the VB6 object
browser will see our events just like we expect:
Summary
Defining events that will be exposed to COM is pretty easy, but it does take a few steps:
- Define your events in your .NET component as required by your design.
- Create an interface that contains the method names and signatures of your .NET events.
- Use the InterfaceType attribute to mark this interface as an IDispatch interface.
- Use the ComSourceInterfaces attribute on your .NET object to define this interface as the "source" interface for COM events.
About the Author
Patrick Steele is an independent consultant in southeastern Michigan. He has a broad range of
.NET experience including ASP.NET, WinForms, COM+ and COM interop. He's the secretary of the
Michigan Great Lakes Area .NET Users group (GANG - http://www.migang.org)
and has been recognized by Microsoft for the past five years as a .NET MVP. Contact Patrick
through his blog at http://weblogs.asp.net/psteele
or patrick (at) mvps (dot) org.
|