.NET Delegate Event Model vs COM Connection Points

WEBINAR: On-demand webcast

How to Boost Database Development Productivity on Linux, Docker, and Kubernetes with Microsoft SQL Server 2017 REGISTER >

Environment: .NET Framework SDK Beta 1, Visual C++ 6 SP4

Introduction :

In this article, we'll take a dip into how the Connection Points Event Handling mechanism in Classic COM Components can be used by .NET applications to receive event Notifications via the COM Interop. What's interesting here is that, .NET applications can continue to use the Delegate Event Model to subscribe to & receive event notifications.The COM Interop acts an adaptor that glues both these models together so that they interoperate seamlessly.You might also want to look up the article Understanding Classic COM interoperability with .NET applications for an introduction on how Classic COM components interoperate with the .NET framework.

How the Connection Point Event Handling mechanism in Classic COM maps to the Delegate event handling mechanism in .NET
Click here for larger image

Figure #1: How the Connection Point Event Handling mechanism in Classic COM maps to the
Delegate event handling mechanism in .NET

A primer on Connection Points :

The Connection Points event handling mechanism, as you know, is one of the primary enablers for bi-directional communication between your COM components and the consumers of your components.Just to jog your memory, I'll brief you a little bit on the event handling mechanism in Classic COM Components. Typically, COM components that support event notifications have what is called, an outgoing interface. The outgoing interface is used by the component to call into the client when a specific event has occured. Outgoing interfaces are marked with the [source] attribute in the coclass section of the component's IDL file. The [source] attribute in the IDL allows development tools and IDEs to parse the typelibrary to check to see if the object supports an outgoing interface. Consumers or clients of these components usually set up a sink object that implements this outgoing interface. An interface pointer to this sink object is passed by the consumer to the component. The component stashes away this interface pointer in typically something like a map that contains a list of outgoing interface pointers to sink objects that are interested in receiving notifications from the component. Whenever a component needs to raise an event, it uses the map to get a list of interface pointers to the sink objects that have subscribed for notifications. It then notifies them by calling the respective method on the outgoing interface that's implemented by the sink object.

Connection Points in Classic COM

Essentially, the COM object that supports outgoing interfaces, implements the IConnectionPointContainer interface. So, a client that wants to receive event notifications does a QI on the COM object for the IConnectionPointContainer interface to see if it supports outgoing interfaces. If the QI fails, then the object does not support events. If the QI succeeds, the client calls the FindConnectionPoint method (could also call EnumConnectionPoints) on the IConnectionPointContainer interface by passing it the IID of the outgoing interface. If such an interface is supported, the client receives back an IConnectionPoint interface pointer corresponding to the outgoing interface. It then calls the IConnectionPoint::Advise method and passes to it, the sink object's IUnknown pointer. The COM object adds this IUnknown pointer to its map to keep a list of the sink objects that have subscribed for notifications. The client gets back a cookie from the COM object that it can subsequently use to revoke event notifications. When the COM object needs to raise events, it iterates through the map, gets a list of all the sink object interface pointers and calls the corresponding event methods on the outgoing interface that's implemented by the sink object. When a client no longer desires to receive notifications, it removes itself from the object's map by calling the IConnectionPoint::Unadvise method by passing it the cookie that it received earlier on the IConnectionPoint::Advise call.

Connection Points in Classic COM
Figure #2: Connection Points in Classic COM

In simple terms, that's how the event handling mechanism & bi-directional communication works in Classic COM components. I'll try not to explain the Connection Points event-handling infrastructure in COM in any more detail. Most books that teach COM programming usually have a full chapter dedicated to explain this architecture and you might want to look them up to further your understanding in this topic.

Connection Points Vs Delegates :

Now, let's take a look at how the Connection Points event handling mechanism in COM translates to the delegate event handling mechanism in the .NET world. We'll take a look at how you can use .NET managed event sinks to catch event notifications sent from COM objects. To get started, let's first take a look at the COM object, that's going to source events to your .NET application. Let's put together a simple COM object that will page your .NET application whenever an airline arrives at a fictitious Airport called John Doe International Airport. We will subscribe to this paging service from our .NET application so that we get paged whenever an airplane taxies down on John Doe's runway.

AirlineArrivalPager Classes (Class View)

