原文地址:devblogs.microsoft.com/oldnewthing…
原文作者:devblogs.microsoft.com/oldnewthing
发布时间:2013年6月12日
上一次,我们学习了连接点的工作原理。其中有一种特殊的情况,就是连接接口是一个调度接口。
派遣接口顾名思义就是基于IDispatch的COM接口。IDispatch接口是OLE自动化对象的基础接口,如果你想让你的连接点接口可以从脚本中使用,你可能应该让它成为一个调度接口。
我假设你知道IDispatch的工作原理。简而言之就是,想要调用方法或属性的脚本调用GetIDsOfNames来获取它想要访问的方法或属性的调度ID,它使用类型库来找出参数和返回值等东西。一旦脚本引擎弄清了方法或属性期望被调用的方式,它就可以调用IDispatch::Invoke,传递调度ID和持有参数的DISPPARAMS结构。
现在,这种事情被冠以反射的花哨名字,但在OLE自动化时代,这简直就是一天的工作。你们这些小屁孩以为自己什么都发明了。
就像普通的连接点接口一样,作为连接点接口使用的dispatch接口由事件组成,这些事件在形式上是以方法来实现的。
dispinterface DWidgetEvents
{
[id(WDISPID_RENAMED)]
HRESULT Renamed([in] BSTR oldName, [in] BSTR newName);
…
};
你在对象声明中注明你的对象是这个接口的事件源。(感谢Medinoc在本文的原始版本中指出了这个错误。)
coclass Widget
{
[default] interface IWidget;
[default, source] dispinterface DWidgetEvents;
}
客户端用DIID_DWidgetEvents接口向连接点注册。按照惯例,调度接口通常以Events结尾,并且通常以字母D为前缀,接口ID符号以DIID开头,而不是简单的IID。这些惯例并不是普遍遵守的,所以如果你看到有人不遵守这些惯例,不要惊慌。如果你在IDL文件中声明你的调度接口,那么MIDL编译器将为你生成带有DIID前缀的调度接口ID)。
现在,从形式上看,当连接点要调用Renamed方法时,它调用GetIDsOfNames来获取名为L "Renamed "的方法的ID,并要求类型库搞清楚参数是什么。但这往往只是毫无意义的忙活。连接点通常已经知道答案了,因为连接点已经知道它在和什么接口对话。它不需要做任何 "反思",因为连接点已经知道ID和调用约定是什么。同样,你的C#代码也不需要使用反射来调用一个对象上的方法,而这个对象的集合你已经在你的项目中引用了。(GetIDsOfNames的存在不是为了连接点,而是为了协助动态类型的语言,你可以尝试在任何对象上调用任何方法,而方法是在运行时查找的)。
换句话说,连接点已经知道Rename方法的ID是WDISPID_RENAMED,而且它需要两个BSTR参数,因为这首先是向连接点注册的合同的一部分。
这意味着在实际操作中,客户端上唯一被调用的方法是IDispatch::Invoke。
这里是一个模板基类,我用它来实现我的连接点接口的调度接口。后面我们再讨论这几块。
template<typename DispInterface>
class CDispInterfaceBase : public DispInterface
{
public:
CDispInterfaceBase() : m_cRef(1), m_dwCookie(0) { }
/* IUnknown */
IFACEMETHODIMP QueryInterface(REFIID riid, void **ppv)
{
*ppv = nullptr;
HRESULT hr = E_NOINTERFACE;
if (riid == IID_IUnknown || riid == IID_IDispatch ||
riid == __uuidof(DispInterface))
{
*ppv = static_cast<DispInterface *>
(static_cast<IDispatch*>(this));
AddRef();
hr = S_OK;
}
return hr;
}
IFACEMETHODIMP_(ULONG) AddRef()
{
return InterlockedIncrement(&m_cRef);
}
IFACEMETHODIMP_(ULONG) Release()
{
LONG cRef = InterlockedDecrement(&m_cRef);
if (cRef == 0) delete this;
return cRef;
}
// *** IDispatch ***
IFACEMETHODIMP GetTypeInfoCount(UINT *pctinfo)
{
*pctinfo = 0;
return E_NOTIMPL;
}
IFACEMETHODIMP GetTypeInfo(UINT iTInfo, LCID lcid,
ITypeInfo **ppTInfo)
{
*ppTInfo = nullptr;
return E_NOTIMPL;
}
IFACEMETHODIMP GetIDsOfNames(REFIID, LPOLESTR *rgszNames,
UINT cNames, LCID lcid,
DISPID *rgDispId)
{
return E_NOTIMPL;
}
IFACEMETHODIMP Invoke(
DISPID dispid, REFIID riid, LCID lcid, WORD wFlags,
DISPPARAMS *pdispparams, VARIANT *pvarResult,
EXCEPINFO *pexcepinfo, UINT *puArgErr)
{
if (pvarResult) VariantInit(pvarResult);
return SimpleInvoke(dispid, pdispparams, pvarResult);
}
// Derived class must implement SimpleInvoke
virtual HRESULT SimpleInvoke(DISPID dispid,
DISPPARAMS *pdispparams, VARIANT *pvarResult) = 0;
public:
HRESULT Connect(IUnknown *punk)
{
HRESULT hr = S_OK;
CComPtr<IConnectionPointContainer> spcpc;
if (SUCCEEDED(hr)) {
hr = punk->QueryInterface(IID_PPV_ARGS(&spcpc));
}
if (SUCCEEDED(hr)) {
hr = spcpc->FindConnectionPoint(__uuidof(DispInterface), &m_spcp);
}
if (SUCCEEDED(hr)) {
hr = m_spcp->Advise(this, &m_dwCookie);
}
return hr;
}
void Disconnect()
{
if (m_dwCookie) {
m_spcp->Unadvise(m_dwCookie);
m_spcp.Release();
m_dwCookie = 0;
}
}
private:
LONG m_cRef;
CComPtr<IConnectionPoint> m_spcp;
DWORD m_dwCookie;
};
首先,分散一下注意力。我们的QueryInterface实现会将其双击到IDispatch,然后再到模板接口。这确保了模板化接口指针和IDispatch是兼容的。如果有人试图用与IDispatch无关的东西来使用这个QueryInterface实现就不好了。(是的,我本可以使用std::is_base_of,但我是一个在TR1之前长大的老前辈)。
该类的大部分内容仅仅是将 IDispatch 的所有方法支取出来,除了 IDispatch::Invoke,它做了一点粗活(初始化结果 VARIANT),然后让派生类来做重活。
最后,有两个公共方法Connect和Disconnect。这些方法执行了我们昨天看到的Advise和Unadvise调用。为了简化调用者的操作,我们保存了我们注册的IConnectionPointer,这样调用者在断开连接时就不必再传回它。
练习。Disconnect中的m_spcp.Release()调用真的有必要吗? (假设Connect最多被调用一次。)
这个助手模板类让编写调度接口连接点客户端变得非常简单,因为你所要做的就是在你关心的调度ID上以开关语句的形式实现SimpleInvoke。
class CWidgetClient : public CDispInterfaceBase
{
public:
CWidgetClient() { }
HRESULT SimpleInvoke(
DISPID dispid, DISPPARAMS *pdispparams, VARIANT *pvarResult)
{
switch (dispid) {
case WDISPID_RENAMED:
HeyLookItGotRenamed(pdispparams->rgvarg[0].bstrVal,
pdispparams->rgvarg[1].bstrVal);
break;
}
return S_OK;
};
在SimpleInvoke方法中,我们打开dispatch ID,如果看到喜欢的,就从pdispparams中提取参数。
更新:评论者parkrrr指出了DISPPARAMS结构的一个巨大的漏洞。参数的传递顺序是相反的 我不知道为什么。它们就是这样。
下一次,我们将开始将事件连接到我们的小程序,这样当用户浏览资源管理器或Internet Explorer窗口时,它就可以更新。
警告!管理代码! CLR理解连接点/调度接口的约定,并以CLR事件和相应的委托人的形式将调度事件暴露给托管代码。例如,我们的Renamed事件被暴露为一个名为Renamed的事件,代表类型为DWidgetEvents_RenamedEventHandler。你可以像监听其他CLR事件一样监听该事件:widget.Renamed += this.OnRenamed;。
注意:我完全忽略了双接口的问题。如果你喜欢的话,你可以读一读这些内容,但我们不需要知道这些内容来完成当前的工作。
通过www.DeepL.com/Translator(免费版)翻译