//
you're reading...
COM Events, Programming Issues/Tips

Supporting COM Events from a Managed Class.

1. Introduction.

1.1 A COM-visible managed class can support COM events just like any COM coclass.

1.2 In this article, I aim to briefly expound on the .NET and COM event models and then zoom in on a working example which I hope will serve as a template for more complex systems,

1.3 In subsequent blog entries, I will also follow up with various tips and suggestions to help the newbie gradually develop greater skill and knowledge in this field.

2. COM and .NET Events.

2.1 COM events are effected through the Connection Point Protocol (see IConnectionPoint and IConnectionPointContainer for more information).

2.2 The .NET Framework event model is primarily based on the delegate (see Events and Delegates for more details).

2.3 Although the objectives of the above 2 event handling mechanisms are fundamentally the same, they are mutually exclusive systems.

2.3 One way to support the COM event system is to manually implement the IConnectionPointContainer and the IConnectionPoint interfaces for a managed class. This is certainly possible albeit tedious.

2.4 It is far more convenient to use the built-in CLR support for COM event sourcing through the use of the ComSourceInterfacesAttribute. This is the normative way to bridge the 2 event systems.

2.5 In essence, the managed class that will serve as the COM event source must be decorated with the ComSourceInterfacesAttribute.

2.6 Furthermore, in order to use the ComSourceInterfacesAttribute, a COM-visible interface that serves as the event interface must be set as the constructor argument for this attribute.

2.7 More implementation details will be provided in section 3 below.

3. Sample C# COM Server.

3.1 Listed below is a full code listing of a COM-visible C# class that supports a COM source interface :

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Runtime.InteropServices;

namespace CSTestEventFiring
{
    // Define a source interface for COM client objects
    // (e.g. VB6 objects) to implement.
    [ComVisible(true)]
    [Guid("76BBC602-9CBD-40b4-A210-CBB844E7AA70")]
    // The ComInterfaceType.InterfaceIsIDispatch argument
    // for the InterfaceTypeAttribute is important especially
    // for VB6 clients.
    [InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]
    public interface ITestEvents
    {
        // Declare the methods of the ITestEvents source interface.
        [DispId(1)]
        void TestEvent01();
    }

    [ComVisible(true)]
    [Guid("581B3A54-55E0-4e07-8F37-5C20AAC47A25")]
    [ClassInterface(ClassInterfaceType.AutoDual)]
    // The ComSourceInterfacesAttribute is the key to
    // exposing COM events from a managed class.
    // Here, we indicate that the CSTestEventFiringClass
    // class will support the ITestEvents source interface.
    [ComSourceInterfaces(typeof(ITestEvents))]
    public class CSTestEventFiringClass
    {
        // A managed delegate must be declared in order
        // to trigger events in the first place.
        // It is the use of the ComSourceInterfacesAttribute
        // together with the ITestEvents argument that links
        // the delegate to the COM event.
        public delegate void TestEvent01_Delegate();
        // Note that the name and signature of each managed event
        // supported by this class must be the same as that
        // of the corresponding method of the source interface.
        //
        // For example, here, the TestEvent01 managed event
        // will correspond to ITestEvents.TestEvent01().
        // They both have the same name and both have the
        // same function return type and parameters.
        //
        // Tip : make sure that the event delegate TestEvent01
        // is declared as private or protected. This will prevent
        // the addition of two extra methods add_TestEvent01()
        // and remove_TestEvent01() from the class interface.
        private event TestEvent01_Delegate TestEvent01;

        // Defined a method to test trigger the TestEvent01 event.
        public void TriggerEvent()
        {
            // As long as a client application has connected
            // with the events of this class, the event delegate
            // TestEvent01 will be non-Null.
            if (TestEvent01 != null)
            {
                TestEvent01();
            }
        }
    }
}

The above code is part of a C# class library project. After successful compilation, CSTestEventFiring.dll will be produced.

The following is a summary of the code above :

On the ITestEvents Interface.

  • The ITestEvents interface serves as the COM event source interface.
  • Although defined in C#, it is nevertheless declared to be COM-visible (via the ComVisibleAttribute) which makes it eligible to be exported into a type library.
  • It is also declared to be a pure dispinterface (by the use of the InterfaceTypeAttribute with ComInterfaceType.InterfaceIsIDispatch as the constructor argument). This is especially important for Visual Basic 6 clients (and several others) due to the fact that VB6 supports only dispinterface-based source interfaces (more will be explained in point 4.1 below).
  • ITestEvents exposes only one very simple method TestEvent01() which takes no parameters and returns nothing.

