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

Tip for Supporting COM Events from a Managed Class : Use DispIds for Event Methods.

1. Introduction.

1.1 I had earlier published Supporting COM Events from a Managed Class. The aim of that article was to impart concise knowledge on how to support COM connection point protocol-based events from a managed class. A set of startup code was also provided for the reader to use as a template.

1.2 This write-up is one of several follow-up articles that are intended to provide additional supplementary tips and techniques useful for writing managed classes that support COM events.

1.3 The theme for the current tip : always use the DispIdAttribute for Event Methods.

2. The DispIdAttribute.

2.1 The DispIdAttribute is applicable to methods and properties of a class or interface.

2.2 It is particularly applicable to interfaces which are COM-visible and which are exported as either dispinterfaces or as IDispatch-based dual interfaces.

2.3 If the DispIdAttribute is not applied to a method or a property, the CLR will automatically assign unique values for them.

2.4 The problem with not assigning individual dispids to dispinterface-based event methods is that it can result in an exception being thrown when a source fires an event which is not implement by a sink.

2.5 This article will demonstrate this problem through an example Managed COM server and a VB6 client application.

3. Sample C# Class that Fires COM Events.

3.1 I have used the same C# COM Server which was developed in the original Supporting COM Events from a Managed Class write-up :

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();
            }
        }
    }
}

Recall that the COM server compiles to CSTestEventFiring.dll.

3.2 Now let’s say we comment out the DispIdAttribute assigned to ITestEvents.TestEvent01() :

    // Define a source interface for COM client objects
    // (e.g. VB6 objects) to implement.
    [ComVisible(true)]
	...
    public interface ITestEvents
    {
        // Declare the methods of the ITestEvents source interface.
        // Comment out the DispIdAttribute for TestEvent01().
        // [DispId(1)]
        void TestEvent01();
    }

3.3 When CSTestEventFiring.dll is COM-registered and a type library (CSTestEventFiring.tlb) produced, the following will be the IDL listing for ITestEvents :

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

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

Notice that despite the fact that the DispIdAttribute has been removed in the C# listing, the TestEvent01() method nevertheless has a dispatch id associated with it. This dispatch id has been automatically assigned by the Type Library Exporter.

4. Sample Visual Basic 6.0 Client.

4.1 We shall also be using the same VB6 client application used in the original article.

4.2 However we will make a small change to the code. We will comment out the TestEvent01 event handler :

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

'' Comment out the TestEvent01 event handler.
''Private Sub TestEventFiringObj_TestEvent01()
''  MsgBox "TestEvent01"
''End Sub

4.3 Note that the call to the TriggerEvent() method (in the click handler for “Trigger Event” button) is still used.

4.4 At runtime, when the “Trigger Event” button is clicked, an exception will be thrown and a message box showing this will be displayed :

4.5 If the DispIdAttribute was re-instated for ITestEvents.TestEvent01(), the exception will not occur even if the VB6 client application did not handle the TestEvent01 event.

4.6 A full analysis behind the cause of this exception will be given in the next section.

5. Analysis of Cause of Exception.

5.1 The exception is undoubtedly connected with the absence of the DispIdAttribute for an event method and the absence of a handler when the event is fired.

5.2 Furthermore, this problem occurs only with dispinterface-based event interfaces.

5.3 Let’s analyze the situation closely. Recall that an event source interface is to be implemented by a sink which is part of the client code. A dispinterface-based event sink is essentially a COM object that implements IDispatch and which executes the methods of the dispinterface late bound through IDispatch.

5.4 Hence when a C# COM server fires a dispinterface-based event, it does so (via the CLR) by using methods of the IDispatch interface.

5.5 If an event method has been docorated with the DispIdAttribute, the CLR will directly use IDispatch::Invoke() to call the event sink method using the known dispatch id which has been declared by the DispIdAttribute.

5.6 If the client sink does not implement the event method, its IDispatch::Invoke() implementation will determine how it reacts. For a VB6 application, the unimplemented event dispatch id is ignored and the call to IDispatch::Invoke() will go through without a hitch.

5.7 If the event method was not decorated with the DispIdAttribute, the CLR will first inquire the sink for the dispatch id of the event method using IDispatch::GetIDsOfNames().

5.8 If the event method is not implemented, IDispatch::GetIDsOfNames() will return DISP_E_UNKNOWNNAME to the caller.

5.9 And this is where the trouble sets in. The CLR regards this as a COMException and will throw it. This exception is thrown at the point where the event delegate is activated. In the case of our CSTestEventFiringClass class, it is where TestEvent01() is called :

// 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(); // Exception thrown here.
    }
}

5.10 Note that as long as a client application has connected with the dispinterface-based source interface of an instance of a COM-visible managed class, all event delegates associated with the source interface will be non-NULL. This is so even if the client application implements only a subset of the methods of the source interface.

5.11 Hence in the TriggerEvent() method, TestEvent01 will be non-NULL because the client application VBClient01.exe has signalled its intension to connect with the ITestEvents source through the use of the WithEvents keyword.

5.12 When the exception occurred inside TriggerEvent(), no exception handler is there to catch it. Hence the exception is forwarded to the VB application at the following point :

Private Sub Command_TriggerEvent_Click()
  TestEventFiringObj.TriggerEvent '' Exception will be pushed to here.
End Sub

5.13 A way to workaround this problem would be to install exception handlers either in the TriggerEvent() method, e.g. :

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.
    try
    {
        if (TestEvent01 != null)
        {
            TestEvent01();
        }
    }
    catch (COMException cex)
    {
	...
    }
}

or in the client application’s call to TriggerEvent(), e.g :

Private Sub Command_TriggerEvent_Click()
On Error GoTo ErrHandler
  TestEventFiringObj.TriggerEvent
  Exit Sub

ErrHandler:
  MsgBox "Error occurred in call to TriggerEvent()."
End Sub

6. In Conclusion.

6.1 Possible workarounds notwithstanding, in my opinion, it is best to always use the DispIdAttribute to assign dispatch ids to each and every method of a source interface.

6.2 I hope you have enjoyed this tip and have acquired a new sense of appreciation for the dispatch id.

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

2 thoughts on “Tip for Supporting COM Events from a Managed Class : Use DispIds for Event Methods.

  1. Thank your for your explanation. It was the exact problem I had to solve. Now I know what is going on under the surface. Thank you very much!

    Posted by Arby | May 13, 2014, 1:13 pm

Trackbacks/Pingbacks

  1. Pingback: Supporting COM Events from a Managed Class. « limbioliong - November 24, 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: