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.
Type wordType = Type.GetTypeFromProgID( "Word.Application" );
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.
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:
Type wordType = Type.GetTypeFromProgID( "Word.Application" );
object wordApplication = Activator.CreateInstance( wordType );
After this creation, you can directly query for the connection point container by calling:
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:
Guid guid = new Guid( "000209FE-0000-0000-C000-000000000046" );
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:
ApplicationEvents2_SinkHelper sink =
new ApplicationEvents2_SinkHelper();
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:
connectionPoint.Unadvise( sinkCookie );
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:
- "Late Binding" at Wikipedia.
- "Understanding COM Event Handling" at CodeProject.
- "UCOMIConnectionPoint Interface", reference manual at MSDN.
- "UCOMIConnectionPointContainer Interface", reference manual at MSDN.
History