On the CSTestEventFiringClass Class.

  • The CSTestEventFiringClass class is necessarily COM-visible.
  • It is decorated with the ClassInterfaceAttribute with ClassInterfaceType.AutoDual as the constructire argument. This makes CSTestEventFiringClass export a class interface that is IDispatch-based.
  • It is also decorated with the ComSourceInterfacesAttribute with the type for ITestEvents being the constructor argument.
  • This is the most significant declaration for CSTestEventFiringClass. When a type library is generated for the compiled class library, the coclass for CSTestEventFiringClass will be listed with ITestEvents being declared as the default source interface (see point 3.3 below).
  • A managed delegate TestEvent01_Delegate is declared inside the CSTestEventFiringClass class. An event object “TestEvent01”, based on the TestEvent01_Delegate delegate is also declared as a member of the class.
  • These are necessary declarations because .NET framework objects trigger events only through delegate-based managed events and nothing else.
  • The connection between a managed event and a COM source interface event are two-fold : the use of the ComSourceInterfacesAttribute (already explained) and the name and signature of the managed event object associated with the COM source interface (explained next).
  • The name of the event object determines which COM source event method it is bound to. The TestEvent01 event object is thus obviously bound to ITestEvents.TestEvent01().
  • The signature of the event object is the signature of the delegate on which it is based. TestEvent01 is based on TestEvent01_Delegate which has been delared to be parameter-less and returns void. This must match the signature of the COM source event method to which TestEvent01 is bound.
  • The TestEvent01 event object is bound to ITestEvents.TestEvent01() and their signatures match.
  • A single method TriggerEvent() is declared which can be called by the client to test fire the COM event.

3.2 After compilation, CSTestEventFiring.dll must be registered to COM via REGASM.EXE together with the use of the /tlb command line option. The /tlb option will produce for us a type library for the class library. The type library is an important tool for use by client applications.

3.3 The following is the IDL listing for the type library (CSTestEventFiring.tlb) thus produced :

// Generated .IDL file (by the OLE/COM Object Viewer)
//
// typelib filename: CSTestEventFiring.tlb

[
  uuid(B4DD8A32-AD45-48FB-933E-12FB215DD79C),
  version(1.0)
]
library CSTestEventFiring
{
    // TLib :     // TLib : mscorlib.dll : {BED7F4EA-1A96-11D2-8F08-00A0C9A6186D}
    importlib("mscorlib.tlb");
    // TLib : OLE Automation : {00020430-0000-0000-C000-000000000046}
    importlib("stdole2.tlb");

    // Forward declare all types defined in this typelib
    dispinterface ITestEvents;
    interface _CSTestEventFiringClass;

    [
      uuid(76BBC602-9CBD-40B4-A210-CBB844E7AA70),
      version(1.0),
        custom({0F21F359-AB84-41E8-9A78-36D110E6D2F9}, "CSTestEventFiring.ITestEvents")    

    ]
    dispinterface ITestEvents {
        properties:
        methods:
            [id(0x00000001)]
            void TestEvent01();
    };

    [
      uuid(581B3A54-55E0-4E07-8F37-5C20AAC47A25),
      version(1.0),
        custom({0F21F359-AB84-41E8-9A78-36D110E6D2F9}, "CSTestEventFiring.CSTestEventFiringClass")
    ]
    coclass CSTestEventFiringClass {
        [default] interface _CSTestEventFiringClass;
        interface _Object;
        [default, source] dispinterface ITestEvents;
    };

    [
      odl,
      uuid(05AD5023-8A62-3D6D-8B6D-77D4F3E3F2ED),
      hidden,
      dual,
      nonextensible,
      oleautomation,
        custom({0F21F359-AB84-41E8-9A78-36D110E6D2F9}, "CSTestEventFiring.CSTestEventFiringClass")    

    ]
    interface _CSTestEventFiringClass : IDispatch {
        [id(00000000), propget,
            custom({54FC8F55-38DE-4703-9C4E-250351302B1C}, "1")]
        HRESULT ToString([out, retval] BSTR* pRetVal);
        [id(0x60020001)]
        HRESULT Equals(
                        [in] VARIANT obj,
                        [out, retval] VARIANT_BOOL* pRetVal);
        [id(0x60020002)]
        HRESULT GetHashCode([out, retval] long* pRetVal);
        [id(0x60020003)]
        HRESULT GetType([out, retval] _Type** pRetVal);
        [id(0x60020004)]
        HRESULT TriggerEvent();
    };
};

Notice the declaration for the dispinterface ITestEvents, and that it has been listed as the default source interface for the coclass CSTestEventFiringClass.

4. Sample VB6 Client Code.

4.1 Listed below is a full code listing for a VB6 client application VBClient01.vbp :

Option Explicit
Private WithEvents TestEventFiringObj As CSTestEventFiringClass

Private Sub Command_TriggerEvent_Click()
  TestEventFiringObj.TriggerEvent
End Sub

Private Sub Form_Load()
  Set TestEventFiringObj = New CSTestEventFiringClass
End Sub

Private Sub TestEventFiringObj_TestEvent01()
  MsgBox "TestEvent01"
End Sub

The above code is part of a standard VB6 application that references the CSTestEventFiring.tlb. After successful compilation, VBClient01.exe will be produced.

