Home > Articles > Programming > Windows Programming

  • Print
  • + Share This
This chapter is from the book

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 7 demonstrates how this could be done in C#.

CAUTION

The code in Listing 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 7—Using Raw Connection Points in C# to Handle Internet Explorer Events

 1: using System;
 2: using SHDocVw;
 3: using System.Runtime.InteropServices;
 4:
 5: public class BrowserListener : DWebBrowserEvents2
 6: {
 7:  private UCOMIConnectionPoint icp; // The connection point
 8:  private int cookie = -1;     // The cookie for the connection
 9:
10:  public BrowserListener()
11:  {
12:   InternetExplorer ie = new InternetExplorer();
13:
14:   // Call QueryInterface for IConnectionPointContainer
15:   UCOMIConnectionPointContainer icpc = (UCOMIConnectionPointContainer)ie;
16:
17:   // Find the connection point for the
18:   // DWebBrowserEvents2 source interface
19:   Guid g = typeof(DWebBrowserEvents2).GUID;
20:   icpc.FindConnectionPoint(ref g, out icp);
21:
22:   // Pass a pointer to the host to the connection point
23:   icp.Advise(this, out cookie);
24:
25:   // Show the browser
26:   ie.Visible = true;
27:   ie.GoHome();
28:  }
29:
30:  ~BrowserListener()
31:  {
32:   // End the connection
33:   if (cookie != -1) icp.Unadvise(cookie);
34:  }
35:
36:  // Event handlers for all of the source interface's methods
37:
38:  public void DownloadBegin()
39:  {
40:   Console.WriteLine("DownloadBegin");
41:  }
42:
43:  public void NavigateComplete2(object pDisp, ref object URL)
44:  {
45:   Console.WriteLine("NavigateComplete2: " + URL);
46:  }
47:
48:  public void OnQuit()
49:  {
50:   Console.WriteLine("OnQuit");
51:
52:   // End the connection
53:   icp.Unadvise(cookie);
54:   cookie = -1;
55:
56:   Environment.Exit(0);
57:  }
58:
59:  public void OnStatusBar(bool StatusBar)
60:  {
61:   Console.WriteLine("OnStatusBar: " + StatusBar);
62:  }
63:
64:  ...
65:
66:  public static void Main()
67:  {
68:   BrowserListener host = new BrowserListener();
69:
70:   // Keep the program running while Internet Explorer is open
71:   Console.WriteLine("*** Press Enter to quit ***");
72:   Console.Read();
73:  }
74: }

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 and 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 7 is shown in Figure 2. The console window logs all events occurring from the user's actions inside the launched Internet Explorer window.

Figure 2 Running the program in Listing 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.

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

DIGGING DEEPER

The event provider and sink helper classes are the only types generated by the type library importer that contain a managed code implementation. Viewing these types using ILDASM.EXE, you can see the IL instructions that differentiate these types from the others.

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 8 is an update to the C# code in Listing 7, using the recommended .NET event abstraction rather than dealing with connection point interfaces.

Listing 8—Using .NET Events in C# to Handle Internet Explorer Events

 1: using System;
 2: using SHDocVw;
 3:
 4: public class BrowserListener
 5: {
 6:  private InternetExplorer ie;
 7:
 8:  public BrowserListener()
 9:  {
10:   ie = new InternetExplorer();
11:
12:   // Hook up event handlers to the events we care about
13:   ie.DocumentComplete += new
14:    DWebBrowserEvents2_DocumentCompleteEventHandler(DocumentComplete);
15:   ie.ProgressChange += new
16:    DWebBrowserEvents2_ProgressChangeEventHandler(ProgressChange);
17:   ie.TitleChange += new
18:    DWebBrowserEvents2_TitleChangeEventHandler(TitleChange);
19:
20:   // Events corresponding to the non-default source interface:
21:   ((DWebBrowserEvents_Event)ie).WindowResize += new
22:    DWebBrowserEvents_WindowResizeEventHandler(WindowResize);
23:   ((DWebBrowserEvents_Event)ie).Quit += new
24:    DWebBrowserEvents_QuitEventHandler(Quit);
25:
26:   // Show the browser
27:   ie.Visible = true;
28:   ie.GoHome();
29:  }
30:
31:  public void DocumentComplete(object pDisp, ref object URL)
32:  {
33:   Console.WriteLine("DocumentComplete: " + URL);
34:  }
35:
36:  public void ProgressChange(int Progress, int ProgressMax)
37:  {
38:   Console.WriteLine("ProgressChange: " + Progress +
39:    " out of " + ProgressMax);
40:  }
41:
42:  public void TitleChange(string Text)
43:  {
44:   Console.WriteLine("TitleChange: " + Text);
45:  }
46:
47:  public void WindowResize()
48:  {
49:   Console.WriteLine("WindowResize");
50:  }
51:
52:  public void Quit(ref bool Cancel)
53:  {
54:   Console.WriteLine("Quit");
55:   Environment.Exit(0);
56:  }
57:
58:  public static void Main()
59:  {
60:   BrowserListener listener = new BrowserListener();
61:
62:   // Keep the program running while Internet Explorer is open
63:   Console.WriteLine("*** Press Enter to quit ***");
64:   Console.Read();
65:  }
66: }

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

