Chapter 13. Exposing .NET Events to COM Clients

In This Chapter

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.

Exposing Events Without Using Extra CLR Support

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

Image

Image

Image

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

Image

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.

Exposing Events Using Extra CLR Support

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.

Using 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:

"IPhoneEventsIPhoneEvents2"

The first interface listed represents the default source interface.

Tip

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 Types) 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

Caution

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.

Defining a Source Interface

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.

The Phone Example Revisited

Listing 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

Image

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

Image

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.

Image

Visual Basic .NET’s 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

Image

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);
};

Caution

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 COMExceptions 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.

Design Guidelines

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.

Caution

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 VARIANTs 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 EventNameEventHandler and the EventArgs-derived class should be called EventNameEventArgs. 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 OnEventName 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

Image

Image

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.

Tip

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.

Example: Handling a .NET Windows Form’s Events from COM

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.

The .NET Event Source

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

Caution

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

Image

Image

Image

Image

Image

Image

Image

Image

Image

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 UICuesExpandedEventHandler, the new delegate replacing ControlEventHandler is named ControlExpandedEventHandler, 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 OnEventName 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.

Tip

Listing 13.7 takes advantage of the fact that Form exposes a virtual OnEventName 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.

The COM Event Sink

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

Image

Image

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.

Image

Conclusion

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.

Tip

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.141.29.145