Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Receiving Events from late-bound COM servers

0.00/5 (No votes)
2 May 2005 1  
An article demonstrating how to receive events from a late-bound COM server using Microsoft Word as an example COM server.

Microsoft Office Word 2003

Introduction

(For the latest changes, please see the history section below)

The term "late binding" refers to a technique that calls COM methods and properties and discover the available methods at runtime through the "_blank" href="http://msdn.microsoft.com/library/en-us/automat/htm/chap5_78v9.asp">IDispatch interface rather than linking to the COM methods and properties at compile time, which is called "early binding".

Usually, I do use late binding in conjunction with Office automation when I do not want to target a special Microsoft Office version (e.g. Microsoft Office 2003 or Microsoft Office XP) but rather want to write code that works well with different versions of Microsoft Office. In such a scenario you cannot use "early binding". (i.e. linking to the COM methods and properties at compile time), because this would always target a single Microsoft Office version and might not work when the interfaces have changed between different Microsoft Office version.

This article focuses on late binding from a C# .NET application.

Whereas calling methods and properties are rather well documented, I wasted more than two full days searching how to get notified from a COM server about certain events. Therefore I wrote this article to document the solution I developed and to hopefully help some other developers to solve similar issues quicker than I did.

Using Late Binding to call a Method

It is rather well documented how to call methods and properties of a COM server from a .NET application. The following code snippet is an excerpt from the sample application attached to this article.

// The type of the Word application. Used for dynamically
// creating.
Type wordType = Type.GetTypeFromProgID( "Word.Application" );
 
// The main Word application object.
object wordApplication = Activator.CreateInstance( wordType );

You first need to get the type for the COM server to start and then create an instance of this type. This is similar to the "_blank" href="http://msdn.microsoft.com/library/en-us/script56/html/vsfctCreateObject.asp">CreateObject function in VBScript. In this example, I create an instance of Microsoft Office Word.

To call a method or property, you must know the object model of the COM server you just created. For Microsoft Office, there is an online reference manual at MSDN. As an example you could make the main window visible.

// Show Word.
wordType.InvokeMember(
    "Visible",
    BindingFlags.SetProperty,
    null,
    wordApplication,
    new object[]
    {
        true
    } );

Receiving Events - The Background

In the COM world, events are transferred through the "_blank" href="http://msdn.microsoft.com/library/en-us/com/html/5e2be055-7baa-4c42-bd20-b338da296ab0.asp">IConnectionPointContainer and "_blank" href="http://msdn.microsoft.com/library/en-us/com/html/ef5a917c-b57f-4000-8daa-86fdbfb47579.asp">IConnectionPoint interfaces. Please see the basic explanation at MSDN for further details.

The difficulty for me was to transfer this to the .NET world in a late binding manner. After searching hours at Google, I found one guy who pointed in the right direction, although he did not succeed.

Then I got the idea that helped me several times in the past: Look how .NET does it itself and then use Lutz Roeder's .NET Reflector and inspect the disassembly. It helped me this time, too. The following section explains the steps I took.

Steps to get from an unmanaged COM DLL to a .NET Disassembly

First, you have to add a reference to the Microsoft Office Word COM object through Visual Studio .NET 2003:

This generates a COM-Interop wrapper which is added to the reference list of the solution in Visual Studio .NET 2003:

Then open the properties of that reference (F4) which tells you the path to the generated wrapper library:

In this example, the path would be "C:\Inetpub\wwwroot\Bodycote\BodySales\ WebWordCommunicationTest\obj\Interop.Word.dll".

Now, open this file in the .NET Reflector application and inspect its properties:

Starting from there, you can examine the interfaces, classes, etc.

Example Interface to sink: IApplicationEvents2

In this section, I will show you the steps to late bind a given event source in Microsoft Office. The interface I've chosen is the (I)ApplicationEvents2 interface. I do not understand why .NET Reflector shows up two interfaces, both an IApplicationEvents2 and an ApplicationEvents2 interface. Only the latter was used by me (although in the attached sample, I named my interface with the leading "I"). The chosen interface is an arbitrary interface. You could use any event source interface you like.

The definition of this interface in the .NET Reflector looks like the following:

