Search code examples
c#eventscomcom-interop

How to late bind an event sink for a COM object of unknown type at runtime in C# .NET 7


I am working in C# .NET 7 where I can create a COM object of a type that is unknown at compile time.

var comTypeName = "Word.Application";//Assume this is passed in by the user and is unknown at compile time.
var comType = Type.GetTypeFromProgID(comTypeName);
var comObj = Activator.CreateInstance(comType);

I would like to be notified of events that happen on the COM object. I have researched extensively about IConnectionPoint/IConnectionPointContainer, event sinks, IConnectionPoint.Advise() and nothing I've found can solve my problem. So I suspect that either the problem is not doable in C#, or is so obvious and axiomatic, that nobody felt the need to explain it in any documentation. I am hoping it's the latter.

The crux of the issue is that every example I've found works with a COM object whose type is known ahead of time. Thus, the code knows which events to listen for, and defines an interface which implements those events, and passes it to IConnectionPoint.Advise():

icpt = (IConnectionPoint)someobject;
icpt.Advise(someinterfacethatimplementsallevents, out var _cookie);

From my research, the first parameter to Advise() is an object which implements the interface the source object is looking for. So how can I know what that interface should be when I don't even know what the source object is at compile time?

Some research seems to say that the sink object should implement IDispatch. But what method would be called on that? Invoke()?

This sounds somewhat plausible, but anything forward of .NET Core (I am on .NET 7) has stripped out IDispatch and much other COM functionality. So they are saying we should use an interface that no longer exists in .NET?

To begin to work around this, I have unearthed implementations of IDispatch from various sources online:

[Guid("00020400-0000-0000-c000-000000000046")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
[ComImport]
public interface IDispatch
{
    //
    // Omitting type info functions for brevity.
    //
    
    //Invoke seems to be what we care about.
    void Invoke(int dispIdMember,
        [MarshalAs(UnmanagedType.LPStruct)] Guid iid,
        int lcid,
        System.Runtime.InteropServices.ComTypes.INVOKEKIND wFlags,
        [In, Out][MarshalAs(UnmanagedType.LPArray)]
        System.Runtime.InteropServices.ComTypes.DISPPARAMS[] paramArray,
        out object? pVarResult,
        out System.Runtime.InteropServices.ComTypes.EXCEPINFO pExcepInfo,
        out uint puArgErr);
}

What I am unsure of is if I should do something like this:

public class MyDispatch : IDispatch
{
    void Invoke(int dispIdMember,
        [MarshalAs(UnmanagedType.LPStruct)] Guid iid,
        int lcid,
        System.Runtime.InteropServices.ComTypes.INVOKEKIND wFlags,
        [In, Out][MarshalAs(UnmanagedType.LPArray)]
        System.Runtime.InteropServices.ComTypes.DISPPARAMS[] paramArray,
        out object? pVarResult,
        out System.Runtime.InteropServices.ComTypes.EXCEPINFO pExcepInfo,
        out uint puArgErr)
        {
            //Do something with the event info and dispatch to the appropriate place in my code.
            //What I *really* need here is the string name of the event so that I can figure out where to properly dispatch it to.
            /*
            if (nameofevent == "Changed")
                ChangeHandler();
            else if (nameofevent == "Closed")
                ClosedHandler();
            */
        }
}

At this point, I've reached the end of the available information online for solving this problem and am unsure how to proceed further.


Solution

  • When 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:

    enter image description here

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