You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.12 06:56:06