The bold methods are the methods which will be called from Microsoft Word upon an event in your COM client. So you must implement this interface in your COM client application and connect it to Microsoft Word in order to enable Microsoft Word to call your methods.

Defining and implementing the Interface

Since I decided to do late binding and thus cannot refer to the generated COM-Interop wrapper, you have to copy the definition of this interface into your COM client application. I did it as following:

[ComImport, Guid("000209FE-0000-0000-C000-000000000046"), 
    TypeLibType((short) 0x10d0)]
public interface ApplicationEvents2
{
    [MethodImpl(MethodImplOptions.InternalCall,
        MethodCodeType=MethodCodeType.Runtime), 
        DispId(1), TypeLibFunc((short) 0x41)]
    void Startup();

    [MethodImpl(MethodImplOptions.InternalCall, 
        MethodCodeType=MethodCodeType.Runtime), 
        DispId(2)]
    void Quit();

    [MethodImpl(MethodImplOptions.InternalCall, 
        MethodCodeType=MethodCodeType.Runtime),  
        DispId(3)]
    void DocumentChange();

    [MethodImpl(MethodImplOptions.InternalCall, 
        MethodCodeType=MethodCodeType.Runtime),  
        DispId(4)]
    void DocumentOpen( 
        [In, MarshalAs(UnmanagedType.Interface)] object Doc);

    [MethodImpl(MethodImplOptions.InternalCall, 
        MethodCodeType=MethodCodeType.Runtime),  
        DispId(6)]
    void DocumentBeforeClose( 
        [In, MarshalAs(UnmanagedType.Interface)] object Doc,  
        [In] ref bool Cancel);

    [MethodImpl(MethodImplOptions.InternalCall, 
        MethodCodeType=MethodCodeType.Runtime),  
        DispId(7)]
    void DocumentBeforePrint( 
        [In, MarshalAs(UnmanagedType.Interface)] object Doc,  
        [In] ref bool Cancel);

    [MethodImpl(MethodImplOptions.InternalCall, 
        MethodCodeType=MethodCodeType.Runtime),  
        DispId(8)]
    void DocumentBeforeSave( 
        [In, MarshalAs(UnmanagedType.Interface)] object Doc,  
        [In] ref bool SaveAsUI,  
        [In] ref bool Cancel);

    [MethodImpl(MethodImplOptions.InternalCall, 
        MethodCodeType=MethodCodeType.Runtime),  
        DispId(9)]
    void NewDocument( 
        [In, MarshalAs(UnmanagedType.Interface)] object Doc);

    [MethodImpl(MethodImplOptions.InternalCall, 
        MethodCodeType=MethodCodeType.Runtime),  
        DispId(10)]
    void WindowActivate( 
        [In, MarshalAs(UnmanagedType.Interface)] object Doc,  
        [In, MarshalAs(UnmanagedType.Interface)] object Wn);

    [MethodImpl(MethodImplOptions.InternalCall, 
        MethodCodeType=MethodCodeType.Runtime),  
        DispId(11)]
    void WindowDeactivate( 
        [In, MarshalAs(UnmanagedType.Interface)] object Doc,  
        [In, MarshalAs(UnmanagedType.Interface)] object Wn);

    [MethodImpl(MethodImplOptions.InternalCall, 
        MethodCodeType=MethodCodeType.Runtime),  
        DispId(12)]
    void WindowSelectionChange( 
        [In, MarshalAs(UnmanagedType.Interface)] object Sel);

    [MethodImpl(MethodImplOptions.InternalCall, 
        MethodCodeType=MethodCodeType.Runtime),  
        DispId(13)]
    void WindowBeforeRightClick( 
        [In, MarshalAs(UnmanagedType.Interface)]  
        object Sel,  
        [In] ref bool Cancel);

    [MethodImpl(MethodImplOptions.InternalCall, 
        MethodCodeType=MethodCodeType.Runtime),  
        DispId(14)]
    void WindowBeforeDoubleClick( 
        [In, MarshalAs(UnmanagedType.Interface)] object Sel,  
        [In] ref bool Cancel);
}