Listing 9—The Ordering of Event Hookup Can Sometimes Make a Difference

Event hookup on instantiation:

 1: Imports System
 2: Imports TAPI3Lib
 3:
 4: Module Module1
 5:
 6:  Private WithEvents t As TAPI
 7:
 8:  Sub Main()
 9:   t = New TAPI() ' Error! Connection points aren't ready!
10:   t.Initialize() ' Now they are but the hookup was already attempted
11:   ...
12:   t.Shutdown()
13:  End Sub
14:
15:  Public Sub t_Event(TapiEvent As TAPI_EVENT, pEvent As Object) _
16:   Handles t.Event
17:   Console.WriteLine("Handling event " + TapiEvent)
18:  End Sub
19: End Module

Lazy event hookup:

 1: Imports System
 2: Imports TAPI3Lib
 3:
 4: Module Module1
 5:
 6:  Private t As TAPI
 7:
 8:  Sub Main()
 9:   t = New TAPI()
10:   t.Initialize() ' Connection points are now ready
11:   AddHandler t.Event, AddressOf t_Event
12:   ...
13:   RemoveHandler t.Event, AddressOf t_Event
14:   t.Shutdown() ' The TAPI object is now "dead"
15:  End Sub
16:
17:  Public Sub t_Event(TapiEvent As TAPI_EVENT, pEvent As Object)
18:   Console.WriteLine("Handling event " + TapiEvent)
19:  End Sub
20: End Module

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.

DIGGING DEEPER

The code example suggesting that the InternetExplorer coclass interface is returned is used just for demonstration purposes. In reality, the importer alone never produces such a signature with coclass interface parameter replacement because the InternetExplorer coclass's default interface (IWebBrowser2) is listed as implemented by four coclasses in the Microsoft Internet Controls type library. For many other examples (or hand-tweaked Interop Assemblies), returning a coclass interface that supports events can be quite common.

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

// GiveMeABrowser returns an IWebBrowser2 interface or System.Object
InternetExplorer ie = (InternetExplorer)obj.GiveMeABrowser();
// Use an event corresponding to the default source interface
ie.DocumentComplete += new
 DWebBrowserEvents2_DocumentCompleteEventHandler(DocumentComplete);
// Use an event corresponding to a non-default source interface
((DWebBrowserEvents_Event)ie).WindowResize += new
 DWebBrowserEvents_WindowResizeEventHandler(WindowResize);

Visual Basic .NET:

' GiveMeABrowser returns an InternetExplorer coclass interface
Dim ie As InternetExplorer = CType(obj.GiveMeABrowser(), InternetExplorer)
' Use an event corresponding to the default source interface
AddHandler ie.DocumentComplete, AddressOf DocumentComplete
' Use an event corresponding to a non-default source interface
AddHandler CType(ie.DocumentComplete, DWebBrowserEvents_Event), _
 AddressOf DocumentComplete

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

FAQ: What can I do if the COM object I want to use raises events but doesn't list the appropriate source interface in its coclass definition?

If the COM object you're using has omitted this information from its type library, then the type library importer doesn't know that it should create the event-related types. Without these types, you can always fall back to the raw connection points method demonstrated in Listing 7. Otherwise, the easiest thing might be editing the type library by using OLEVIEW.EXE to extract IDL, adding the source interface, then compiling a new type library to import using MIDL.EXE. Or, refer to Chapter 21, which demonstrates how to perform the event transformations done by the type library importer in any .NET language.

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 3 updates this diagram with a full description of what happens when you attempt to cast an RCW to any type.

Figure 3 The process of casting a COM object (Runtime-Callable Wrapper): The full story.

Listing 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 10—Hooking Up Event Handlers to Objects We Don't Instantiate

 1: using System;
 2: using SHDocVw;
 3: using mshtml;
 4:
 5: public class WebBrowserHost
 6: {
 7:  private InternetExplorer ie;
 8:
 9:  ...
10:
11:  public void Document_MouseOver()
12:  {
13:   Console.WriteLine("MouseOver");
14:  }
15:
16:  public bool Document_Click(IHTMLEventObj pEvtObj)
17:  {
18:   Console.WriteLine("Click: " + pEvtObj.x + ", " + pEvtObj.y);
19:   return true;
20:  }
21:
22:  public void DocumentComplete(object pDisp, ref object URL)
23:  {
24:   Console.WriteLine("DocumentComplete");
25:
26:   ((HTMLDocumentEvents_Event)ie.Document).onmouseover += new
27:    HTMLDocumentEvents_onmouseoverEventHandler(Document_MouseOver);
28:   ((HTMLDocumentEvents2_Event)ie.Document).onclick += new
29:    HTMLDocumentEvents_onclickEventHandler(Document_Click);
30:  }
31:  ...
32: }

The omitted parts of this example are the same as the code shown in Listing 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.

  • + Share This
  • 🔖 Save To Your Account