We'll create an ATL EXE project that hosts an object called AirlineArrivalPager . The AirlineArrivalPager object supports an incoming interface called IAirlineArrivalPager and an outgoing interface called _IAirlineArrivalPagerEvents. Here's the interface definition of the _IAirlineArrivalPagerEvents outgoing interface. This interface is marked with the [source]attribute in the coclass definition.

interface IAirlineArrivalPager : IDispatch
{
 [id(1), helpstring("method AddArrivalDetails")] 
 HRESULT AddArrivalDetails([in] BSTR bstrAirlineName, 
                           [in] BSTR bstrArrivalTerminal);
};
....

dispinterface _IAirlineArrivalPagerEvents
{
 properties:
 methods:
 [id(1), helpstring("method OnAirlineArrivedEvent")] 
 HRESULT OnAirlineArrivedEvent([in] BSTR bstrAirlineName, 
                               [in] BSTR bstrArrivalTerminal);
};

....

coclass AirlineArrivalPager
{
 [default] interface IAirlineArrivalPager;
 [default, source] dispinterface _IAirlineArrivalPagerEvents;
};

Take a look at the implementation of the incoming IAirlineArrivalPager interface's AddArrivalDetails method:

STDMETHODIMP CAirlineArrivalPager::AddArrivalDetails(BSTR bstrAirlineName,
                                                     BSTR bstrArrivalTerminal)
{
 // Notify all subscribers that an Airline 	
 // has hit the tarmac	
 Fire_OnAirlineArrivedEvent(bstrAirlineName, 
                            bstrArrivalTerminal);

 // Return the status to the caller
 return S_OK;
}

The implementation of this method uses the Fire_OnAirlineArrivedEvent helper method to notify all sink objects implementing _IAirlineArrivalPagerEvents that have subscribed for event notifications. The Fire_OnAirlineArrivedEvent is a method of the helper proxy class derived from IConnectionPointImpl that is generated automatically by the ATL Implement Connection point wizard. Essentially, it iterates through the map where it stored the interface pointers to the sink objects when IConnectionPoint::Advise was called and uses these interface pointers to call the event notification method (OnAirlineArrivedEvent) implemented by the client's sink object.

If you were a C++ programmer coding a COM aware client application to receive notifications, you would set up a sink object in the client application that implements the _IAirlineArrivalPagerEvents interface. You would then create the AirlineArrivalPager object and pass it the IUnknown interface pointer of the sink object through a call to IConnectionPoint::Advise or use a helper method such as AtlAdvise to wire your sink to the source object so that you can receive event notifications. With VB, it's as simple as using the WithEvents keyword in your declaration and defining a handler function for receiving notifications. VB will do all the hard work under the covers to wire the notifications made on the outgoing interface to the appropriate handler function. Event handling in .NET is primarily based on the Delegate Event model. A delegate is something akin to function pointers that we use in C/C++. The delegate based Event model was popularized by the simplicity of its use right from the WFC (Windows Foundation Classes in Visual J++) days. Delegates allow an event raised by any component to be connected to a handler function or method of any other component as long as the function signatures of the handler function or method matches the exact signature of that of the event being raised. Take a look at this simple example below that shows you how you can put delegates to action :

 // Here's the SayGoodMorning delegate
 delegate string SayGoodMorning();

 public class HelloWorld
 {
  public string SpeakEnglish() {
  return "Good Morning";
 }

 public string SpeakFrench() {
  return "Bonjour";
 }

 public static void Main(String[] args) {

  HelloWorld obj = new HelloWorld();

  // Associate the delegate with a method reference
  SayGoodMorning english = new SayGoodMorning(obj.SpeakEnglish);
  SayGoodMorning french = new SayGoodMorning(obj.SpeakFrench);

  // Prints the following :
  // Good Morning
  // Bonjour
  System.Console.WriteLine(english());
  System.Console.WriteLine(french());
 }

}/* end class */

In the example above, we declare a delegate called SayGoodMorning. Then we wire the delegate to reference the SpeakEnglish and SpeakFrench methods of the HelloWorld object. All that is required is that the SpeakEnglish and SpeakFrench methods have the same signature as that of the SayGoodMorning delegate. The reference is typically made by instantiating the delegate as if it were an object and passing in the referenced method as its parameter. This parameter could be an object's method or could be even be a static function. The delegate maintains the reference it needs to call the right handler for the event. So delegates are first class object-oriented citizens and are also type-safe and secure to deal with. The .NET event-handling model is based primarily on the delegate event model. Take a look at the following example:

