• Exposing Events Without Using Extra CLR Support
• Exposing Events Using Extra CLR Support
• Example: Handling a .NET Windows Form’s Events from COM
Events are a popular way to expose callback functionality in .NET, and are used extensively in the .NET Framework. That’s why it’s great that the type library importer exposes COM classes that use COM’s connection point protocol as .NET classes with .NET events, as explained in Chapter 5, “Responding to COM Events.” Unfortunately, as described in Chapter 10, “Advanced Topics for Using .NET Components,” the reverse direction (exposing .NET events via connection points) isn’t automatically handled by COM Interoperability. A .NET instance with event members is not exposed to COM as a connectable object. Instead, event members are exposed as a pair of accessor methods—add_
EventName
and remove_
EventName
.
As explained in Chapter 10, COM clients cannot hook and unhook unmanaged event handlers using these accessor methods. Attempting to work around this is especially painful for Visual Basic 6 clients because the language, runtime, and IDE are tailored for connection points, exposing them as easy to use events.
Fortunately, the Common Language Runtime (CLR) does have built-in support for exposing components with .NET events as connectable objects to COM—you just have to do a little work to enable it. This chapter discusses the steps to do this and issues you should be aware of.
The easiest way to expose callback functionality to COM would be to use a callback interface, as discussed in Chapter 5. But if you design your .NET type with events and you want it to be useable to COM clients, you should follow the steps in this chapter to enable connection-point support. Exposing .NET events as connection points involves a small amount of effort for a great gain in COM usability. None of the classes in the .NET Framework take advantage of this support to expose events nicely to COM, but then again most types are marked as COM-invisible anyway.
Before discussing how to use the built-in support, let’s examine the state of affairs without any special support in the CLR to expose .NET events as COM connection points. Imagine that we want to write a simple .NET Phone
class that defines two events—Ring
and CallerId
. This could be defined as follows in C#:
// Delegates for the events
public delegate void RingEventHandler();
public delegate void CallerIdEventHandler(string callerName,
byte [] callerPhoneNumber);
public class Phone
{
// The two events
public event RingEventHandler Ring;
public event CallerIdEventHandler CallerId;
...
}
Each event is associated with a delegate that defines the signature that any corresponding event handler must have. Ring
is raised with no parameters, but the CallerId
event is raised with the caller’s name and phone number (represented as a byte array to accommodate ever-growing phone numbers).
Whereas such a class works great in .NET languages, it cannot work from COM without extra effort. If a dual class interface were exposed for Phone
(using ClassInterface(ClassInterfaceType.AutoDual)
), we’d see the following four event accessor methods in an exported type library:
[id(0x60020004)]
HRESULT add_Ring([in] _RingEventHandler* value);
[id(0x60020005)]
HRESULT remove_Ring([in] _RingEventHandler* value);
[id(0x60020006)]
HRESULT add_CallerId([in] _CallerIdEventHandler* value);
[id(0x60020007)]
HRESULT remove_CallerId([in] _CallerIdEventHandler* value);
Theoretically, a COM client could call add_Ring
to hook up an event handler to the Ring
event, and remove_Ring
to unhook an event handler. The problem is that a COM client has no good way to pass an object implementing _RingEventHandler
or _CallerIdEventHandler
to any of these methods, as discussed in Chapter 10. The COM client would need to write some managed code that does the job of hooking and unhooking event handlers and exposing that code to COM in a usable way.
Rather than forcing COM clients to come up with a way of exposing .NET events in a usable way, Listing 13.1 updates the Phone
class and adds some supporting types to make it COM-friendly without sacrificing the design exposed to .NET clients.
Listing 13.1. One Way to Expose .NET Events to COM Without Using Built-In Connection Point Support from the CLR
Lines 1 and 2 use the System.Collections
namespace for Hashtable
and the System. Runtime.InteropServices
namespace for ClassInterfaceAttribute
and ClassInterfaceType
. Lines 5–9 define a callback interface that COM clients can implement to provide an event handler for each of the two events. This IPhoneEvents
interface serves as a replacement for both the _RingEventHandler
and _CallerIdEventHandler
class interfaces that COM objects can’t usefully implement. Note that by using a single interface for both events, a COM object must now provide some sort of implementation (even if it’s just an empty implementation) for both event handlers even if it only wants to handle one of the events.
The signature of IPhoneEvents.CallerId
differs from the signature of CallerIdEventHandler
in that callerPhoneNumber
is defined as a by-reference array. This is done for the sake of Visual Basic 6 clients who can’t consume signatures with by-value SAFEARRAY
parameters. Changing CallerIdEventHandler
’s signature to use a by-reference array would be misleading to .NET clients because the array reference should not be modified by event handlers. Therefore, the COM-focused signature and the .NET-focused signatures are left as being different, and the implementation of OnCallerId
(Lines 73–77) negotiate their differences.
Lines 12–16 define another interface, but this is one for COM clients to use rather than implement in order to add and remove event handlers. The Phone
class implements this interface, which serves as a COM-friendly version of the add and remove event accessors. The Add
method takes the place of both add_Ring
and add_CallerId
, and takes an IPhoneEvents
interface parameter. Therefore, a COM object that implements IPhoneEvents
can be passed in so its Ring
and CallerId
methods can be invoked when the corresponding events are raised. Add
returns a “cookie” value that uniquely identifies the event sink from the event source’s perspective. This cookie can be passed to the Remove
method defined in Line 15 to unhook a specific event sink.
Lines 48–61 contain the Phone
class’s implementation of the IPhoneEventHookup
interface. The Add
method simply adds the passed in object to the class’s private Hashtable
and returns a unique cookie that is used as the object’s key in the Hashtable
. The Remove
method removes the object from the Hashtable
using the passed-in cookie value. Because these Add
and Remove
methods are meant for COM clients only, Phone
attempts to hide them from .NET clients by using explicit interface implementation. .NET clients browsing or using the Phone
class directly don’t see these methods, yet because COM can only communicate with Phone
via the IPhoneEventHookup
interface, these methods are quite visible. Note that if Phone
has other methods that should be exposed to COM (like Dial
, HangUp
, and so on) then another interface defining these methods should be implemented or a class interface should be exposed.
The Phone
class’s constructor in Lines 37–45 initializes the Hashtable
and hooks up two private event handlers to its own events—OnRing
and OnCallerId
, defined in Lines 65–77. Both of these methods iterate through the Hashtable
and invoke the appropriate callback methods on each COM object. This technique transforms a single .NET event handler into a source of a semantically related COM event.
Using the types from Listing 13.1 in a COM client is fairly straightforward, but it’s a custom protocol that doesn’t provide the same kind of support and widespread understanding as do connection points. Listing 13.2 contains code from a Visual Basic 6 class module that references an exported type library for the assembly containing the Phone
class in Listing 13.1 and hooks up its event handlers using the custom protocol. It assumes the existence of a ConvertPhoneNumberToString
method that formats a byte array as a suitable phone number string.
Listing 13.2. A Visual Basic 6 Class That Implements the IPhoneEvents
Interface and Hooks Up Event Handlers to the Phone
Class
The communication between the COM client in Listing 13.2 and the .NET component in Listing 13.1 is similar to the connection point protocol. The IPhoneEventHookup
interface acts like a customized IConnectionPoint
interface, with Add
serving the same purpose as an interface-specific Advise
method, and Remove
functioning as an Unadvise
method. (Even the use of a cookie to identify event sinks works the same way.) In addition, the IPhoneEvents
interface serves the same role as a source interface. Consult Chapter 5 for a refresher on COM’s connection point protocol.
Unless possible increased performance of a custom protocol is desired, using the general connection points mechanism is more COM-friendly because many COM clients are designed to use connection points. Visual Basic 6 clients, for example, don’t need to worry about calling Advise
and Unadvise
like Listing 13.2 calls Add
and Remove
. Instead, when a variable is declared using WithEvents
, instantiating the object causes a QueryInterface
call to IConnectionPointContainer
, followed by a call to IConnectionPointContainer. FindConnectionPoint
, then a call to IConnectionPoint.Advise
. The Visual Basic 6 runtime even generates a sink object that implements the source interface on-the-fly, which forwards calls to any event handlers you define in your code.
ComSourceInterfacesAttribute
A .NET class could manually implement IConnectionPointContainer
and IConnectionPoint
to expose itself as an official connectable object, but COM Interoperability provides special support that achieves the same thing through the use of a custom attribute. This custom attribute is ComSourceInterfacesAttribute
, which is defined in the System.Runtime. InteropServices
namespace. This attribute can only be marked on a class, and contains the types of any source interfaces that the class supports. Using this attribute looks like the following:
C#:
[ComSourceInterfaces(typeof(IPhoneEvents))]
public class Phone
{
...
}
Visual Basic .NET:
<ComSourceInterfaces(GetType(IPhoneEvents))> _
Public Class Phone
...
End Class
C++:
[ComSourceInterfaces(__typeof(IPhoneEvents))]
public __gc class Phone
{
...
};
By using this custom attribute, classes can be exposed to COM with source interfaces that can be implemented by COM event sinks to handle .NET events.
The constructor for ComSourceInterfacesAttribute
has several overloads to accommodate specifying one to four types of source interfaces, for example (in Visual Basic .NET):
<ComSourceInterfaces(GetType(IPhoneEvents), GetType(IPhoneEvents2), _
GetType(IPhoneEvents3), GetType(IPhoneEvents4))> _
Public Class Phone
...
End Class
As with listing regular interfaces implemented by a class, the first parameter corresponds to the default source interface, whereas the others are just regular additional source interfaces.
In the rare case that you need to specify more than four source interfaces, you could instead use a constructor overload with a string parameter and provide a list of type names delimited with a null character. Here’s an example in Visual Basic .NET using just two source interfaces (for brevity):
<ComSourceInterfaces("IPhoneEvents" & Chr(0) & "IPhoneEvents2")> _
Public Class Phone
...
End Class
In C# and C++, the same string would be specified as:
"IPhoneEvents IPhoneEvents2"
The first interface listed represents the default source interface.
You should design classes that only use one source interface to expose methods for all the class’s events. Although unmanaged C++ clients can use multiple source interfaces, Visual Basic 6 only supports WithEvents
syntax for a single default source interface. Using multiple source interfaces should be restricted to the case of writing a COM-compatible class for COM clients already written to expect multiple source interfaces.
As demonstrated by the example, a string can be used for any number of source interfaces. It may seem odd that a constructor exists to use such a string (rather than an array of Type
s) but regardless of whether an overload with Type
parameters is used or the overload with a string parameter is used, the information is persisted as one null-delimited string in metadata anyway. Plus, using a string is useful to make a “late bound” type reference—one that doesn’t require the interface type’s metadata at compilation time.
As is the case with late binding in general, extra care is required because a compiler doesn’t check that the string is in the correct format or that it corresponds to a valid interface type. The string used to specify an interface (or a substring when more than one interface is specified) has the same format as a string used in the System.Type.GetType
. This means that a simple interface name can be used if the type is in the same assembly as the custom attribute; otherwise, a string of the following form must be used:
TypeName, AssemblyName
The AssemblyName portion can be a partial name like MyAssembly
or MyAssembly, Version=1.0.0.0
if the referenced assembly is not in the Global Assembly Cache; or a complete specification including the simple name, version, culture, and public key token, for example:
MyAssembly, Version=1.0.0.0, Culture=neutral, PublicKeyToken=b1bf107f04d50a3a
If you must use a string parameter with ComSourceInterfacesAttribute
, you should always use a full assembly name if the interface is defined in a different assembly. This is the only string that works if the assembly ends up being installed in the Global Assembly Cache. Specifying the version number in the assembly name does not mean that your class won’t work with a later version of the referenced assembly; .NET version policy can redirect requests for the original version to a new version if desired.
Using a constructor overload with Type
parameter(s) is preferred over using a string because the Type
instances are automatically persisted with complete assembly names for types in different assemblies. Plus, using the type is faster and less error-prone than using lengthy strings.
Using ComSourceInterfacesAttribute
requires a definition of at least one source interface that would not be required if COM clients weren’t involved. Defining the event members alone is not enough! You could define your own source interface, or import a type library containing the definition of an existing COM source interface for your .NET class to expose. A source interface has no special marking to make it a source interface; any interface can be used as a source interface simply by listing it inside the ComSourceInterfacesAttribute
custom attribute.
For a source interface to be useful, each of its methods should correspond to an event defined on a .NET class (or on its base classes) marked with ComSourceInterfacesAttribute
. Each source interface method must have the exact same name as the corresponding event, and must have the exact same signature as the event’s delegate type, with two exceptions:
• A parameter defined on a source interface could be replaced with a class that it derives from (a superclass). This fact comes in handy in the “Example: Handling Windows Forms Events from COM” section at the end of this chapter.
• A delegate and the corresponding source interface method could have different custom attributes.
A source interface’s methods can be listed in any order. The CLR determines which source interface methods correspond to which events by name and signature only. Failure to follow these rules is unfortunately not caught by any .NET compilers (because they don’t have knowledge about source interfaces) and prevents raised events from reaching COM clients.
Although a source interface definition doesn’t require any special custom attributes to work with C++ COM clients, there are two custom attributes you should use to make a source interface usable for Visual Basic 6 and script COM clients:
• InterfaceTypeAttribute
. Mark the source interface with InterfaceType(ComInterfaceType.InterfaceIsIDispatch)
to make it exposed to COM as a dispinterface. Visual Basic 6 and scripting languages only support the use of source interfaces that are pure dispinterfaces.
• DispIdAttribute
. Mark each of the source interface’s methods with DispId(
n
)
, where n is a unique number greater than zero. This is needed to support Visual Basic 6 clients who provide event handlers for only a subset of the source interface’s methods.
Making the source interface a dispinterface is understandable, but the reason for marking each method with a DISPID is subtle. If a COM event sink implementing the source interface is connected to a .NET class with events, the CLR invokes the appropriate source interface methods when its events are raised. When the source interface is a dispinterface, the CLR must call the event sink late bound via IDispatch
. If a source interface method is marked with a DISPID in metadata (with DispIdAttribute
), the CLR directly calls IDispatch.Invoke
. Otherwise, it must call IDispatch.GetIDsOfNames
first to get the method’s DISPID. However, if a Visual Basic 6 client doesn’t provide an event handler corresponding to the event being raised, the VB6 dynamic sink object returns the failure HRESULT
value DISP_E_UNKNOWNNAME
when calling GetIDsOfNames
with that event’s name. Yet if Invoke
is called with its DISPID, the call succeeds and just does nothing because the VB6 user didn’t provide any implementation.
Therefore, if you don’t mark your source interface’s methods with DISPIDs, an exception like the following occurs if a Visual Basic 6 client doesn’t provide event handlers for every source interface method:
System.Runtime.InteropServices.COMException (0x80020006): Unknown name.
at System.RuntimeType.InvokeDispMethod(...)
at System.RuntimeType.InvokeMember(...)
at System.RuntimeType.ForwardCallToInvokeMember(...)
at ISourceInterface.MyEvent()
at ManagedClass.MethodThatRaisesEvent()
Future versions of the CLR are likely to solve this problem by always avoiding the call to GetIDsOfNames
, because .NET methods without explicit DISPIDs have CLR-assigned DISPIDs that could be used. But since version 1.0 of the CLR does call GetIDsOfNames
when no explicit DISPID exists, you should always provide them explicitly.
Phone
Example RevisitedListing 13.3 updates the Phone
class from Listing 13.1 to expose a connection point to COM using ComSourceInterfacesAttribute
and a source interface, complete with explicit DISPIDs. Because the source interface signatures used by COM must match the delegate signatures used by .NET, the second parameter of CallerIdEventHandler
has been changed from a byte array to a string to avoid the unnatural situation of making the array a by-reference parameter for the sake of VB6.
Listing 13.3. Defining a Source Interface and Using ComSourceInterfacesAttribute
to Expose Connection Points to COM
No code inside the Phone
class needs to be concerned with treating the events specially for COM purposes. The custom attribute in Line 17 and source interface in Lines 5–10 enable the CLR to handle it all. A type library exported for an assembly with the code from Listing 13.3 contains the following coclass:
[
uuid(9963B116-B0DC-3AAB-81DF-286170D63E85),
version(1.0),
custom({0F21F359-AB84-41E8-9A78-36D110E6D2F9}, "Phone")
]
coclass Phone {
[default] interface _Phone;
interface _Object;
[default, source] dispinterface IPhoneEvents;
};
Listing 13.4 shows an update to the Visual Basic 6 code from Listing 13.2 that hooks up event handlers to the Phone
class from Listing 13.3. This code is much simpler than Listing 13.4, as Visual Basic 6 code should be. The event handler hookup is handled by the VB6 runtime because the WithEvents
keyword is used.
Listing 13.4. A Visual Basic 6 Class That Uses the WithEvents
Keyword to Enable Automatic Event Handler Hookup to the Phone
Class
Figure 13.1 shows the list of events that appears in the Visual Basic 6 IDE for a type declared using WithEvents
.
Figure 13.1. The Visual Basic 6 IDE provides a drop-down list of events for easy event handler hookup.
ComClassAttribute
A great feature of VB .NET’s ComClassAttribute
custom attribute, introduced in the previous chapter, is that it prompts the VB .NET compiler to automatically emit a source interface for any class marked with the attribute that has public event members. The generated source interface has an IID equal to the third GUID specified in the ComClassAttribute
constructor (identified as the EventsId
constant in the Visual Studio .NET COM Class
template). The interface contains one method for every public event (excluding events on base classes), is a dispinterface, and is marked with explicit DISPIDs. The compiler also emits the appropriate ComSourceInterfacesAttribute
on the class so no extra work is required to expose .NET events as connection points using the best practices described in the previous section. Listing 13.5 is a translation of the C# code from Listing 13.3 that takes advantage of ComClassAttribute
’s event support.
Listing 13.5. Using Microsoft.VisualBasic.ComClassAttribute
to Expose Connection Points to COM
Using the IL Disassembler (ILDASM.EXE
) to examine the metadata produced by compiling Listing 13.5, you can see the various .NET supporting types emitted by the VB .NET compiler, all of which are nested types of the Phone
class. Besides the _Phone
class interface and the RingEventHandler
and CallerIdEventHandler
delegates, the compiler generates a source interface called __Phone
. This interface looks like the IPhoneEvents
interface in Listing 13.3, just with a different name. The compiler even emits DISPIDs the same way: sequentially starting from one based on the order the events are defined.
You can also see these compiler-generated types when exporting a type library for an assembly containing the code from Listing 13.5. The Phone
class is exported as follows, in IDL syntax:
[
uuid(F7EB2080-18FE-43CE-8FB1-B7CB7DE91C4B),
version(1.0),
custom(0F21F359-AB84-41E8-9A78-36D110E6D2F9, "Phone")
]
coclass Phone {
interface _Object;
[default] interface _Phone;
[default, source] dispinterface __Phone;
};
The __Phone
source interface is exported as follows, in IDL syntax:
[
uuid(4E4B25B1-7266-41D9-BC09-425394E14D1B),
version(1.0),
custom(0F21F359-AB84-41E8-9A78-36D110E6D2F9, "Phone+__Phone")
]
dispinterface __Phone {
properties:
methods:
[id(0x00000001)]
void Ring();
[id(0x00000002)]
void CallerId([in] BSTR callerName, [in] BSTR callerPhoneNumber);
};
The behavior of ComClassAttribute
does not version properly when you add new events to your class, because you have little control over the source interface generated for you. First, if you don’t list the new events last in your class definition, the DISPIDs for existing source interface methods will change, which would be problematic for any component that cached the DISPIDs from the previous version of your exported type library. (This also means you should never rearrange the order of your class’s events in source code when using ComClassAttribute
, something that could be done if you controlled the definition of the corresponding source interface.) Second, existing COM clients would no longer be implementing the entire source interface, causing failures whenever the .NET class raised the new events. Adding methods to an interface is never compatible when existing clients implement it!
To solve this problem, you could switch away from using ComClassAttribute
if you need to add new events, and instead use ComSourceInterfacesAttribute
as in Listing 13.3. When you explicitly define your source interface, you can omit methods corresponding to the new events in order to remain compatible with the previous version of your class. Plus, you could define a second source interface with new methods corresponding to the new events and add this interface as a second type listed in your class’s ComSourceInterfacesAttribute
custom attribute.
An easier solution would be to continue using ComClassAttribute
then catching and ignoring COMException
s thrown when raising the new events, because raising a new event would cause the event sink’s IDispatch.Invoke
method to return a failure HRESULT
. This is not ideal because, besides worse performance, the exception could prevent other clients from receiving the event when multiple event sinks are listening.
The Phone
example from all the previous listings used events and delegates in the simplest possible way so as not to complicate the demonstration of exposing events to COM. However, the .NET Framework SDK lists design guidelines regarding events, and the Phone
class in Listings 13.3 and 13.5 does not follow these guidelines. Since this chapter is about designing .NET events, it would be a disservice not to discuss these guidelines. They include the following:
• An event’s delegate should return void
(be a Sub
in VB .NET) and have two parameters. The first one should be a System.Object
representing the source or sender of the event (the class who raised it), and the second one should be a class derived from System.EventArgs
containing properties that can be read and/or written by an event sink. (These parameters should be named sender
and e
, respectively.) If the event has no associated data to send or receive, the second parameter should just be the System.EventArgs
type. If you want an event sink to be able to “return” a value, this can simply be done via properties on the EventArgs
-derived class. Since classes are reference types, any changes to property values within an event handler can be seen by the event source without having to pass the EventArgs
-derived instance by-reference.
Don’t ever use a user-defined value type as a delegate parameter. If a source interface contains a method with the matching signature, and if the source interface is a dispinterface (as it should be), COM clients would be unable to handle the event. This is because the CLR would make a late-bound call to the source interface’s method with the value type parameter, which would not work because the Interop Marshaler does not support VARIANT
s with the VT_RECORD
type in version 1.0. Fortunately, by sticking to the .NET design guidelines of encapsulating all data in an EventArgs
-derived class, you avoid any such problems.
• For clarity and consistency, a delegate used by an event should be named EventName
EventHandler
and the EventArgs
-derived class should be called EventName
EventArgs
. If the delegate and EventArgs
-derived class are used for multiple events, a more generic name should be used that describes the type of events these should be applied to.
• The raising of each event should be done in a protected On
EventName
method. This way, subclasses can override the event behavior because it’s not possible for them to directly raise base class events by default.
Listing 13.6 updates Listing 13.5, keeping these design guidelines in mind. Although the delegates are hidden by Visual Basic .NET, the compiler generates them with names that coincide with the .NET design guidelines.
Listing 13.6. An Update to Listing 13.5 That Makes the Phone
Class and Its Events Compliant with .NET Design Guidelines
Lines 12–14 contain the new delegate signatures that comply with .NET design guidelines. The methods on the corresponding source interface must match these new signatures, which is taken care of by the VB .NET compiler. Lines 30–52 define the new CallerIdEventArgs
class that exposes what used to be parameters of the CallerIdEventHandler
delegate as read-only properties.
Lines 18–24 define the two On
... methods that encapsulate raising each event. In Listing 13.5, code that raises the two events was omitted because it could’ve been done in any of the class’s methods that were omitted. Here, this code is shown because we’re assuming that any other methods implemented by Phone
call OnRing
and OnCallerId
to raise the events rather than doing it directly.
Before raising an event, you should always check to see if the event member is null first. Visual Basic .NET’s RaiseEvent
statement does this for you, so an equivalent implementation of OnRing
would look as follows in C#:
protected virtual void OnRing(EventArgs e) { if (Ring != null) Ring(this, e);}
An event is null when no event handlers are hooked up to its underlying delegate, so attempting to raise the event in this situation results in a NullReferenceException
.
For a realistic example of exposing events to COM, this section examines what it takes to expose all of a .NET Windows Form’s events to COM event sinks. We’ll create an application that consists of a Visual Basic 6 COM client functioning as an event sink for a .NET Windows Form it instantiates. Every event raised by the Windows Form is recorded by the COM client by adding it to a TreeView
control with the time it was raised plus additional data communicated through the event’s delegate.
System.Windows.Forms.Form
raises a whopping 71 events, most of which are inherited from base classes. The event members and delegate types are already defined by the .NET Framework, so all that’s needed is a source interface definition containing 71 methods whose names match the event names and whose signatures match the delegate signatures. (These event members can be seen in the Visual Studio .NET object browser marked with lightning bolts, or in the IL Disassembler with red triangles pointing upward. In both cases you need to check all the base classes to see all the inherited events.) Of course, a subset of the methods could be defined if we aren’t concerned with exposing all of Form
’s events to COM.
Since the Form
class isn’t marked with the necessary ComSourceInterfacesAttribute
, we need to define a new class—ComFriendlyForm
—that has this custom attribute. The easiest way to have ComFriendlyForm
raise the same events as Form
appropriately is to derive the class from Form
. Any instance of ComFriendlyForm
would then inherit all of the event-raising functionality, yet COM clients could use it as a connectable object thanks to the custom attribute. Such a class would look simply like the following (in Visual Basic .NET):
' An empty Windows Form that exposes its
' events to COM using connection points
<ComSourceInterfaces(GetType(IFormEvents))> _
Public Class ComFriendlyForm
Inherits Form
End Class
This assumes that we’ve defined an IFormEvents
source interface containing all of the necessary methods. Doing this with Form
is not so easy, however, because many of its events’ delegate parameters are marked as COM-invisible. This means that many methods of the source interface would be exported with IUnknown
parameters. For example, Form
has a Validating
event that uses the System.ComponentModel.CancelEventHandler
delegate type defined as follows (in Visual Basic .NET):
Public Delegate Sub CancelEventHandler(ByVal sender As Object, _
ByVal e As CancelEventArgs)
CancelEventHandler
is COM-invisible, but that’s actually irrelevant because COM clients interact with source interface methods directly rather than using delegates. What is important, however, is that the CancelEventHandler
delegate’s second parameter—System.ComponentModel. CancelEventArgs
—is COM-invisible. Therefore, a source interface method corresponding to the Validating
event, as the following in Visual Basic .NET:
<InterfaceTypeAttribute(ComInterfaceType.InterfaceIsIDispatch)> _
Public Interface IFormEvents
...
<DispId(68)> Sub Validating(sender As Object, e As CancelEventArgs)
...
End Interface
would be exported as follows (in IDL):
void Validating([in] VARIANT sender, [in] IUnknown* e);
This isn’t always a problem for unmanaged C++ clients; they could just ignore the second parameter or attempt to query for COM-visible interfaces. In this case, however, a CancelEventArgs
instance doesn’t implement any useful COM-visible interfaces.
Such a signature is more problematic for Visual Basic 6 clients. Visual Basic 6 doesn’t support event handler signatures with IUnknown
parameters, so handling the Validating
event isn’t an option—even when ignoring the second parameter! Referencing a type library containing the previous Validating
signature and selecting it in the Visual Basic 6 IDE (for a windowsForm
variable declared using WithEvents
) generates the following signature:
Private Sub windowsForm_Validating(ByVal sender As Variant, ByVal e As 0)
End Sub
Unfortunately, neither changing nor leaving such a signature results in something that can be compiled by VB6. Therefore, we have three options for accommodating Visual Basic 6 clients:
• Option 1—Omit problematic methods from the source interface we define.
• Option 2—Replace COM-invisible parameters in source interface methods with COM-visible superclasses. For example, we could replace System.ComponentModel. CancelEventArgs
with System.EventArgs
.
• Option 3—Change the signatures of problematic source interface methods to expose the same data with different COM-visible types. This involves defining new delegates to match these signatures and defining corresponding events that are raised whenever the original events are raised.
The first option is the easiest but doesn’t enable the problematic events to be raised to COM clients. The second option is a slick way to enable COM clients to handle all events, but they would not be able to access the COM-invisible data that accompanies the event. The VB .NET compiler performs option 2 when encountering COM-invisible delegate parameters used by a class marked with the ComClassAttribute
custom attribute.
The third option involves the most work but provides the best experience for COM clients, because they can access all the data that accompanies every event. Listing 13.8 demonstrates this technique. For System.Windows.Forms.Form
, this involves defining 10 new events that correspond to problematic events, and 8 new delegates for these events (because some events share the same delegate type). You can compile this listing with the C# command-line compiler as follows:
csc /t:library Listing13_7.cs /r:System.Windows.Forms.dll /r:System.Drawing.dll /r:System.dll
Version 7.0 of the Visual C# .NET compiler emits ten warnings when compiling Listing 13.7 stating that the ten events in Lines 143–154 require the new
keyword, but these events are defined with the new
keyword. This is a bug in the compiler, but because they’re just warnings they can be safely ignored.
Listing 13.7. A .NET Windows Form with an IFormEvents
Source Interface Exposing All 71 Events to COM
Lines 1–5 use the System
namespace for EventArgs
and IntPtr
, System.Globalization
for CultureInfo
, System.Windows.Forms
for Form
and the various EventArgs
-derived classes, System.ComponentModel
for CancelEventArgs
and IComponent
, and System.Runtime. InteropServices
for ComSourceInterfacesAttribute
, DispIdAttribute
, and InterfaceTypeAttribute
.
Lines 8–101 define the IFormEvents
source interface with all 71 methods. It’s defined as a dispinterface and has explicit DISPIDs to provide maximum flexibility to COM clients. Since the order of the source interface’s methods doesn’t matter, they are arranged by first listing the unaltered event handlers for Form
and its two base classes (Control
and Component
), followed by the altered event handlers. These altered signatures (in Lines 81–100) match the delegate signatures in Lines 111–136.
These delegates are nested inside the ComFriendlyForm
class, which is a common practice for expressing the relationship between delegates and the class that makes use of them. They could have easily been defined outside of the ComFriendlyForm
declaration, and would have no effect on COM clients. The new delegate replacing UICuesEventHandler
is named UICues
Expanded
EventHandler
, the new delegate replacing ControlEventHandler
is named Control
Expanded
EventHandler
, and so on. These names are chosen because the various COM-invisible EventArgs
-derived classes are expanded in these delegate signatures; rather than being contained inside a single EventArgs
-derived parameter, the properties are exposed directly because they are often COM-visible types like strings and integers.
Looking at UICuesExpandedEventHandler
in Lines 111–113, notice how the original delegate signature:
public delegate void UICuesEventHandler(object sender, UICuesEventArgs e);
is “replaced” with:
public delegate void UICuesExpandedEventHandler(object sender,
bool changeFocus, bool changeKeyboard, bool showFocus, bool showKeyboard);
because UICuesEventArgs
has four boolean properties. ControlEventArgs
has a single Control
property, but exposing a Control
parameter in ControlExpandedEventHandler
doesn’t help much because Control
is also COM-invisible. Therefore, Lines 115 and 116 define ControlExpandedEventHandler
with an IComponent
parameter because Control
implements this COM-visible interface. (Making the parameter a Control
type would work for unmanaged C++ clients because they could call QueryInterface
on the exported IUnknown
type and obtain an IComponent
interface pointer, but Visual Basic 6 clients would still be stuck due to lack of support for event handler signatures with IUnknown
parameters.)
For InvalidateExpandedEventHandler
in Lines 118 and 119, exposing the single Rectangle
property from InvalidateEventArgs
is undesirable because System.Drawing.Rectangle
is COM-invisible. Therefore, Rectangle
’s integer properties are exposed instead. For PaintExpandedEventHandler
in Lines 124–125, we expose an HDC (a handle to a Windows Device Context) for the COM-invisible Graphics
property of PaintEventArgs
because unmanaged clients are accustomed to using HDCs when handling painting. The CancelExpandedEventHandler
delegate in Lines 127 and 128 must define the boolean cancel
parameter by-reference because the client can validly set its value inside the event handler code to indicate whether or not to cancel a raised event that uses this delegate.
The new events that use these delegate types are defined in Lines 143–154. Because they are given the same names as the events in the base classes, the new
keyword is used (Shadows
in Visual Basic .NET) with the goal of avoiding compiler warnings about hiding members of the base class. (It doesn’t work in this case, however, as mentioned earlier.) These events could have been given different names, for example:
public event UICuesExpandedEventHandler ComFriendlyChangeUICues;
However, because the methods on the source interface must match the names of the events, keeping the original event names is desirable so our customized method signatures on the source interface can retain the familiar event names.
To make the new delegates and events we’ve defined useful, these new events must be raised with the appropriate data at the appropriate times. This is accomplished in Lines 162–228 by overriding Form
’s On
EventName
methods and firing the new events inside these methods.
For example, the overridden OnChangeUICues
method in Lines 162–170 first calls the base OnChangeUICues
, which raises the original ChangeUICues
event. Then, in Lines 167 and 168, it raises the new ChangeUICues
event if there are any event handlers hooked up. This is checked by comparing the event member to null. To fill in the parameters of the new ChangeUICues
delegate, the properties of the passed-in UICuesEventArgs
instance are used. Most of the remaining methods follow this simple pattern of gluing the two events together.
The Validating
, Closing
, and InputLanguageChanging
events are handled specially because their delegates have a boolean by-reference cancel
parameter whose value may be changed by the event sink. Rather than overriding Form
’s OnValidating
, OnClosing
, and OnInputLanguageChanging
methods, ComFriendlyForm
defines three event handlers (Lines 236–269) that it hooks up to these three events inside its constructor (Lines 274–282). Looking at HandleValidating
as an example in Lines 236–245, a temporary variable is declared on Line 241 to retrieve and store the boolean value. This is done because C# doesn’t allow passing an object’s property by-reference directly. After the new event is raised to any COM event sinks that may be listening in Line 242, the cancel
member of the passed-in CancelEventArgs
instance is set to value of the by-reference parameter so the original event source can act on it appropriately. This technique of using an event handler to raise a similar event to COM event sinks was used back in Listing 13.1.
Listing 13.7 takes advantage of the fact that Form
exposes a virtual On
EventName
method for every event that needs to be customized. When an event doesn’t have such a method, you could simply do what was done for the Validating
, Closing
, and InputLanguageChanging
events—create a managed event handler that handles the COM interaction. An example of an event that doesn’t have a corresponding On
... method is Form
’s QueryAccessibilityHelp
event, but fortunately its parameters are COM-visible.
Listing 13.8 lists parts of a Visual Basic 6 event sink that references an exported type library from Listing 13.7 and acts as an event sink for ComFriendlyForm
. For brevity, portions of the code are omitted, but the full source code is available on this book’s Web site. To compile this listing, first register the assembly from Listing 13.7 and export a type library. For example:
regasm /tlb Listing13_7.dll /codebase
The /codebase
option can be used for convenience so the COM client can run within the VB6 IDE without having to install the assembly in the GAC or placing it in the directory containing VB6.EXE
. Once a type library has been exported, ensure that the Visual Basic 6 project references not only Listing13_7.tlb
, but also mscorlib.tlb
, System.Windows.Forms.tlb
, and System.tlb
. These additional type libraries must be referenced because the IFormEvents
methods have parameters whose types are defined in these dependent type libraries, such as IComponent
or KeyEventArgs
.
Listing 13.8. A Visual Basic 6 Form that Sinks All the Events Exposed by ComFriendlyForm
from Listing 13.7
Line 1 declares a ComFriendlyForm
variable using WithEvents
to enable the automatic event hookup, and Line 4 instantiates the ComFriendlyForm
object, triggering the FindConnectionPoint
calls behind-the-scenes. Lines 8–11 contain a standard event handler for the Visual Basic 6 Form’s resize event, simply resizing the TreeView
control named TreeView1
to occupy the entire surface area of the form. The TreeView
control is the form’s only control.
Lines 15–22 define the AddMessage
method used by all of the Windows Form event handlers. This method adds a node to the TreeView
control whose parent is specified by the node
parameter, containing the text in the message
parameter. If the tree node specified by the node
string doesn’t already exist, one is created so the child node can be added. With this technique, only the events raised show up in the TreeView
control, and in the order each was first raised.
The windowsForm_Activated
method in Lines 24–27 is the event handler for the Activated
event. It’s an example of a handler for a simple event that conveys no extra information besides the sender. For this kind of event, AddMessage
is called with the event name for the node and the current time (using Visual Basic 6’s Time
method) for the message. The windowsForm_ ChangeUICues
method in Lines 29–35 demonstrates using additional parameters from a customized event handler and appending the information to the string sent to AddMessage
.
For many of the event handlers whose signatures weren’t modified, the EventArgs
-derived class exposed has properties that need to be invoked via late binding because the class has an auto-dispatch class interface. The windowsForm_DragDrop
method shown in Lines 37–44 late binds to the DragEventArgs
parameter by setting it to an Object
variable before invoking the properties. Some of the EventArgs
-derived classes not shown in this listing have COM-invisible enum members, but their integral values can still be obtained from COM thanks to the internal IDispatch
implementation used by .NET objects (described in the next chapter).
Figure 13.2 demonstrates what happens when running the Visual Basic 6 client application from Listing 13.8 and manipulating the Windows Form in order to provoke events.
Figure 13.2. The Visual Basic 6 Form displays events raised by the foreground .NET Windows Form.
This chapter was mainly about the proper use of a single custom attribute—ComSourceInterfacesAttribute
. It’s important to highlight this custom attribute more than the ones from Chapter 12, “Customizing COM’s View of .NET Components,” because of its importance in providing COM clients with a useable programming experience “out of the box.” Exposing .NET events to COM is the most notable area for which the default behavior performed by COM Interoperability is not the behavior that most people want.
Because defining an appropriate source interface, marking it as a dispinterface, and placing DISPIDs on all of its members is tedious and a bit mysterious to those not familiar with COM Interoperability or connection points, VB .NET’s ComClassAttribute
provides a nice mechanism for shielding developers from this work and enabling a rapid application development (RAD) experience. There’s a price to pay for this easy-to-use mechanism, and that’s the inability to customize the generated source interface (without making use of the IL Disassembler and IL Assembler, which defeats the RAD motivation behind using ComClassAttribute
). The main drawback of using ComClassAttribute
for events support is the negative impact on versioning when you want to add more events to your class, an action that would have been safe to do otherwise.
If you want your .NET class to expose a source interface already defined in a type library (say ISourceInterface
), you can go one step further than importing the type library and marking the class with a ComSourceInterfacesAttribute
that lists the source interface. You can also have your class implement the type library importer-generated interface ISourceInterface_Event
, which contains all the event members that correspond to the methods of the source interface. These events use the delegate types also generated by the importer, so you don’t have to go through the hassle of defining your own delegates. Plus, by implementing the ISourceInterface_Event
interface you get compile-time errors rather than run-time errors if your class doesn’t correctly define all the necessary event members. You might even consider defining a brand new source interface in a type library and importing a Primary Interop Assembly (rather than starting in managed code) to take advantage of this extra support.