Chapter 5. Responding to COM Events

In This Chapter

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.

Callbacks in .NET

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

Callback Interfaces

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

Image

Image

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.

Delegates

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#

Image

Image

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

Image

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

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#

Image

Image

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

Image

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

Image

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.

Callbacks in COM

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.

Image

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.

Handling COM Events in Managed Code

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.

The Raw Approach

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

Caution

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

Image

Image

Image

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.

Tip

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.

Image

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.

Type Library Importer Transformations

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_MethodNameEventHandler—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.

Caution

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.

Caution

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.

Using the Event Abstraction

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

Image

Image

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

Lazy Connection Point Initialization

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

Image

Image

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.

Connectable Objects You Don’t Instantiate

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

Image

Visual Basic .NET:

Image

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.

Image

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

Image

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.

Handling ActiveX Control Events in Managed Code

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.

ActiveX Importer Transformations

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_MethodNameEventHandler—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_MethodNameEventA 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.

AxCoClassName 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 AxCoClassName class is always used directly. There’s also no separate event provider class because that functionality is merged into the AxCoClassName 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

Image

Image

Image

Image

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.

Tip

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_MethodNameEventHandler to simply MethodNameEventHandler 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.

Tip

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.

Using ActiveX Events

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.

Image

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.

Image

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

Image

Image

Image

Image

Image

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

Conclusion

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.

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

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