// Create a button
WinForms.Button AngryButton = new WinForms.Button ();

// Add a delegate to the button's Click event
AngryButton.Click += new System.EventHandler(AngryButton_Click);

...

// Here's the handler function that the delegate references
protected void AngryButton_Click(object sender,EventArgs e)
{
 MessageBox.Show("Please Stop clicking me !!");
}

When your application deals with controls and wants to receive specific notifications, it creates a new instance of an EventHandler delegate that contains a reference to the actual handler function that will handle the events raised by the control. In the example shown above, the EventHandler delegate contains a reference to the AngryButton_Click method. The AngryButton_Click method needs to have the same method signature as that of the EventHandler delegate. Here's how the signature of the System.EventHandler delegate looks like:

public delegate void EventHandler(object sender, EventArgs e);

The EventHandler delegate instance will then have to be added to the control's Event list. When the control raises an event, all the delegates that have been added to the event list will be invoked and each delegate will route it to the correct handler function that it references. In our example,whenever the Click Event occurs in the button the call will be routed to the AngryButton_Click method. I guess this gives you a fairly good idea on how delegates play their role in the event-handling infrastructure in the .NET framework. The reason, I explained to you how delegates work is because it's one of the primary enablers of the .NET event handling model and it's important to understand this to appreciate how NET applications use delegates to subscribe to event notifications from Classic COM Components.

VB Client - Control Tower App

Here's a simple VB Client application that assumes the role of the Control Tower at John Doe International Airport and calls the AddArrivalDetails method in the incoming interface. The implementation of this method in turn triggers the event notifications that are subsequently caught by the handler functions in the .NET application that have subscribed for event notifications for the OnAirlineArrivedEvent event. The AirlineArrivalPage COM object is itself a singleton object hosted in an out-of-proc COM server. So the same instance of the object services both the VB based Control tower application (that triggers events) and the .NET pager applications that have subscribed for OnAirlineArrivedEvent event notifications.


Dim AirlinePager As New AIRLINENOTIFYLib.AirlineArrivalPager

Private Sub AirlineArrived_Click()
 AirlinePager.AddArrivalDetails Me.AirlineName, Me.ArrivalTerminal
End Sub

With that said, let's see how a .NET managed application receives event notifications generated by the AirlineArrivalPager COM object. Firstly, you need to generate a .NET metadata proxy from the COM objects' typelibrary so that it can be consumed by a .NET application. So let's use the Type Library Importer(TLBIMP) to generate the metadata assembly for us.

tlbimp AirlineNotify.tlb /out:AirlineNotifyMetadata.dll

This metadata proxy will be referenced in your .NET application. Here's a simple .NET WinForms application that subscribes to event notifications from the AirlineArrivalPager COM Component by using a delegate.

WinForms Pager Application

...
using AIRLINENOTIFYLib;

public class AirlineNotifyForm : System.WinForms.Form
{
 private System.WinForms.CheckBox checkBoxPaging;
 private System.WinForms.ListBox listPager;
 private AirlineArrivalPager m_pager = null;
 ...

 public AirlineNotifyForm() {
...
  // Subscribe to event notifications from
  // the AirlineArrivalPager component
  subscribePaging();
 }

 ...

 void subscribePaging() {
  // Create an AirlineArrivalPager object
  m_pager = new AirlineArrivalPager(); 

  // Add the delegate referencing the OnMyPagerNotify method
  // to the OnAirlineArrivedEvent event list (ICP::Advise)
  m_pager.OnAirlineArrivedEvent += 
  new _IAirlineArrivalPagerEvents_OnAirlineArrivedEventEventHandler(OnMyPagerNotify);

 }/* end subscribePaging */

 protected void checkBoxPaging_CheckedChanged (object sender, System.EventArgs e) {
  if(checkBoxPaging.Checked) {
   // If checked, register the delegate referencing OnMyPagerNotify
   // to the OnAirlineArrivedEvent event list (ICP::Advise)
   m_pager.OnAirlineArrivedEvent += 
   new _IAirlineArrivalPagerEvents_OnAirlineArrivedEventEventHandler(OnMyPagerNotify);
  }
  else {
   // If Unchecked, remove the delegate referencing OnMyPagerNotify
   // from the OnAirlineArrivedEvent event list (ICP::Unadvise)
   m_pager.OnAirlineArrivedEvent -= 
   new _IAirlineArrivalPagerEvents_OnAirlineArrivedEventEventHandler(OnMyPagerNotify);
  }
 }/* end checkBoxPaging_CheckedChanged */

