How to late bind an event sink for a COM object of unknown type at runtime in C# .NET 7
See the question and my original answer on StackOverflowWhen the instance is dynamic, it's not easy to get events. But as you found out, you can get them using raw COM interfaces (IConnectionPoint, IConnectionPointContainer and IDispatch)
Here is a C# utility class that wraps IDispatch
and connects to the requested "dispinterface" events interfaces.
The first thing to do is determine:
- the IID (interface id) of dispinterface you're after (the one that contains the events you need).
- the events DISPID (the integer that identifies events).
For that you can use the OleView tool from the Windows SDK, open a type library file which describes public interfaces that a COM object supports. It's often a .TLB file (or embedded in a .dll) but in the case of Office it's an .OLB. For Word, it's located in C:\Program Files\Microsoft Office\root\Office16\MSWORD.OLB
(or a similar path).
In this sample I want to get Application.DocumentOpen event. This is what OleView shows me:
So here is how to get the event:
static void Main()
{
var comTypeName = "Word.Application";
var comType = Type.GetTypeFromProgID(comTypeName);
dynamic comObj = Activator.CreateInstance(comType);
try
{
// to get IID and DISPIDs from DispInterfaces, open C:\Program Files\Microsoft Office\root\Office16\MSWORD.OLB (or similar)
// with OleView tool from Windows SDK
var dispatcher = new Dispatcher(new Guid("000209FE-0000-0000-C000-000000000046"), comObj);
dispatcher.Event += (s, e) =>
{
switch (e.DispId)
{
case 4: // method DocumentOpen(Document doc)
dynamic doc = e.Arguments[0]; // arg 1 is "doc"
Console.WriteLine("Document '" + doc.Name + "' opened.");
break;
}
};
comObj.Documents.Open(@"c:\somepath\some.docx");
}
finally
{
comObj.Quit(false);
}
}
And the Dispatcher utility class:
using System;
using System.Runtime.InteropServices;
using System.Threading;
public class Dispatcher : IDisposable, Dispatcher.IDispatch, ICustomQueryInterface
{
private IConnectionPoint _connection;
private int _cookie;
private bool _disposedValue;
public event EventHandler<DispatcherEventArgs> Event;
public Dispatcher(Guid interfaceId, object container)
{
ArgumentNullException.ThrowIfNull(container);
if (container is not IConnectionPointContainer cpContainer)
throw new ArgumentException(null, nameof(container));
InterfaceId = interfaceId;
Marshal.ThrowExceptionForHR(cpContainer.FindConnectionPoint(InterfaceId, out _connection));
_connection.Advise(this, out _cookie);
}
public Guid InterfaceId { get; }
protected virtual void OnEvent(object sender, DispatcherEventArgs e) => Event?.Invoke(this, e);
protected virtual void Dispose(bool disposing)
{
if (!_disposedValue)
{
var connection = Interlocked.Exchange(ref _connection, null);
if (connection != null)
{
connection.Unadvise(_cookie);
_cookie = 0;
Marshal.ReleaseComObject(connection);
}
_disposedValue = true;
}
}
~Dispatcher() { Dispose(disposing: false); }
public void Dispose() { Dispose(disposing: true); GC.SuppressFinalize(this); }
CustomQueryInterfaceResult ICustomQueryInterface.GetInterface(ref Guid iid, out IntPtr ppv)
{
if (iid == typeof(IDispatch).GUID || iid == InterfaceId)
{
ppv = Marshal.GetComInterfaceForObject(this, typeof(IDispatch), CustomQueryInterfaceMode.Ignore);
return CustomQueryInterfaceResult.Handled;
}
ppv = IntPtr.Zero;
if (iid == IID_IManagedObject)
return CustomQueryInterfaceResult.Failed;
return CustomQueryInterfaceResult.NotHandled;
}
int IDispatch.Invoke(int dispIdMember, Guid riid, int lcid, System.Runtime.InteropServices.ComTypes.INVOKEKIND wFlags, ref System.Runtime.InteropServices.ComTypes.DISPPARAMS pDispParams, IntPtr pvarResult, IntPtr pExcepInfo, IntPtr puArgErr)
{
var args = pDispParams.cArgs > 0 ? Marshal.GetObjectsForNativeVariants(pDispParams.rgvarg, pDispParams.cArgs) : null;
var evt = new DispatcherEventArgs(dispIdMember, args);
OnEvent(this, evt);
var result = evt.Result;
if (pvarResult != IntPtr.Zero)
{
Marshal.GetNativeVariantForObject(result, pvarResult);
}
return 0;
}
int IDispatch.GetIDsOfNames(Guid riid, string[] names, int cNames, int lcid, int[] rgDispId) => E_NOTIMPL;
int IDispatch.GetTypeInfo(int iTInfo, int lcid, out /*ITypeInfo*/ IntPtr ppTInfo) { ppTInfo = IntPtr.Zero; return E_NOTIMPL; }
int IDispatch.GetTypeInfoCount(out int pctinfo) { pctinfo = 0; return 0; }
private const int E_NOTIMPL = unchecked((int)0x80004001);
private static readonly Guid IID_IManagedObject = new("{C3FCC19E-A970-11D2-8B5A-00A0C9B7C9C4}");
[ComImport, Guid("00020400-0000-0000-C000-000000000046"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
private interface IDispatch
{
[PreserveSig]
int GetTypeInfoCount(out int pctinfo);
[PreserveSig]
int GetTypeInfo(int iTInfo, int lcid, out /*ITypeInfo*/ IntPtr ppTInfo);
[PreserveSig]
int GetIDsOfNames([MarshalAs(UnmanagedType.LPStruct)] Guid riid, [MarshalAs(UnmanagedType.LPArray, ArraySubType = UnmanagedType.LPWStr, SizeParamIndex = 2)] string[] names, int cNames, int lcid, [Out, MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 2)] int[] rgDispId);
[PreserveSig]
int Invoke(int dispIdMember, [MarshalAs(UnmanagedType.LPStruct)] Guid riid, int lcid, System.Runtime.InteropServices.ComTypes.INVOKEKIND wFlags, ref System.Runtime.InteropServices.ComTypes.DISPPARAMS pDispParams, IntPtr pvarResult, IntPtr pExcepInfo, IntPtr puArgErr);
}
[ComImport, Guid("b196b286-bab4-101a-b69c-00aa00341d07"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
private interface IConnectionPoint
{
[PreserveSig]
int GetConnectionInterface(out Guid pIID);
[PreserveSig]
int GetConnectionPointContainer(out IConnectionPointContainer ppCPC);
[PreserveSig]
int Advise([MarshalAs(UnmanagedType.IUnknown)] object pUnkSink, out int pdwCookie);
[PreserveSig]
int Unadvise(int dwCookie);
[PreserveSig]
int EnumConnections(out /*IEnumConnections**/ IntPtr ppEnum);
}
[ComImport, Guid("b196b284-bab4-101a-b69c-00aa00341d07"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
private interface IConnectionPointContainer
{
[PreserveSig]
int EnumConnectionPoints(out /*IEnumConnectionPoints*/ IntPtr ppEnum);
[PreserveSig]
int FindConnectionPoint([MarshalAs(UnmanagedType.LPStruct)] Guid riid, out IConnectionPoint ppCP);
}
}
public class DispatcherEventArgs : EventArgs
{
public DispatcherEventArgs(int dispId, params object[] arguments)
{
DispId = dispId;
Arguments = arguments ?? Array.Empty<object>();
}
public int DispId { get; }
public object[] Arguments { get; }
public object Result { get; set; }
}