• Callbacks in .NET
• Callbacks in COM
• Handling COM Events in Managed Code
• Handling ActiveX Control Events in Managed Code
In both COM and .NET, events play a central role in bi-directional communication. Bi-directional communication refers to the paradigm in which clients call into components, which in turn call back into clients. In graphical user interfaces, for example, bi-directional communication is natural because clients of user interface objects such as buttons, check boxes, and tabs wish to be notified when the human user changes their states. Besides graphical applications, callback functionality is commonly used for asynchronous processing when a method has a lot of work to do, or used to let clients insert their own logic inside a method’s algorithm. For example, the caller of a sorting algorithm could be called back whenever two objects in a collection need to be compared, so the client can customize the algorithm to suit its needs.
The title of this chapter is overly simplified because, strictly speaking, COM doesn’t have built-in events like .NET does. Instead, COM components often use a standard connection points protocol, described in this chapter, that tools like Visual Basic 6 are able to conceal in an abstraction of events. The developers of COM Interoperability went to great lengths to make “events” in COM be exposed as .NET events, but some aspects of this support are not intuitive to people encountering it for the first time. This chapter tells you everything you need to know about handling a COM component’s events in managed code.
Although arguably less convoluted than COM’s bi-directional conventions, the subject of bi-directional communication in .NET can often cause confusion. Before jumping into the topic of how COM events are exposed to .NET, it helps to understand the .NET callback mechanisms. This section briefly covers the three main mechanisms for implementing callback behavior in .NET applications:
• Passing an interface instance
• Passing a delegate
• Hooking up to events
In the “callback interfaces” approach, a member can be defined with an interface parameter on which it calls members, perhaps on the same thread or perhaps on a different thread. With this scheme, any client object can simply implement the interface then pass itself (or an instance of another object implementing the interface) to the member. This is demonstrated with a contrived example in Listing 5.1.
Listing 5.1. Using an Interface to Provide Callback Functionality in .NET
Lines 3–8 define a callback interface—IChatRoomDisplay
—that is used by the Run
method in Lines 42–48. The ConsoleDisplay
class in Lines 10–26 implements the three callback methods and acts as the callee when ChatRoomServer.Run
calls its DisplayMessage
method.
This pattern of callback interfaces is used in several places in the .NET Framework. For example, an object implementing System.Collections.IComparer
can be passed to System.Array. Sort
or System.Array.BinarySearch
for callback purposes. A class type could be used in a method signature for callbacks rather than an interface, but this is usually undesirable because it limits the types of objects that can be passed in.
Whereas an interface is a contract for a collection of methods, a delegate is a contract for a single method. Delegates are often referred to as type-safe function pointers because they represent a reference to any method with a signature that matches the delegate definition—whether it’s an instance method or static method (Shared
in Visual Basic .NET).
Delegates can be used for callbacks on a method-by-method basis rather than using an interface with potentially several members (such as IChatRoomClient
and its three methods). Besides supporting bi-directional communication at the granularity of methods, delegates also enable multicasting of callbacks. This means that multiple delegates can be combined together such that what appears to be a single callback to the callback initiator is actually a method invocation on every one of a list of methods.
A delegate is declared like a method with no body plus an extra keyword:
C#:
delegate void DisplayMessage(string text);
Visual Basic .NET:
Delegate Sub DisplayMessage(text As String)
C++:
__delegate DisplayMessage(String* text);
When compiled to metadata, delegates are represented as classes deriving from System.Delegate
or the derived System.MulticastDelegate
class (for multicast delegates). All delegates defined in C#, Visual Basic .NET, or C++ are emitted as multicast delegates. Because delegates are types just like classes or interfaces, they can appear outside a type definition or inside as a nested type. Listing 5.2 updates the callback example from Listing 5.1 using delegates.
Listing 5.2. Using Delegates to Provide Callback Functionality in C#
Lines 4–6 define three delegates replacing the single interface from Listing 5.1. In this example, the Display
class chooses to provide two methods matching only one delegate—DisplayMessage
. Because an implementation for UserJoined
or UserLeft
is not required by this version of the ChatRoomServer.Run
method, the class doesn’t have to bother with them. Lines 27 and 28 create two delegates by passing the function name with a given instance to the delegate’s constructor. The C# compiler treats delegate construction specially, hiding the fact that a delegate’s constructor requires an instance and a function pointer. In C++, you have to instantiate a delegate as follows:
DisplayMessage* one = new DisplayMessage(display, &Display::ConsoleMessage);
DisplayMessage* two = new DisplayMessage(display, &Display::AnnoyingMessage);
Line 29 creates a third delegate by adding the previous two. This provides multicasting behavior so the call to the delegate in Line 42 invokes both the ConsoleMessage
and AnnoyingMessage
methods (in no guaranteed order). Often, the +=
and -=
operators are used with multicast delegates in order to add/subtract a new delegate from an existing one.
Listing 5.3 demonstrates the same code that uses delegates from Listing 5.2, but in Visual Basic .NET rather than C#.
Listing 5.3. Using Delegates to Provide Callback Functionality in Visual Basic .NET
Lines 4–6 define the three delegates, and Lines 8–16 contain the Display
class with two methods that match the signature of DisplayMessage
. Lines 22 and 23 create two delegates by simply using the AddressOf
keyword before the name of each method. Lines 22 and 23 could have been replaced with the following longer syntax:
Dim one As DisplayMessage = _
New DisplayMessage(AddressOf display.ConsoleMessage)
Dim two As DisplayMessage = _
New DisplayMessage(AddressOf display.AnnoyingMessage)
but the shorter version used in the listing is preferred.
Line 24 creates the both
delegate by combining the previous two. Visual Basic .NET doesn’t support overloaded operators (such as +
used in Listing 5.2), but calling Delegate.Combine
has the same effect as adding two delegates in C#. Similarly, calling Delegate.Remove
has the same effect as subtracting one delegate from another in C#.
Events are a layer of abstraction over multicast delegates. Whereas delegates are types, events are first class members of types just like methods, properties, and fields. An event member is defined with a multicast delegate type, representing the “type” of event, or the signature that any handlers of it must have, for example:
C#:
// The delegate
public delegate void DisplayMessageEventHandler(string text);
// The event
public event DisplayMessageEventHandler DisplayMessage;
Visual Basic .NET:
' The delegate
Public Delegate Sub DisplayMessageEventHandler(text As String)
' The event
Public Event DisplayMessage As DisplayMessageEventHandler
C++:
// The delegate
public: __delegate DisplayMessageEventHandler(String* text);
// The event
public: __event DisplayMessageEventHandler* DisplayMessage;
By convention, delegates representing event handler signatures are given an EventHandler
suffix. Visual Basic .NET further simplifies event definitions by enabling you to specify a delegate signature directly in the event definition:
Public Event DisplayMessage(text As String)
In this case, the compiler emits a delegate with the name DisplayMessageEventHandler
, nested in the class containing the event.
Just as a property is typically just an abstraction over a private field of the same type with get and/or set accessors, an event, by default, is an abstraction over a private field of the same delegate type with two accessors called add and remove. These two accessor methods expose the delegate’s Combine
(+
) and Remove
(-
) functionality, respectively, and nothing else.
A consequence of the event’s corresponding delegate field being private is that it can only be invoked—and therefore the event can only be raised—by the class defining the event, even if the event member is public. This is the reason classes with events often define protected OnEventName methods that raise an event, so derived classes can raise them too.
An interesting difference between defining events and defining properties is that an event’s accessors and the private field being used by the accessors are all emitted by default in C# and Visual Basic .NET. In C#, you can opt to explicitly define these accessors in case you want to implement the event in an alternative way, but the default behavior is almost always sufficient. To get a clear picture of what an event definition really means, here’s how you could define everything explicitly in C# and get the same behavior as the DisplayMessage
event definitions shown previously:
// Private delegate field
private DisplayMessageEventHandler displayMessage;
// Public event with explicit add/remove accessors
public event DisplayMessageEventHandler DisplayMessage
{
[MethodImpl(MethodImplOptions.Synchronized)]
add { displayMessage += value; }
[MethodImpl(MethodImplOptions.Synchronized)]
remove { displayMessage -= value; }
}
The MethodImplAttribute
pseudo-custom attribute is used with the Synchronized
value to ensure that multiple threads can’t execute these accessors simultaneously.
Listing 5.4 continues the example from the previous three listings, but this time using events instead of callback interfaces or just delegates.
Listing 5.4. Using Events to Provide Callback Functionality in C#
This listing starts the same as Listing 5.2, but with the delegates renamed with an EventHandler
suffix. ChatRoomServer
now has three event members defined in Lines 44–46, each one using one of the three delegate types. ChatRoomClient
adds two delegates to the DisplayMessage
event (Lines 27–30) and removes them when it’s finished listening to the events (Lines 34–37). Line 52, by invoking the delegate, causes the event to be raised so both ConsoleMessage
and AnnoyingMessage
are executed.
In Visual Basic .NET, hooking and unhooking an event handler to an event is done using AddHandler
and RemoveHandler
statements, respectively, rather than +=
and -=
. Raising an event must be done with a RaiseEvent
statement. Listing 5.5 demonstrates this by updating Listing 5.4 to Visual Basic .NET.
Listing 5.5. Using Events to Provide Callback Functionality in Visual Basic .NET
The combination of delegate-less event definitions in Lines 28–30 and delegate-less event hookup using AddHandler
and RemoveHandler
in Lines 17–18 and 22–23 means that Visual Basic .NET programmers often don’t need to be aware of the existence of delegates to use events. The compiler hides all of the underlying delegate details. The RaiseEvent
statement in Line 35 handles the null check before invoking the event’s delegate, which had to be performed explicitly in C#. So if the DisplayMessage
event had no handlers attached, Line 35 would have no effect.
As with Visual Basic 6, Visual Basic .NET enables the use of a WithEvents
keyword to make the use of events even easier than in Listing 5.5. Listing 5.6 demonstrates how to use WithEvents
in Visual Basic .NET.
Listing 5.6. Using the WithEvents
Shortcut in Visual Basic .NET
When you declare a class or module variable using WithEvents
(Line 4), any of the class’s or module’s event handler methods are automatically added and removed to the instance when appropriate. Event handler methods are identified with the Handles
keyword, seen in Lines 13 and 18. The Handles
keyword in Line 13 states that the ConsoleMessage
method is an event handler for the server
object’s DisplayMessage
event. Similarly, the Handles
keyword in Line 18 states that AnnoyingMessage
is also an event handler for the server
object’s DisplayMessage
event. The appropriate AddHandler
and RemoveHandler
functionality is handled automatically by the compiler.
In COM, three callback mechanisms are analogous to the .NET mechanisms:
• Passing an interface instance
• Passing a function pointer
• Using connection points (events in Visual Basic 6)
Using callback interfaces in COM is the exact same idea as using callback interfaces in .NET. It tends to be a fairly common pattern, especially for C++-authored COM components, because using connection points adds complexity for both ends of the communication channel. Chapter 6, “Advanced Topics for Using COM Components,” has an example of a DirectX COM object that uses a callback interface. Function pointers are a precursor to delegates. Because they aren’t commonly used in COM and aren’t important for this chapter, there’s no need to cover them any further. Using unmanaged function pointers in managed code is covered in Chapter 19, “Deeper Into PInvoke and Useful Examples.”
Connection points are the COM equivalent of .NET events. Connection points are really just a general protocol for setting up and using interface callbacks. The generic nature of the protocol enables tools like Visual Basic 6 to abstract it inside event-like syntax. Notice the difference here—.NET events are based on delegates for method-level granularity whereas COM events are really based on interfaces (collections of methods).
The terminology for describing connection points in COM is notoriously confusing, so here’s an attempt to sort out the terms before describing the interfaces and protocol:
• The object causing an event to be raised is called the source, also known as a connectable object. This was the ChatRoomServer
class in Listings 5.4, 5.5, and 5.6. This is clearer than calling it something like a “server” because bi-directional communication causes clients to sometimes be servers and servers to sometimes be clients!
• The object handling (“listening to”) events is called the sink. This was the Display
class in Listings 5.4 and 5.5 because it contained the implementation that was being called from the source.
• The callback interface that is implemented by the sink and called by the source is called a source interface, or sometimes called an outgoing interface. This was the IChatRoomDisplay
interface in Listing 5.1.
• For each source interface that the source calls back upon, the object doing the actual callbacks is called a connection point. Every connection point corresponds exactly to one source interface.
• A sink object being attached to a connection point (from passing it a callback interface) is considered a connection.
The source interface is really the focus of the connection point protocol, and is nothing more than a callback interface. To Visual Basic 6 programs, source interfaces appear to behave like a collection of events. Through a series of initialization steps, the sink (or an object using the sink) passes the sink-implemented interface pointer to the source so it can call back on the interface to simulate events. The part that often confuses people is that the source coclass usually lists source interfaces in its coclass statement marked with the IDL [source]
attribute. For example, a coclass representing the ChatRoomServer
class from Listing 5.1 might look like the following:
[
uuid(0002DF01-0000-0000-C000-000000000046)
]
coclass ChatRoomServer {
[default] interface IChatRoomServer;
[default, source] dispinterface IChatRoomDisplay;
};
This does not mean that the ChatRoomServer
class implements IChatRoomDisplay
; it doesn’t. Instead, it’s simply expressing that it supports calling back members of the IChatRoomDisplay
interface when an instance is passed to it using the connection points protocol. Listing source interfaces is the way for a COM class to advertise to type library consumers such as Visual Basic 6 that the class “has events.” This is analogous to .NET classes defining public event members. In any coclass’s list of source interfaces, one is always considered the default, just as with implemented interfaces. The default source interface is the only one accessible as a collection of events in Visual Basic 6 programs.
At run time, the [source]
markings in a type library are meaningless; COM classes advertise that they support events by implementing an interface called IConnectionPointContainer
and by returning connection point objects via this interface. This interface has two methods—EnumConnectionPoints
and FindConnectionPoint
.
EnumConnectionPoints
can be called to discover all the connection points and their source interfaces supported by the object. This method returns an IEnumConnectionPoints
interface, which provides methods to enumerate over the collection of connection points, each of which is represented as an IConnectionPoint
interface.
FindConnectionPoint
enables a client to ask for a specific connection point identified by an IID of a source interface. This is extremely similar to QueryInterface
, but in this case a client asks what source interfaces the object calls rather than what interfaces the object implements. If successful, an IConnectionPoint
interface is returned.
Connection point objects (which implement IConnectionPoint
) are provided by the connectable object, representing a collection of events. Just as a .NET event has add
and remove
accessors that enable clients to hook and unhook event handler delegates, IConnectionPoint
has Advise
and Unadvise
methods that enable clients to hook and unhook a callback interface pointer as follows:
• A client calls Advise
with an IUnknown
pointer to itself, then Advise
passes back an integral “cookie” that uniquely identifies the connection. The callback object being passed to Advise
must implement the appropriate source interface.
• When the client is finished listening to events, it can call Unadvise
with the cookie value obtained from Advise
to notify the connectable object.
IConnectionPoint
has a few additional methods that enable enumerating of connections, obtaining the IID of a connection point’s source interface, or obtaining a connection point’s container, but they aren’t important for this discussion.
Figure 5.1 illustrates the steps of the connection points protocol in a typical scenario. The sink object is traditionally the client object that initiates the connections, waits to receive event notifications, then terminates the connections when finished. The first four steps comprise the initialization phase, similar to hooking up event handlers in .NET. Step 5 represents the period of time when any “events are raised”—the source object calls back on the sink object’s source interface whenever appropriate. Finally, Step 6 terminates the connection. In this illustration, the object that sets up the connection point communication is the sink object, but often this client might contain one or more sink objects, much like the source object contains one or more connection points.
Figure 5.1. Connection points summarized.
The connection point infrastructure doesn’t prevent multicasting of events, but this needs to be managed manually by the connectable objects, just as it could be done with callback interfaces. For example, a connectable object could enumerate through its list of connections and call each sink object’s method one-by-one.
Now that we’ve covered how callback functionality commonly appears in .NET and COM applications, it’s time to examine how to handle callbacks from COM in .NET applications. As mentioned in the previous section, handling unmanaged callback interfaces and function pointers in managed code is demonstrated in Chapters 6 and 19. In addition, the details of implementing COM interfaces in managed code are covered in Chapter 14, “Implementing COM Interfaces for Binary Compatibility,” because this falls under the realm of writing .NET components for COM “clients.” So let’s move on to connection points.
First, we’ll briefly look at the raw approach of using connection points in managed code. This approach works, but it doesn’t leverage any of COM Interoperability’s event-specific support. Then, we’ll look at the transformations made by the type library importer related to events, and the behavior they enable.
Suppose that you want to use the InternetExplorer
coclass defined in the Microsoft Internet Controls type library (SHDOCVW.DLL
), which is defined as follows:
[
uuid(0002DF01-0000-0000-C000-000000000046),
helpstring("Internet Explorer Application.")
]
coclass InternetExplorer {
[default] interface IWebBrowser2;
interface IWebBrowserApp;
[default, source] dispinterface DWebBrowserEvents2;
[source] dispinterface DWebBrowserEvents;
};
Also suppose that you want to handle “events” raised from its default source interface. Following the steps explained in the last section, you could interact with connection points the same way you would in unmanaged C++ code. The .NET Framework provides .NET definitions of all the connection point interfaces in the System.Runtime.InteropServices
namespace, although renamed with a UCOM
prefix (which stands for unmanaged COM). Listing 5.7 demonstrates how this could be done in C#.
The code in Listing 5.7 is not the recommended way to handle COM events in managed code, because it doesn’t take advantage of built-in event-specific Interop support discussed in the “Type Library Importer Transformations” section. When you have an Interop Assembly with these transformations, you can use events in a more natural fashion.
Listing 5.7. Using Raw Connection Points in C# to Handle Internet Explorer Events
A Primary Interop Assembly (PIA) for SHDOCVW.DLL
does not exist at the time of writing, so this listing references a standard Interop Assembly, which can be generated by running the following from a command prompt:
tlbimp shdocvw.dll /namespace:SHDocVw /out:Interop.SHDocVw.dll
The BrowserListener
class implements the DWebBrowserEvents2
source interface because it’s acting as a sink object that will be passed to the connection point. Therefore, all of the source interface’s methods must be implemented in Lines 38–64. For brevity, only four such methods are shown, but the complete source code is available on this book’s Web site. Each method implementation prints the “event” name to the console plus some additional information, if applicable, passed in as parameters to the method.
The constructor in Lines 10–28 instantiates the InternetExplorer
coclass then does the connection point initialization described in the previous section. First, it obtains a reference to the InternetExplorer
object’s IConnectionPointContainer
interface by casting the object to UCOMIConnectionPointContainer
in Line 15. Line 19 retrieves the IID for the source interface corresponding to the desired connection point, and Line 20 calls FindConnectionPoint
to retrieve an IConnectionPoint
instance for the IID.
System.Type
’s GUID
property is a handy way to obtain the GUID for any COM type with a metadata definition. An instance of the object isn’t needed because you can obtain the desired type object by using typeof
in C#, GetType
in Visual Basic .NET, or __typeof
in C++.
Line 23 calls UCOMIConnectionPoint.Advise
, which sends a reference to the sink object to the connection point and gets a cookie in return. The cookie is used when calling Unadvise
in the finalizer on Line 33 or in the OnQuit
event handler on Line 53. Line 54 sets the cookie value to -1 so Unadvise
isn’t called twice when the user closes Internet Explorer before closing our example application.
The result of running the program in Listing 5.7 is shown in Figure 5.2. The console window logs all events occurring from the user’s actions inside the launched Internet Explorer window.
Figure 5.2. Running the program in Listing 5.7.
That’s all there is to manually performing event handling using connection points in managed code. Of course, the previous listing undoubtedly seems unacceptable to Visual Basic 6 programmers. This approach has the following problems:
• It’s obvious that we’re dealing with a COM object, because the connection point protocol is foreign to .NET components.
• Using connection points isn’t as easy as the event abstraction provided by Visual Basic 6.
• All of the source interface methods had to be implemented, even if we only cared about a handful of the events.
To solve these problems, the type library importer produces several types to expose connection points as standard .NET events. These types are discussed in the next section.
To expose connection points as .NET events, the type library importer does a lot of extra work besides the usual transformations. Every time the type library importer encounters an interface listed with the [source]
attribute, it creates some additional types:
• SourceInterfaceName
_Event
—An interface just like the source interface but with .NET event members instead of plain methods. Each event is named the same as its corresponding method on the source interface and has a delegate type with the same signature as its corresponding source interface method. This is commonly referred to as an event interface. Such an interface’s name typically looks unusual because source interfaces usually have an Events
suffix, resulting in a name with an Events_Event
suffix.
• SourceInterfaceName
_
MethodName
EventHandler
—A delegate corresponding to a single method on the source interface. The delegate has the same signature as the source interface’s corresponding method. Such a delegate is generated for every source interface method.
• SourceInterfaceName
_EventProvider
—A private class that implements the SourceInterfaceName
_Event
interface, handling the interaction with the connection point inside the events’ implementation.
• SourceInterfaceName
_SinkHelper
—A private sink class that implements the source interface. These objects are passed to the COM object’s IConnectionPoint.Advise
method and receive the callbacks, as in Listing 5.7.
The private event provider class obtains the COM object’s connection point container and the appropriate connection point, and the private sink helper class implements the source interface, so managed code that uses the importer-generated events is insulated from any connection point interaction. These types work the same way even if multiple coclasses share the same source interface(s). The sink helper effectively transforms an entire interface into independent methods that can be selectively used on a method-by-method basis. Visual Basic 6 provides a similar abstraction with its dynamic sink object, discussed in Chapter 13, “Exposing .NET Events to COM Clients.”
To help make using the events as seamless as possible, an imported class and its coclass interface are also affected when a coclass lists at least one source interface in its type library. The .NET class type (such as InternetExplorerClass
for the previous example) implements the event interface for each one of the coclass’s source interfaces. As with its regular implemented interfaces, any name conflicts caused by multiple source interfaces with same-named members are handled by renaming conflicting members to InterfaceName
_
MemberName
. Unfortunately, in this case InterfaceName
corresponds to the importer-generated event interface name and not the original source interface. So, in the case of a name conflict, the event gets the odd-looking name SourceInterfaceName
_Event_
SourceInterfaceMethodName
.
Name conflicts in classes that support multiple source interfaces can be quite common, resulting in really long event member names. For example, it’s a common practice to implement multiple source interfaces where one interface is a later version of another, duplicating all of its methods and adding a few more. The InternetExplorer
coclass does this with its DWebBrowserEvents2
and DWebBrowserEvents
source interfaces (although each interface has members that the other does not). This results in the .NET InternetExplorerClass
type having event members such as StatusTextChange
and DWebBrowserEvents_Event_StatusTextChange
.
Another common example of name conflicts occurs between methods and events. It’s common to have a Quit
method and a Quit
event, for instance. For these conflicts, the .NET method gets the original name and the .NET event gets the decorated name such as DWebBrowserEvents_Event_Quit
.
Fortunately, C# and Visual Basic .NET programs usually don’t use the importer-generated class types directly due to the abstraction provided for coclass interfaces. These renamed members are mostly noticeable for C++ programmers or for users of an object browser.
The coclass interface (such as InternetExplorer
, in the previous example) derives from the event interface corresponding to the default source interface, but no others. This is consistent with the fact that a coclass interface only derives from its default interface and no other interfaces implemented by the original coclass. Therefore, the event members can be used directly on these types.
In version 1.0 of the CLR, the type library importer doesn’t properly handle type libraries containing a class that lists a source interface defined in a separate type library. One workaround is to edit the type library by extracting an IDL file using a tool like OLEVIEW.EXE
, modifying the IDL, then compiling a new type library to import using MIDL.EXE
. In IDL, you could either omit the source interface from the coclass’s interface list, or you could redefine the interface in the same type library.
Now that you’ve seen all the extra types that the type library importer creates for connectable COM objects, it’s time to use them. Listing 5.8 is an update to the C# code in Listing 5.7, using the recommended .NET event abstraction rather than dealing with connection point interfaces.
Listing 5.8. Using .NET Events in C# to Handle Internet Explorer Events
Notice that the System.Runtime.InteropServices
namespace is not needed in this listing. That’s usually a good sign that the use of COM Interoperability is seamless in the example. Lines 13–24 hook up the class’s event handlers to only the events we desire to handle. This is in contrast to Listing 15.7, in which we needed to implement every method of the DWebBrowserEvents2
source interface. The trickiest thing about handling COM events is knowing the names of the corresponding delegates (although Visual Studio .NET’s IntelliSense solves this problem) because, for example, Visual Basic 6 programmers moving to C# are likely being exposed to the source interface names for the first time. Visual Basic .NET helps a great deal in this regard because the programmer doesn’t need to know about the delegate names.
Whereas Lines 13–18 attach event handlers to events that correspond to the default source interface (DWebBrowserEvents2
), Lines 21–24 attach event handlers to events that correspond to the second source interface (DWebBrowserEvents
). Because the InternetExplorer
coclass interface only derives from the default event interface (DWebBrowserEvents2_Event
), it’s necessary to explicitly cast the variable to the other event interface implemented by the class (DWebBrowserEvents_Event
). If this approach doesn’t appeal to you, the alternative is to declare the ie
variable as the class type instead of the coclass interface, so you can take advantage of its multiple interfaces without casting. For example, changing Lines 6–10 to:
6: private InternetExplorerClass ie;
7:
8: public BrowserListener()
9: {
10: ie = new InternetExplorerClass();
means that Lines 21–24 could be changed to:
21: ie.WindowResize += new
22: DWebBrowserEvents_WindowResizeEventHandler(WindowResize);
23: ie.DWebBrowserEvents_Event_Quit += new
24: DWebBrowserEvents_QuitEventHandler(Quit);
The WindowResize
event can now be handled without casting, but the drawback to using the class type is that member names might be renamed to avoid conflicts with member from other interfaces. In this case, the InternetExplorer
class already has a Quit
method, so the Quit
event must be prefixed with its event interface name.
Hooking up event handlers to events corresponding to non-default source interfaces is pretty easy when you consider what would need to be done in Listing 5.7 to achieve the same effect using the raw approach. Besides implementing the DWebBrowserEvents2
source interface and its 27 methods, the class would need to also implement the DWebBrowserEvents
source interface and its 17 methods, many of which are identical to DWebBrowserEvents
methods. Furthermore, the class would need to call FindConnectionPoint
for both source interfaces, call Advise
for both, store two cookie values, and call Unadvise
for both.
This listing doesn’t bother with unhooking the event handlers, because this is handled during finalization and there’s no compelling reason to unhook them earlier. To see exactly how the importer-generated types wrap connection point interaction, see Chapter 21, “Manually Defining COM Types in Source Code.”
When providing event support, the CLR always calls FindConnectionPoint
as late as possible; in other words, the first time a source interface’s method has a corresponding event to which a handler is being added. After that, subsequent event handler additions corresponding to the same source interface can be handled by the sink helper object without communicating with the COM connection point.
In Listing 5.8, Line 13 provokes a FindConnectionPoint
call for DWebBrowserEvents2
, and Line 21 provokes a FindConnectionPoint
call for DWebBrowserEvents
. This “lazy connection point initialization,” besides saving some work if all or some of an object’s connection points are never used, can be critical for COM objects requiring some sort of initialization before its connection points are used.
The need for extra initialization besides that which is done by instantiation (CoCreateInstance
) is not a common occurrence, but the COM-based Microsoft Telephony API (TAPI), introduced in Windows 2000, has an example of such an object. The Microsoft TAPI 3.0 Type Library (contained in TAPI3.DLL
in your Windows system directory) defines a TAPI
class with Initialize
and Shutdown
methods. Initialize
must be called after instantiating a TAPI
object but before calling any of its members. Similarly, Shutdown
must be the last member called on the object. The TAPI
class supports a source interface with a single method called Event
that represents all events raised by the object.
When using this TAPI
type in .NET, you must take care not to use any of its event members before calling its Initialize
method. This way, TAPI
’s IConnectionPointContainer. FindConnectionPoint
method won’t be called until after the object has been initialized. Attempting to hook up event handlers before the object is ready results in a non-intuitive exception thrown.
When using the Visual Basic .NET-specific WithEvents
and Handles
support to respond to events, you can’t take advantage of the lazy connection point initialization. This is because instantiating a WithEvents
variable in Visual Basic .NET effectively calls AddHandler
at that time to attach any methods that use the Handles
statement. Therefore, declaring a TAPI
type using WithEvents
in Visual Basic .NET and implementing an event handler for it causes instantiation to fail. The workaround for this is to manually call AddHandler
after Initialize
, rather than using the language’s WithEvents
support. This is demonstrated in Listing 5.9.
Listing 5.9. The Ordering of Event Hookup Can Sometimes Make a Difference
This listing uses the Microsoft TAPI 3.0 Type Library, which can be referenced in Visual Studio .NET or created by running TLBIMP.EXE
on TAPI3.DLL
. The first version of the code is the straightforward approach for Visual Basic .NET programmers, but it fails due to attempting to setup connection points before Initialize
is called. The second version of the code has the workaround—calling AddHandler
yourself after calling Initialize
and likewise calling RemoveHandler
before calling Shutdown
.
You’ve now seen that using event members on a COM class you instantiate is usually straightforward. If you’re using the coclass interface type, you can directly use any event members on the default source interface, or cast to one of the importer-generated event interfaces the class implements to use non-default event members. If you’re using the RCW class directly (such as InternetExplorerClass
), then all event members can be used directly without casting, although some of the event names may be prefixed with its source interface name if there are conflicts.
Sometimes you want to use event members on an object you didn’t instantiate, such as an object returned to you from a method call. There are four main possibilities for such a situation:
• Scenario 1—A .NET signature returns a coclass interface type that supports events. At run time, the returned instance is wrapped in the strongly-typed RCW with the specific class type (such as InternetExplorerClass
).
• Scenario 2—A .NET signature returns a coclass interface type that supports events. At run time, the returned instance is wrapped in the generic RCW (System.__ComObject
).
• Scenario 3—A .NET signature returns a regular interface type or System.Object
type, but you know that the instance returned will support events. At run time, the returned instance is wrapped in the strongly typed RCW with the specific class type (such as InternetExplorerClass
).
• Scenario 4—A .NET signature returns a regular interface type or System.Object
type, but you know that the instance returned will support events. At run time, the returned instance is wrapped in the generic RCW (System.__ComObject
).
The wrapping of returned COM objects is what differentiates scenario 1 versus scenario 2, and scenario 3 versus scenario 4. When the returned object is defined as the coclass interface type in the .NET signature, the returned object is always wrapped in the strongly typed RCW unless the same instance has previously been wrapped in with the generic System.__ComObject
type. When the returned object is defined as any other interface or System.Object
, the returned object is always wrapped in System.__ComObject
unless the object implements IProvideClassInfo
and its Interop Assembly has been registered.
In scenario 1, any members on the event interface corresponding to the class’s default source interface can be used directly. For example:
C#:
// GiveMeInternetExplorer returns an InternetExplorer coclass interface
InternetExplorer ie = obj.GiveMeInternetExplorer();
ie.DocumentComplete += new
DWebBrowserEvents2_DocumentCompleteEventHandler(DocumentComplete);
Visual Basic .NET:
' GiveMeInternetExplorer returns an InternetExplorer coclass interface
Dim ie As InternetExplorer = obj.GiveMeInternetExplorer()
AddHandler ie.DocumentComplete, AddressOf DocumentComplete
This can be done because the coclass interface contains all the events from the default source interface via inheritance.
Any members on event interfaces corresponding to non-default source interfaces can be obtained with a simple cast. For example:
C#:
// GiveMeInternetExplorer returns an InternetExplorer coclass interface
InternetExplorer ie = obj.GiveMeInternetExplorer();
((DWebBrowserEvents_Event)ie).WindowResize += new
DWebBrowserEvents_WindowResizeEventHandler(WindowResize);
Visual Basic .NET:
' GiveMeInternetExplorer returns an InternetExplorer coclass interface
Dim ie As InternetExplorer = obj.GiveMeInternetExplorer()
AddHandler CType(ie.DocumentComplete, DWebBrowserEvents_Event), _
AddressOf DocumentComplete
This casting should seem natural, because the strongly typed RCW implements the entire set of event interfaces corresponding to all of its source interfaces. Another option would be to cast to the instance’s class type (InternetExplorerClass
) and use all event members directly, but this isn’t recommended because it wouldn’t work for scenario 2 and it’s not always easy to know which scenario applies to your current situation.
Scenario 2 behaves just like scenario 1 from the programmer’s perspective, as long as you don’t attempt to cast the returned object to a class type. Any events corresponding to the default source interface can be used directly on the coclass interface type, and the returned object can be cast to any additional importer-generated event interfaces.
Scenarios 3 and 4 always require a cast because the type returned has no explicit relationship to an interface or class with event members. For example:
C#:
Visual Basic .NET:
Regardless of how you obtain a COM object that supports events, hooking up handlers to its event members is only a cast away (as long as a metadata definition of the event interface is available).
If you step back and think about scenarios 2 and 4, you might wonder how casting the System.__ComObject
instance to an event interface could possibly work. The metadata for the System.__ComObject
type does not claim to implement any interfaces, and calling the COM object’s QueryInterface
with a request for an event interface would fail because the COM object knows nothing about these .NET-specific interfaces.
The “magic” that makes the cast succeed is nothing other than a custom attribute. Every event interface created by the type library importer is marked with the ComEventInterfaceAttribute
custom attribute, which contains two Type
instances. The first Type
represents the .NET definition of the source interface to which the event interface belongs. The second Type
represents the event provider class that implements the event members.
When performing a cast from a COM object (an RCW) to an event interface, the CLR uses the information in this custom attribute to hook up all the pieces. As long as the COM object implements IConnectionPointContainer
and responds successfully to a FindConnectionPoint
call with the IID of the source interface listed in the ComEventInterfaceAttribute
custom attribute, the cast succeeds. Otherwise, the cast fails with an InvalidCastException
.
This event interface behavior is the area omitted from Figure 3.11 in Chapter 3, “The Essentials for Using COM in Managed Code.” Figure 5.3 updates this diagram with a full description of what happens when you attempt to cast an RCW to any type.
Figure 5.3. The process of casting a COM object (Runtime-Callable Wrapper): The full story.
Listing 5.10 adds a twist to the previous examples of handling events from the InternetExplorer
type. Here, we attach event handlers to the object’s Document
property.
Listing 5.10. Hooking Up Event Handlers to Objects We Don’t Instantiate
The omitted parts of this example are the same as the code shown in Listing 5.8. Besides referencing an Interop Assembly for the Microsoft Internet Controls type library, this listing also references the Primary Interop Assembly for the Microsoft HTML Object Library (MSHTML.TLB
) for definitions of IHTMLEventObj
, HTMLDocumentEvents_Event
, and HTMLDocumentEvents2_Event
.
Lines 26–29 hook up event handlers to two of the events supported by the InternetExplorer.Document
property. Document
is defined as a generic System.Object
, but we know that the instance is always an HTMLDocument
type. (The property was likely defined as such to avoid a dependency on the large MSHTML type library.) Therefore, the property can be cast to any event interfaces implemented by the .NET HTMLDocumentClass
type. The onmouseover
event corresponds to the HTMLDocument
coclass’s default source interface (HTMLDocumentEvents
) whereas the onclick
event corresponds to a second source interface (HTMLDocumentEvents2
).
The listing could have cast the Document
property to the HTMLDocument
coclass interface, for example:
HTMLDocument doc = (HTMLDocument)ie.Document;
Ordinarily, this would enable the use of the default event interface’s event members directly but in this case it wouldn’t because the HTMLDocument
interface, via inheriting the coclass’s default DispHTMLDocument
interface, has properties with the same names as every event! To disambiguate between the onmouseover
property and the onmouseover
event, you’d need to cast to the HTMLDocumentEvents_Event
interface anyway.
This example doesn’t unhook its event handlers from the ie.Document
object, but it’s a good idea to do so as soon as you’re finished with the current document because this might occur well before garbage collection.
As discussed in the previous two chapters, the ActiveX importer produces its own classes that wrap coclasses representing ActiveX controls as Windows Forms controls. If an ActiveX Assembly didn’t also contain some extra transformations for events, then the AxHost
-derived wrapper classes that you can host in a Windows Forms control would not appear to have any events. Therefore, the ActiveX importer must clearly do some transformations as well. These transformations, and their use, are covered in this section.
Just as classes generated by the type library importer contain event members when the coclass lists an interface marked with the IDL [source]
attribute, classes generated by the ActiveX importer also contain event members. However, only event members corresponding to the default source interface are created. Any non-default source interfaces are ignored. Name conflicts are handled by appending an Event
suffix to applicable event names.
Besides the additions made to the AxHost
-derived classes (more of which are shown in the next listing), the ActiveX importer creates some additional types every time it encounters a coclass listing a source interface:
• SourceInterfaceName
_
MethodName
EventHandler
—A delegate, one for each method on the default source interface (excluding methods that have no parameters, which use the System.EventHandler
delegate). Unlike the delegates created by the type library importer, these signatures do not match the signatures of methods in the source interface. Instead, the delegate signature always has two parameters to (almost) match the convention used by all delegates in Windows Forms. The first parameter is a System.Object
type named sender
. The second parameter, named e
, is a type described next in the list.
• SourceInterfaceName
_
MethodName
Event
—A class with a public field representing each parameter of a source interface method. One of these classes exists for each method on the source interface that has one or more arguments. This is the second e
parameter used in each delegate signature, but fails to conform to .NET guidelines in two ways: the class does not derive from System.EventArgs
and it does not have an EventArgs
suffix.
• Ax
CoClassName
EventMulticaster
—A public sink class that implements the source interface. This serves a similar role as the sink helper class generated by the type library importer.
No event interfaces are generated because the Ax
CoClassName
class is always used directly. There’s also no separate event provider class because that functionality is merged into the Ax
CoClassName
class.
Listing 5.11 shows snippets of C# code inspired by the code obtained by running the ActiveX importer on the file containing the Microsoft Internet Controls type library, for example:
aximp C:WindowsSystem32shdocvw.dll /source
This type library contains the WebBrowser
control introduced in Chapter 3:
[
uuid(8856F961-340A-11D0-A96B-00C04FD705A2),
helpstring("WebBrowser Control"),
control
]
coclass WebBrowser {
[default] interface IWebBrowser2;
interface IWebBrowser;
[default, source] dispinterface DWebBrowserEvents2;
[source] dispinterface DWebBrowserEvents;
};
The WebBrowser
coclass has the same two source interfaces as the InternetExplorer
coclass used throughout this chapter, so you can easily compare how events are handled with imported ActiveX controls to how they are handled with plain imported coclasses.
Listing 5.11. Some of the Types and Members Generated by the ActiveX Importer for Event Support
The snippets of the AxWebBrowser
class shown focus on two events and their supporting types and members—DownloadBegin
and CommandStateChange
. Lines 10–12 define the two events. Because the DownloadBegin
source interface method has no parameters, the simple System.EventHandler
delegate is used rather than defining a new DWebBrowserEvents2_ DownloadBeginEventHandler
delegate with the exact same signature. The CommandStateChange
source interface method does have parameters, so a specific delegate type is used with this event.
The CreateSink
and DetachSink
methods in Lines 14–31 connect and disconnect the connection point for the object’s default source interface. CreateSink
is invoked when the control’s System.ComponentModel.ISupportInitialize.EndInit
implementation is called, as is done inside the Visual Studio .NET-generated InitializeComponent
method when a Windows Forms control is dragged onto a form in the designer. DetachSink
is invoked inside the control’s IDisposable.Dispose
implementation. Keep this in mind in case you’re using an ActiveX control that’s picky about when its default connection point is used (as in the TAPI example earlier in the chapter). If some custom initialization routine must be called first, you’d need to insert a call to it somewhere in-between the control’s instantiation and the call to EndInit
, which would unfortunately be inside the designer-generated InitializeComponent
method that you’re not supposed to touch.
The RaiseOn
... methods, one per event, are defined in Lines 33–44 so that the event multicaster class, defined later, has access to raising the events. Lines 48–62 contain the pair of delegate and quasi-EventArgs
class for the event that doesn’t use the standard System.EventArgs
delegate. Besides not having the EventArgs
suffix and not deriving from System.EventArgs
, the event argument classes generated by the ActiveX importer have another oddity that goes against .NET conventions—every field name is lowercase, even if the original parameters in the source interface method were uppercase (as were Command
and Enable
). The constructor in Lines 56–61 simply provides a convenient means for setting all of the class’s fields.
To minimize confusion when using types generated by the ActiveX importer and to conform to .NET guidelines, it might be a good idea to modify the types produced. This can easily be done using AXIMP.EXE
’s /source
option to generate C# source code for the ActiveX assembly. Before compiling the generated source, you can rename the ...Event
classes to ...EventArgs
classes, perhaps capitalize the public fields of these classes, and make them derive from System.EventArgs
. Another user-friendly change would be to rename the delegate types from SourceInterfaceName
_
MethodName
EventHandler
to simply MethodName
EventHandler
as long as the name doesn’t conflict with others.
When compiling source code generated from AXIMP.EXE
, you’ll need to reference the corresponding Interop Assembly, System.Windows.Forms.dll
, and System.dll
.
Finally, the AxWebBrowserEventMulticaster
class in Lines 64–87 is the event sink that implements the default source interface, receives the callbacks, and raises the .NET event to anyone who may be listening.
Besides renaming types, you can take advantage of ActiveX importer-generated source code to make changes that can add functionality. A good example of this would be to add the code necessary to handle non-default source interfaces just as the default source interface is currently handled.
To conclude this chapter, we’ll update the Web Browser example from Listing 3.4 in Chapter 3 with event support. We’ll not only fix the behavior of the Back
and Forward
buttons to be implemented the way the ActiveX control intended, but add a history list and a log of all events. The final product is pictured in Figure 5.4.
Figure 5.4. The event-enabled .NET Web browser.
Inside Visual Studio .NET, the easiest way to add event handlers to an event is to click on the Events
lightning bolt in the property browser, then double-click on any events you wish to handle. An empty method signature and the appropriate event hooking and unhooking code are then emitted for you. Figure 5.5 displays the events for the WebBrowser
control. When displayed in categorized mode, the events originating from COM can easily be identified because they fall under the Misc
category and have no description in the lower pane.
Figure 5.5. The Visual Studio .NET property browser showing events.
Listing 5.12 shows the important parts of the source code for the updated example. The full source code is available in C# and Visual Basic .NET on this book’s Web site.
Listing 5.12. Using Events on a Hosted ActiveX Control
The first difference between this listing and the corresponding listing in Chapter 3 is in Lines 24–38. This shows a sampling of some of the events being handled. This code is automatically generated by Visual Studio .NET when double-clicking on events in the property browser.
Lines 42–90 show all of the interesting events that do something other than add information to the log. The implementation of the CommandStateChange
event handler in Lines 42–59 first adds some information to the eventList
log, which is a ListView
control. Then, it toggles the state of either the Back
or Forward
button based on the information passed into the event. The NavigateComplete2
event handler in Lines 61–67 updates the TextBox
control with the current URL and adds it to the history list, a ListBox
control. Notice how the lowercase transformations done by the ActiveX importer produces a funny looking field called uRL
!
The ProgressChange
event handler in Lines 69–76 uses the passed-in information to control the form’s ProgressBar
control, and the StatusTextChange
event handler in Lines 78–83 updates the form’s StatusBar
control with the passed-in text. Finally, the TitleChange
event handler in Lines 85–90 updates the form’s caption with the title of the current Web page.
The updated toolBar1_ButtonClick
implementation now simply calls the methods corresponding to each button’s action. The calls to GoBack
and GoForward
no longer need to be wrapped inside exception handling because the user shouldn’t be able to click these buttons when there are no more pages in the list. (If an exception were to occur, it would be a problem that we’d want to know about.)
The goal of this chapter was to explain how to handle events raised by COM components (including ActiveX controls) when they use the connection points protocol. The complement to this chapter is Chapter 13, which covers the opposite direction in which a .NET component is the event source and a COM component is the event sink.
In either case, this type of bi-directional communication is sometimes called tightly coupled events. Although the event source has no knowledge of the event sinks that are listening, every event sink must have prior knowledge about the event source. In a loosely coupled events system, as in COM+, an event sink can receive event notifications from event sources that it’s not even aware of.