 public void OnMyPagerNotify(String strAirline, String strTerminal) {
  StringBuilder strDetails = new StringBuilder("Airline ");
  strDetails.Append(strAirline);
  strDetails.Append(" has arrived in ");
  strDetails.Append(strTerminal);
  listPager.InsertItem(0,strDetails);
 }/* end OnMyPagerNotify */
}/* end class */


The line of code that is most important here is the line:

m_pager.OnAirlineArrivedEvent += 
          new _IAirlineArrivalPagerEvents_OnAirlineArrivedEventEventHandler(OnMyPagerNotify);

If you understand the semantics of how delegates work, you should have absolutely no problem comprehending what's going on here. What you're doing is adding the _IAirlineArrivalPagerEvents_OnAirlineArrivedEventEventHandler delegate to the OnAirlineArrivedEvent event list. The delegate references the OnMyPagerNotify method. Usually the name of the event (OnAirlineArrivedEvent) is the same as the method name in the outgoing interface. The delegate name (_IAirlineArrivalPagerEvents_OnAirlineArrivedEventEventHandler) usually follows the pattern InterfaceName_EventNameEventHandler. That's all there is to receiving event notifications from COM components. All you need to do is create an instance of the component and then add a delegate referencing your handler function to the event list. Effectively, what you are doing here is something that's analogous to the IConnectionPoint::Advise in the COM world. Whenever, the OnAirlineArrivedEvent event is raised by the COM component, the OnMyPagerNotify method will be called to handle the event notification. It's that simple in .NET, to wire a handler sink to receive event notifications from a COM object that sources events.

When you no longer want to receive notifications, you can remove the delegate from the event list by calling:

m_pager.OnAirlineArrivedEvent -=
 new _IAirlineArrivalPagerEvents_OnAirlineArrivedEventEventHandler(OnMyPagerNotify);

This is analogous to the IConnectionPoint::Unadvise method call that revokes further notifications by removing your sink object's interface pointer from the map using the cookie that you received in the Advise call.So who handles the mapping between the Connection point event handling model in Classic COM and delegate event model in .NET?. The metadata proxy generated by the Typelibrary importer(TLBIMP) contains classes that act as adaptors to wire the Connection point Event Model in the unmanaged world to the Delegate based event model in the .NET world via the RCW stub that is created at runtime. If you are interested in examining what happens under the hood, I encourage you to open up the metadata proxy (AirlineNotifyMetadata.dll) using the IL Disasembler (ILDASM) and examine the MSIL code for the various methods in the helper classes.

Conclusion :

I hope this article will get you started on how you can use your .NET applications to receive event notifications from Classic COM Components. From a programmer's perspective, you get to leverage the simplicity and ease of use of the .NET Delegate event model to receive event notifications from Classic COM Components via the COM Interop. Yet again, the COM Interop shields us from the nuts and bolts of having to wire these two models together.So there you go, yet another testimony to the fact that 'COM is not dead' and will continue to live in harmony with the über-powerful .NET world.

Downloads

Download demo project - 31 Kb


Comments

  • Are you sure you would use new with unregistering to events?

    Posted by Legacy on 12/17/2001 12:00am

    Originally posted by: Tony Juricic

    Great article!

    However, the code like:

    m_pager.OnAirlineArrivedEvent -=
    new _IAirlineArrivalPagerEvents_OnAirlineArrivedEventEventHandler(OnMyPagerNotify);

    looks pretty strange to me. Wouldn't you save object reference created when adding event handler with += and use it here to unregister *that* particular object, rather than creating a new one and unregistering it?

    Tony

    Reply
Leave a Comment
  • Your email address will not be published. All fields are required.

Top White Papers and Webcasts

  • As all sorts of data becomes available for storage, analysis and retrieval - so called 'Big Data' - there are potentially huge benefits, but equally huge challenges...
  • The agile organization needs knowledge to act on, quickly and effectively. Though many organizations are clamouring for "Big Data", not nearly as many know what to do with it...
  • Cloud-based integration solutions can be confusing. Adding to the confusion are the multiple ways IT departments can deliver such integration...

Most Popular Programming Stories

More for Developers

RSS Feeds

Thanks for your registration, follow us on our social networks to keep up-to-date