Note the following significant points about the VB6 code above :

  • The presence of the “WithEvents” keyword causes VBClient01.exe, at runtime, to hookup to the default source interface of the CSTestEventFiringClass class.
  • This is done when TestEventFiringObj is instantiated (by the “New” statement) during Form_Load().
  • The low-level connection point protocol is used for this purpose :  VBClient01.exe will query the TestEventFiringObj object for an IConnectionPointContainer interface.
  • If successful, IConnectionPointContainer::FindConnectionPoint() is called to obtain the IConnectionPoint interface pointer associated with the ITestEvents source interface.
  • Thereafter, IConnectionPoint::Advise() is called to hookup the ITestEvents sink object of VBClient01.exe with the IConnectionPoint implementation of the CSTestEventFiringClass object.
  • The ITestEvents sink object of VBClient01.exe is a pure dispinterface-based object created dynamically at runtime. The VB6 compiler emits code for this purpose.
  • One advantage of being created on the fly is that it need not implement all the methods of the ITestEvents interface. Only those event methods that are actually handled in the VBClient01 source code will have actual implementation stubs.
  • For instance, we know that the TestEvent01() event is handled in VBClient01. Hence the VB6 compiler will emit code for the ITestEvents sink object such that there is a code stub for this event. If there is another event named TestEvent02() but is not handled, VB6 will not create any stub for it.
  • This is the advantage of using a pure dispinterface-based sink implementation.

4.2 At runtime, the following window will be displayed :

4.3 And when the “Trigger Event” button is clicked, a message box displaying “TestEvent01” will be displayed :

5. In Conclusion.

5.1 And there you have it. A concise treatment on supporting COM events from a managed class.

5.2 I hope the code in section 3 will serve as a useful start-up template.

5.3 For additional supplementary tips and useful programming techniques relevant to COM event sourcing from managed code, please refer to the following :

 

Advertisements

About Lim Bio Liong

I've been in software development for nearly 20 years specializing in C , COM and C#. It's truly an exicting time we live in, with so much resources at our disposal to gain and share knowledge. I hope my blog will serve a small part in this global knowledge sharing network. For many years now I've been deeply involved with C development work. However since circa 2010, my current work has required me to use more and more on C# with a particular focus on COM interop. I've also written several articles for CodeProject. However, in recent years I've concentrated my time more on helping others in the MSDN forums. Please feel free to leave a comment whenever you have any constructive criticism over any of my blog posts.

Discussion

9 thoughts on “Supporting COM Events from a Managed Class.

  1. It’s challenging to find knowledgeable folks on this topic, but you sound like you know what you’re talking about! Thanks

    Posted by Event models | November 22, 2011, 9:44 am
  2. Very thorough example, but it raises some questions. When I use your technique then build the .tlb using REGASM, it does not export the event interface that I defined and referenced with ComSourceInterfaces. When I use TLBEXP, that event interface does end up in the .tlb.

    Also, no matter which Type Library builder I use, I do not get the HRESULT ToString([out, retval] BSTR* pRetVal); and other default object fields.

    Do you have any idea why?

    Posted by Mike Leslie | May 30, 2014, 8:41 pm
    • Hello Mike,

      >> When I use your technique then build the .tlb using REGASM, it does not export the event interface that I defined and referenced with ComSourceInterfaces.
      The event interface must be declared as public and decorated with the ComVisibleAttribute. Have you ensured these criteria ?

      Posted by Lim Bio Liong | May 31, 2014, 10:17 am
    • Hello Mike,

      >> Also, no matter which Type Library builder I use, I do not get the HRESULT ToString([out, retval] BSTR* pRetVal); and other default object fields.

      I assume you are referring to the methods and fields of the managed System.Object class (which all managed objects inherit). In order to have these visible in your class’ interface, your managed class must be decorated with the ClassInterfaceAttribute with ClassInterfaceType.AutoDual as constructor parameter.

      Posted by Lim Bio Liong | May 31, 2014, 10:39 am
  3. Hi Bio Liong,

    Not sure if you by any chance have sample VC++ (MFC) client code? According to http://msdn.microsoft.com/en-us/library/1hee64c7(v=vs.110).aspx (excerpted below), seems C++ client code needs extra steps.

    Visual Basic hides most details of connection points; however, you must include a WithEvents directive in the client’s declaration of the server. In C++, client code must query for and call methods on IConnectionPointContainer and IConnectionPoint. The server implements both interfaces.

    Thanks and regards,
    Xiangjun

    Posted by LI XIANGJUN | December 2, 2014, 7:31 am
  4. This excellent post was a really great help for me. Thank you so much! Very well explained and carried out.

    Posted by Joe | December 11, 2014, 3:52 pm
  5. Your excellent article about implementing outgoing COM interfaces in .NET has been of tremendous help for my project. Thank you!

    Posted by Henri Manson | December 12, 2015, 5:20 pm

Trackbacks/Pingbacks

  1. Pingback: Handling .NET Events in Unmanaged Code using Event Accessors. « limbioliong - November 22, 2011

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: