提问人:Feem 提问时间:8/31/2023 更新时间:8/31/2023 访问量:52
如何在 C# .NET 7 中的运行时为未知类型的 COM 对象后期绑定事件接收器
How to late bind an event sink for a COM object of unknown type at runtime in C# .NET 7
问:
我正在使用 C# .NET 7,在那里我可以创建一个在编译时未知类型的 COM 对象。
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);
我希望收到 COM 对象上发生的事件的通知。我已经对事件接收器进行了广泛的研究,但我发现没有什么可以解决我的问题。因此,我怀疑这个问题要么在 C# 中不可行,要么太明显和公理化了,以至于没有人觉得有必要在任何文档中解释它。我希望是后者。IConnectionPoint/IConnectionPointContainer
IConnectionPoint.Advise()
问题的症结在于,我发现的每个示例都适用于类型提前已知的 COM 对象。因此,代码知道要侦听哪些事件,并定义一个实现这些事件的接口,并将其传递给:IConnectionPoint.Advise()
icpt = (IConnectionPoint)someobject;
icpt.Advise(someinterfacethatimplementsallevents, out var _cookie);
根据我的研究,第一个参数 to 是一个对象,它实现了源对象正在寻找的接口。那么,当我在编译时甚至不知道源对象是什么时,我怎么能知道该接口应该是什么呢?Advise()
一些研究似乎说 sink 对象应该实现。但是会调用什么方法呢?IDispatch
Invoke()
这听起来有些合理,但 .NET Core(我在 .NET 7 上)的任何前卫都已经剥离了许多其他 COM 功能。所以他们说我们应该使用.NET中不再存在的接口?IDispatch
为了开始解决这个问题,我从网上的各种来源挖掘了以下实现:IDispatch
[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);
}
我不确定的是,我是否应该做这样的事情:
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();
*/
}
}
在这一点上,我已经到达了解决此问题的在线可用信息的末尾,并且不确定如何进一步进行。
答:
当实例是动态的时,获取事件并不容易。但正如你所发现的,你可以使用原始 COM 接口(IConnectionPoint、IConnectionPointContainer 和 IDispatch)获取它们)
下面是一个 C# 实用工具类,它包装并连接到请求的“dispinterface”事件接口。IDispatch
首先要做的是确定:
- 您正在寻找的 dispinterface 的 IID(接口 ID)(包含所需事件的接口)。
- 事件 DISPID(标识事件的整数)。
为此,可以使用 Windows SDK 中的 OleView 工具,打开一个类型库文件,该文件描述了 COM 对象支持的公共接口。它通常是一个 .TLB 文件(或嵌入在 .dll 中),但在 Office 的情况下,它是一个 .OLB。对于 Word,它位于(或类似路径)中。C:\Program Files\Microsoft Office\root\Office16\MSWORD.OLB
在此示例中,我想获取 Application.DocumentOpen 事件。这是 OleView 向我展示的内容:
因此,以下是获取事件的方法:
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);
}
}
以及 Dispatcher 实用程序类:
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; }
}
评论