This article was inspired by some work carried out by my friend Misha at http://blogs.msdn.com/mshneer/archive/2008/10/28/better-eventing-support-in-clr-4-0-using-nopia-support.aspx and his PDC talk relating to the upcoming .NET Framework 4.0 feature known as “no PIA”. This article does not use any .NET 4.0-specific features and is relevant to most if not all current versions of the framework and Common Language Runtime (CLR).
I would like to point out that I wrote at least two earlier versions of this article before I settled on what you see here—plus I’m sure I’ll be tweaking the text here a few more times for some considerable time to come. The first version lingered on my blog for all of three days before I realized that it was fundamentally flawed. The intention was to describe how all the accumulated knowledge in various articles on COM eventing strewn across the Internet was subtly wrong. This wasn’t to be. Most of the literature is quite valid, as long as one reads it correctly. Thus I learnt a great deal on the way and that’s what I’m going to share with you now. Much of it centres on IDispatch and I’m going to be concentrating on Excel for this, the first.
Excel as an event source
I’ll be automating Excel from managed code using its Primary Interop Assemblies. This is something I have been doing on a daily basis for quite some time now. One day, however, I woke up and decided that I wanted to truly understand what goes on under the covers when COM servers call back into clients to signal events. First I’ll discuss hooking into events using the standard .NET delegate approach. Then I’ll move on to the classic COM event model employing the IConnectionPointContainer and IConnectionPoint interfaces and how to do this from managed code.
The most common way to hook into events is using managed delegates and the event properties defined on the Primary Interop Assembly interfaces. This is straightforward and Sample 1 below provides a full example of how this is done:
The sequence of steps is as follows:
1. I create, and make visible, an instance of the Excel ApplicationClass class.
2. I add a new, empty workbook.
3. I subscribe to the BeforeClose event using the standard delegate += syntax.
4. I attempt to close the workbook which fires my BeforeClose event handler and pops up a message box allowing the user the opportunity to cancel the event or proceed.
This all works very well and is quite enough for most people’s requirements. I’m sure you’ve all seen this a thousand times before.
The classic COM connection point model
There are times when you’ll want to employ COM connection points while still consuming the Excel object model from a managed-code client. This is certainly a more advanced case and not something you’ll need to do very often. However, if you need fine-grained control over how events are hooked up or if, for whatever reason, you only have access to a subset of the types defined in the Primary Interop Assembly, this is something you’ll need to understand.
Following the standard approach detailed in many of the sources available on the web you might end up with something like Sample 2:
I explicitly declare my event sink class WorkbookEventSink as internal since I don’t want to expose it outside my assembly and implement Excel.WorkbookEvents on it. Unfortunately, this sample suffers from one main drawback: it doesn’t work. In fact, it throws an InvalidCastException at the point at which it calls Advise on the connection point.
Of course, I could’ve popped down the corridor and spoken to any one of my many colleagues and obtained the answer straight away but I wanted to find out for myself. Eventually it became clear that the exception is thrown due to a failure to query the WorkbookEventSink class for the IDispatch interface inside the CLR’s COM-callable wrapper (CCW). This is the crux of the matter: Excel invokes handlers in event sinks through late-bound calls on the IDispatch interface, as do all other Office applications as it turns out. This would have been obvious had I dared to look at the Excel type library where the event interfaces are attributed as dispatch interfaces. Thus, it turns out that the methods on the event interfaces are almost totally irrelevant: they merely serve as hints to the developer. In the case of Primary Interop Assemblies and .NET delegates, the C# or Visual Basic compiler will force the handlers to follow the expected signature through the “shape” of the delegates. For those wishing to create sinks and register them using IConnectionPoint this is not the case and it is up to the developer to implement the correct handlers himself.
Fortunately, the CLR provides built-in IDispatch functionality for CCWs wrapped managed classes. The interesting part is declaring the classes such that the appropriate IDispatch behaviour is implemented by the CLR for you. After some trial and error my WorkbookEventSink class ended up like that shown in Sample 3:
It required the following three changes:
1. The class needed to be marked as COM-visible with ComVisibleAttribute. Without this piece of metadata the CLR will not create a CCW for the managed class at runtime.
2. The class needed to be public, which is really just a requirement for COM visibility. What might not also be obvious is that if this is a nested class then all its enclosing types also need to be public.
3. The class needed to be marked with ClassInterfaceType.None to specify that no class interface should be generated for the class. This overrides the default behaviour of ClassInterfaceType.AutoDispatch. This, it turns out, is the only way to directly expose the class’s implementation of the Excel.WorkbookEvents interface through IDispatch to Excel. The default IDispatch implementation employed by the generated CCW does not include any information about the interfaces implemented by the class. At first this seemed counterintuitive: ClassInterfaceType.AutoDispatch seems to suggest that the class will provide some kind of IDispatch implementation thus fulfilling the requirements of the cast within IConnectionPoint.Advise. After rereading the literature it eventually made sense. ClassInterfaceType.None directs the CLR to generate an IDispatch implementation that considers the event interfaces and the dispatch IDs defined on them through instances of the DispIdAttribute attribute.
So, there you have it. This was not at all obvious to me but investigating it certainly taught me a thing or two. Next time I write on this subject I intend to address the main issue, or flaw, I have with Sample 3: specifically, the requirement that I declare my class public. I might want to avoid this for both aesthetic and technical reasons. Personally, the requirement that the type and all its enclosing types also have to be public is generally going to be too much for me to swallow for real, shipping code situations. Perhaps more significantly, in order to declare the type public, all the interfaces it implements have to be public also. Moreover, if the type subclasses another class, this class has to be public. In a future instalment I will demonstrate one way to sidestep these accessibility restrictions.