As you can see, it looks slightly different from the original disassembly in .NET Reflector. Namely, I changed strongly typed parameters to the function (e.g. a passed Document object, defined elsewhere in the COM-Interop wrapper) to untyped object objects. I did so, because I only was interested in the methods being called, not the type. If you want to get the type of the object again, you would have to late call the methods and properties as described at the beginning of this article.

The automatically generated COM-Interop wrapper generated multiple classes, delegates and events to comfortably receive events in a standard .NET manner. In my example, I will limit this to the methods of the interface defined above. In the concrete project where I needed this code, it was sufficient.

A second class is needed to actually implement the interface. Later, you do connect this class to the Microsoft Word instance to receive the notifications about the events. The class looks like the following (excerpt only, see attached sample for full code):

[ClassInterface(ClassInterfaceType.None)]
public sealed class ApplicationEvents2_SinkHelper : 
    IApplicationEvents2
{
    public void Startup() {}
    public void Quit() {}
    public void DocumentChange() {}
    public void DocumentOpen( object Doc ) {}
    public void DocumentBeforeClose( object Doc, ref bool Cancel ) {}
    public void DocumentBeforePrint( object Doc, ref bool Cancel ) {}
    public void DocumentBeforeSave( object Doc, 
         ref bool SaveAsUI, ref bool Cancel)  {}
    public void NewDocument( object Doc ) {}
    public void WindowActivate( object Doc, object Wn ) {}
    public void WindowDeactivate( object Doc, object Wn ) {}
    public void WindowSelectionChange( object Sel ) {}
    public void WindowBeforeRightClick( object Sel, ref bool Cancel ) {}
    public void WindowBeforeDoubleClick( object Sel, ref bool Cancel ) {}
}

Now, the helper classes are finished. You need to connect them to Microsoft Word.

Connecting to Microsoft Word

As shown in the example at the beginning of this article, you need to create a late bound instance of Microsoft Word:

// The type of the Word application. Used for dynamically
// creating.
Type wordType = Type.GetTypeFromProgID( "Word.Application" );
 
// The main Word application object.
object wordApplication = Activator.CreateInstance( wordType );

After this creation, you can directly query for the connection point container by calling:

// Get the connection point container.
UCOMIConnectionPointContainer connectionPointContainer =
    wordApplication as UCOMIConnectionPointContainer;

Then you must query the returned connection point container reference for the actual connection point for the interface to implement:

// The GUID of the connection point to query for.
// This is the same as the GUID of the IApplicationEvents2.
Guid guid = new Guid( "000209FE-0000-0000-C000-000000000046" );

// Get the connection point of the given GUID.
UCOMIConnectionPoint connectionPoint;
connectionPointContainer.FindConnectionPoint( 
    ref guid, 
    out connectionPoint );

If the connection point was found, it is returned. You now can create an instance of your implemented event sink interface and tell Microsoft Word to send events to this sink by calling the Advise method and passing a reference to your class:

// Create the actual object to receive the event
// notifications.
ApplicationEvents2_SinkHelper sink = 
    new ApplicationEvents2_SinkHelper();

// Connect the sink.
int sinkCookie;
connectionPoint.Advise( sink, out sinkCookie );

Now you're done! You now can call late bound methods and properties and get notified whenever an event occurs.

If you are finished with your code, you must instruct Microsoft Word to stop sending notifications to your object. This is done by calling the Unadvise method as shown in the following code:

// Disconnect the sink.
connectionPoint.Unadvise( sinkCookie );

// Release the com object.
Marshal.ReleaseComObject( connectionPoint );

Conclusion

In this article I've shown you how to connect an event sink to a COM server when you cannot use early binding. The main work that you must do is to define the interface of the event sink by yourself. Once you've defined the interface and implemented the interface in a class, you simply need to connect the interface to the COM server and disconnect again once you're done.

Hopefully this article helps you a little bit when it comes to COM-Interop in the .NET world.

For questions, comments and remarks, please use the commenting section at the bottom of this article.

References

In addition to the links in the article, the following references might be of interest:

  1. "Late Binding" at Wikipedia.
  2. "Understanding COM Event Handling" at CodeProject.
  3. "UCOMIConnectionPoint Interface", reference manual at MSDN.
  4. "UCOMIConnectionPointContainer Interface", reference manual at MSDN.

History

  • 2005-05-02

    Created first version of article.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here