[Windows翻译]调度接口作为连接点接口

423 阅读5分钟

原文地址: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(免费版)翻译