如何在C# .NET 7中为运行时未知类型的COM对象迟绑定事件接收器
解决.NET 7中编译时未知COM对象的事件监听问题
要监听编译时类型未知的COM对象事件,核心是通过IDispatch接口实现通用事件接收器,结合IConnectionPointContainer完成事件注册。以下是完整实现步骤:
1. 完整定义IDispatch接口
.NET 7虽未内置IDispatch,但可手动导入完整接口(不能省略必要方法,COM会调用它们):
[Guid("00020400-0000-0000-C000-000000000046")] [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] [ComImport] public interface IDispatch { void GetTypeInfoCount(out uint pctinfo); void GetTypeInfo(uint iTInfo, int lcid, out IntPtr ppTInfo); void GetIDsOfNames([MarshalAs(UnmanagedType.LPStruct)] Guid riid, [MarshalAs(UnmanagedType.LPArray, ArraySubType = UnmanagedType.LPWStr)] string[] rgszNames, uint cNames, int lcid, [MarshalAs(UnmanagedType.LPArray)] int[] rgDispId); void Invoke(int dispIdMember, [MarshalAs(UnmanagedType.LPStruct)] Guid iid, int lcid, System.Runtime.InteropServices.ComTypes.INVOKEKIND wFlags, ref System.Runtime.InteropServices.ComTypes.DISPPARAMS pDispParams, out object? pVarResult, ref System.Runtime.InteropServices.ComTypes.EXCEPINFO pExcepInfo, out uint puArgErr); }
注意:Invoke方法的pDispParams参数应为ref而非数组,原代码定义有误,会导致封送错误。
2. 实现通用事件接收器
实现IDispatch,维护dispId与事件名称的映射,在Invoke中处理事件:
public class ComEventReceiver : IDispatch { private readonly Dictionary<int, string> _dispIdToName = new(); public event EventHandler<ComEventArgs>? EventRaised; public void GetTypeInfoCount(out uint pctinfo) { pctinfo = 0; } public void GetTypeInfo(uint iTInfo, int lcid, out IntPtr ppTInfo) { ppTInfo = IntPtr.Zero; throw new NotImplementedException(); } public void GetIDsOfNames([MarshalAs(UnmanagedType.LPStruct)] Guid riid, string[] rgszNames, uint cNames, int lcid, int[] rgDispId) { // 记录dispId与事件名称的映射 for (int i = 0; i < cNames; i++) { _dispIdToName[rgDispId[i]] = rgszNames[i]; } // 调用默认封送器处理ID查找,避免手动实现DISPID解析 Marshal.GetIDsOfNames(typeof(IDispatch).GUID, rgszNames, cNames, lcid, rgDispId); } public void Invoke(int dispIdMember, [MarshalAs(UnmanagedType.LPStruct)] Guid iid, int lcid, System.Runtime.InteropServices.ComTypes.INVOKEKIND wFlags, ref System.Runtime.InteropServices.ComTypes.DISPPARAMS pDispParams, out object? pVarResult, ref System.Runtime.InteropServices.ComTypes.EXCEPINFO pExcepInfo, out uint puArgErr) { // 初始化返回值 pVarResult = null; puArgErr = 0; pExcepInfo = new System.Runtime.InteropServices.ComTypes.EXCEPINFO(); // 获取事件名称 _dispIdToName.TryGetValue(dispIdMember, out var eventName); eventName ??= $"UnknownEvent_{dispIdMember}"; // 解析事件参数:DISPPARAMS的rgvarg是反向存储的,最后一个参数在前 var parameters = new List<object?>(); if (pDispParams.rgvarg != null) { for (int i = pDispParams.cArgs - 1; i >= 0; i--) { parameters.Add(pDispParams.rgvarg[i].Value); } } // 触发自定义事件,供业务代码处理 EventRaised?.Invoke(this, new ComEventArgs(eventName, parameters.ToArray())); } } // 自定义事件参数类,传递事件名称和参数 public class ComEventArgs : EventArgs { public string EventName { get; } public object?[] Parameters { get; } public ComEventArgs(string eventName, object?[] parameters) { EventName = eventName; Parameters = parameters; } }
3. 注册COM事件监听
通过IConnectionPointContainer找到对应连接点,注册接收器:
public static int RegisterComEvent(object comObj, ComEventReceiver receiver) { if (comObj is not IConnectionPointContainer container) { throw new InvalidOperationException("COM对象不支持连接点容器"); } // 查找支持IDispatch接口的连接点,大多数脚本化COM事件源用IDispatch作为事件接口 var dispatchGuid = typeof(IDispatch).GUID; container.FindConnectionPoint(ref dispatchGuid, out var connectionPoint); // 注册接收器,获取Cookie用于取消注册 connectionPoint.Advise(receiver, out int cookie); return cookie; } // 取消事件注册 public static void UnregisterComEvent(object comObj, int cookie) { if (comObj is IConnectionPointContainer container) { var dispatchGuid = typeof(IDispatch).GUID; container.FindConnectionPoint(ref dispatchGuid, out var connectionPoint); connectionPoint.Unadvise(cookie); Marshal.ReleaseComObject(connectionPoint); } }
4. 使用示例
// 创建未知类型的COM对象 var comTypeName = "Word.Application"; var comType = Type.GetTypeFromProgID(comTypeName); var comObj = Activator.CreateInstance(comType); // 初始化接收器并绑定事件处理 var receiver = new ComEventReceiver(); receiver.EventRaised += (sender, e) => { Console.WriteLine($"触发事件:{e.EventName}"); for (int i = 0; i < e.Parameters.Length; i++) { Console.WriteLine($"参数{i}: {e.Parameters[i]}"); } }; // 注册事件 int cookie = RegisterComEvent(comObj, receiver); // 执行操作触发事件,比如打开Word文档 dynamic wordApp = comObj; wordApp.Visible = true; wordApp.Documents.Add(); // 用完后取消注册并释放资源 UnregisterComEvent(comObj, cookie); Marshal.ReleaseComObject(comObj);
关键说明
- IDispatch方法必要性:
GetIDsOfNames是COM将事件名称转换为dispId的关键,必须正确实现,这里借助Marshal.GetIDsOfNames简化处理。 - 参数解析:
DISPPARAMS的参数数组是反向存储的,最后一个参数在数组第一个位置,需要倒序读取。 - 资源释放:务必调用
Unadvise取消事件注册,并释放COM对象,避免内存泄漏。
内容的提问来源于stack exchange,提问作者Feem
相关产品推荐
相关产品推荐

