向 VBScript (ATL) 公开 COM 事件
Posted
技术标签:
【中文标题】向 VBScript (ATL) 公开 COM 事件【英文标题】:Exposing COM events to VBScript (ATL) 【发布时间】:2012-07-27 15:27:20 【问题描述】:我使用“ATL 简单对象”向导在 C++ 中使用 ATL 构建了一个 COM 服务器 DLL。我遵循了 Microsoft 的 ATLDLLCOMServer 示例。一切正常,除了一件事:我没有收到 VBScript 中的 COM 事件。我确实收到了 C# 中的事件。在早期的基于 MFC 的实现中,我曾在 VBScript 中作为 ActiveX 控件工作。
我的控件是这样定义的:
class ATL_NO_VTABLE CSetACLCOMServer :
public CComObjectRootEx<CComSingleThreadModel>,
public CComCoClass<CSetACLCOMServer, &CLSID_SetACLCOMServer>,
public IConnectionPointContainerImpl<CSetACLCOMServer>,
public CProxy_ISetACLCOMServerEvents<CSetACLCOMServer>,
public IDispatchImpl<ISetACLCOMServer, &IID_ISetACLCOMServer, &LIBID_SetACLCOMLibrary, /*wMajor =*/ 1, /*wMinor =*/ 0>,
public IProvideClassInfo2Impl<&CLSID_SetACLCOMServer, &DIID__ISetACLCOMServerEvents, &LIBID_SetACLCOMLibrary> /* Required for event support in VBS */
public:
BEGIN_COM_MAP(CSetACLCOMServer)
COM_INTERFACE_ENTRY(ISetACLCOMServer)
COM_INTERFACE_ENTRY(IDispatch)
COM_INTERFACE_ENTRY(IConnectionPointContainer)
COM_INTERFACE_ENTRY(IProvideClassInfo)
COM_INTERFACE_ENTRY(IProvideClassInfo2)
END_COM_MAP()
BEGIN_CONNECTION_POINT_MAP(CSetACLCOMServer)
CONNECTION_POINT_ENTRY(__uuidof(_ISetACLCOMServerEvents))
END_CONNECTION_POINT_MAP()
[...]
在 VBScript 中,我像这样使用 COM 对象:
set objSetACL = WScript.CreateObject("SetACL.SetACL", "SetACL_")
' Catch and print messages from the SetACL COM server which are passed (fired) as events.
' The name postfix of this function (MessageEvent) must be identical to the event name
' as defined by SetACL.
' The prefix (SetACL_) can be set freely in the call to WScript.CreateObject
sub SetACL_MessageEvent (Message)
WScript.Echo Message
end sub
IDL 的相关部分如下所示:
[
object,
uuid(E1B57CA5-FD7F-4304-BC0A-8BEBDE231D53),
dual,
nonextensible,
pointer_default(unique),
helpstring("SetACL COM Interface")
]
interface ISetACLCOMServer : IDispatch
;
[
uuid(00D4DCD3-02B9-4A71-AB61-2283504620C8),
version(1.0),
helpstring ("SetACL Type Library")
]
library SetACLCOMLibrary
importlib("stdole2.tlb");
[
uuid(35F76182-7F52-4D6A-BD6E-1317345F98FB),
helpstring ("SetACL Event Interface")
]
dispinterface _ISetACLCOMServerEvents
properties:
methods:
[id(1), helpstring("Receives string messages that would appear on the screen in the command line version")]
void MessageEvent([in] BSTR message);
;
[
uuid(13379563-8F21-4579-8AC7-CBCD488735DB),
helpstring ("SetACL COM Server"),
]
coclass SetACLCOMServer
[default] interface ISetACLCOMServer;
[default, source] dispinterface _ISetACLCOMServerEvents;
;
;
问题:SetACL_MessageEvent 永远不会被调用。
我尝试过的:
阅读this KB article 后,我添加了IProvideClassInfo2 的实现,但这并没有帮助。 FAQ 提到不应将传出接口定义为双接口。我从接口定义中删除了“双重”,但这没有帮助。 我发现this SO question 似乎描述了一个类似的问题。从未给出答案,只有一种解决方法。更新 1:
我现在知道失败的地方,但不知道为什么:在 _ISetACLCOMServerEvents_CP.h 中的触发事件方法中,调用 Invoke 失败并显示 E_UNEXPECTED。这是一行:
hr = pConnection->Invoke(1, IID_NULL, LOCALE_USER_DEFAULT, DISPATCH_METHOD, ¶ms, NULL, NULL, NULL);
解决方案:
问题是我从 COM 对象中的后台线程调用事件触发函数,换句话说,从错误的单元。
【问题讨论】:
你不是必须在 vbscript/vba 中使用WithEvents
才能响应 COM 事件吗?
不,根据msdn.microsoft.com/en-us/library/ms974564.aspx你没有。
我认为您在IProvideClassInfo2Impl
中丢失了第二个参数。我应该是事件接口IID
。
谢谢@RomanR.,我添加了 IID(并更新了问题),但问题仍然存在。
你加错了。你需要DIID__ISetACLCOMServerEvents
。
【参考方案1】:
IProvideClassInfo2
的实现应该足够了(尽管我的印象是即使没有它也应该可以工作)。我从模板创建了一个简约的 ATL 项目作为示例。这是 ATL DLL + COM Class + 一个方法和一个事件 + IProvideClassInfo2
:
源代码 [SVN, Browser Friendly] + Win32 Binary.
测试.vbs
如下:
Function Test_Event(Text)
WScript.Echo "Event: " + Text
End Function
Dim Test
Set Test = WScript.CreateObject("AlaxInfo.VbsEvents.Foo", "Test_")
Test.Method("Event Fun")
而方法本身是:
// IFoo
STDMETHOD(Method)(BSTR sText) throw()
ATLTRACE(atlTraceCOM, 4, _T("sText \"%s\"\n"), CString(sText));
ATLVERIFY(SUCCEEDED(Fire_Event(sText)));
return S_OK;
执行此操作后,输出为:
我认为如果您从后台线程触发事件,您的示例项目中的事件可能会出现问题。您还可以使用调试器单步执行您的项目,并检查Fire_
调用中是否有任何错误。
【讨论】:
谢谢,您的项目构建并运行良好。我将每个文件都与我的文件进行了比较,但没有发现任何有意义的差异。正如我发现的那样,在我的项目中调用 fire 方法的调用失败 - 我已经更新了问题。 你会展示你的项目吗?我会检查你的代理类。也许经过某些修改后,它不再与 IDL 匹配。 我删除并重新生成了连接点代理类 - 没有变化。我已经上传了这个项目——如果你想看看:docs.google.com/open?id=0B_otVBEVufIiazV1MzBmb2Y2Y28(使用文件下载->下载) 您的 IDL 和代理类 - 它们很好。我可以想到您从错误的线程中触发事件,或者其他与 COM 相关的问题。 你没有跟进,我又遇到了这个Q。我认为您应该尝试将事件触发调用直接放入 COM 方法中,类似于我的in my code。你应该有这个工作,当你将代码移动到不同的地方时它会停止工作,这让我认为它被错误的线程触发了。但是您要确认这一点,因为您拥有可构建的代码。您可以只检查线程,并且使用 COM,您不能从错误的公寓呼叫...【参考方案2】:您可以将 VBScript 函数转换为 IDispatch*
指针。例如Microsoft.XMLHTTP
对象中的onreadystatechange
处理程序。
Option Explicit
Dim xmlhttp
Function OnReadyStateChange
WScript.Echo "ReadyState: " & xmlhttp.readyState
End Function
Set xmlhttp = CreateObject("Microsoft.XMLHTTP")
xmlhttp.onreadystatechange = GetRef("OnReadyStateChange")
xmlhttp.open "GET", "http://***.com", True
xmlhttp.send
WScript.Echo "Waiting for 20 seconds to allow OnReadyStateChange events to finish."
WScript.Sleep 20000
WScript.Echo "Done!"
输出:
ReadyState: 1
Waiting for 20 seconds to allow OnReadyStateChange events to finish.
ReadyState: 2
ReadyState: 3
ReadyState: 4
Done!
提取 C:\Windows\System32\msxml3.dll
的 IDL 表明我们正在设置 IDispatch*
属性:
[
odl,
uuid(ED8C108D-4349-11D2-91A4-00C04F7969E8),
helpstring("IXMLHTTPRequest Interface"),
dual,
oleautomation
]
interface IXMLHTTPRequest : IDispatch
// ...
[id(0x0000000e), propput, helpstring("Register a complete event handler")]
HRESULT onreadystatechange([in] IDispatch* rhs);
;
当将此方法应用于您的 C++ 应用程序时,您的调用代码将需要使用 IDispatch::Invoke
,其 DISPID 为 DISPID_VALUE
,例如:
// IDispatch* pOnReadyStateChange <-- set from VBScript.
HRESULT hr = S_OK;
CComVariant vResult;
EXCEPINFO ei = ;
DISPPARAMS dispParams = NULL, 0, 0, 0 ;
hr = pOnReadyStateChange->Invoke(
DISPID_VALUE,
IID_NULL,
0,
DISPATCH_METHOD,
&dispParams,
&vResult,
&ei,
NULL);
【讨论】:
以上是关于向 VBScript (ATL) 公开 COM 事件的主要内容,如果未能解决你的问题,请参考以下文章