重写 MFC 的默认客户端站点以实现 IServiceProvider






4.31/5 (9投票s)
2003年10月6日
10分钟阅读

78282

1461
描述了如何重写 MFC 提供的默认控件容器,以便为容器托管的 ActiveX 控件提供服务预置的自定义客户端站点。
引言
本文介绍了如何重写 MFC 提供的用于托管 ActiveX 控件的默认客户端站点实现。乍一看这可能有点神秘,但在几种情况下您可能会发现这种需求,例如当您正在开发一个 MFC 应用程序并使用一个 ActiveX 控件时,该控件期望通过标准 OLE 控件容器框架访问托管应用程序中的自定义服务实现。您也可能会发现在某些情况下,如果您是 ActiveX 控件的开发人员,并且希望利用托管应用程序提供的服务,那么理解这项技术也会很有用。
举个例子,我曾开发过一个 MFC 应用程序,它使用了一个第三方 ActiveX 控件,该控件要求托管应用程序实现一些与身份验证相关的服务,并通过 IServiceProvider
和控件容器模型向控件提供这些服务的访问权限。这需要本文中描述的重写技术,以便将控件指向我创建的自定义客户端站点,而不是 MFC 的默认客户端站点。
在本文中,我修改了微软的 driller
示例(可在 MSDN 上获得),作为如何实现重写的示例。尽管 driller
示例已经免费提供了一段时间,但本文中对其的修改和描述之所以有用,原因有二。首先,微软的示例更侧重于演示如何操作嵌入式 WebBrowser
控件,而不是解释控件容器/客户端站点重写的机制。其次,有限的文档对示例背后的概念解释很少。由于这些原因,可能很难确切理解 MFC 容器重写是如何实现的,以及它如何在除了使用 WebBrowser
控件之外的其他上下文中对您有用。
背景
首先,一个重要的警告:据我所知,微软没有提供任何官方支持的手段来完成本文所描述的内容。微软将 driller
示例作为一种可能的方法,我以此以及一些未文档化的 MFC 功能为基础,来描述本文中的方法。微软对其示例的文档声明它“是 MFC 特定的实现。因此,如果 MFC 的未来版本发生变化……这个示例(如果您使用这种技术,您的代码也会)可能无法工作。我们正在研究 MFC 暴露客户端站点以进行自定义的可能方法。”如果您打算在代码中使用这种方法,请务必仔细考虑此警告对您意味着什么。
控件容器和客户端站点
我不会在这里讨论 ActiveX 控件容器的细节,但对于不熟悉这个概念的人来说,一些背景知识会很有帮助。您可以将控件容器理解为顾名思义:托管应用程序中的一个“容器”,允许应用程序包含 ActiveX 控件并有效利用它们。对于应用程序“包含”的每个 ActiveX 控件,容器都必须实现一个客户端站点。
客户端站点提供控件和容器之间的通信。为了支持这一点,客户端站点实现了一些定义的接口,每个控件都知道它可以调用这些接口来向容器报告或请求服务。同样,控件也可以实现一些众所周知的接口,容器可以自信地通过每个控件的客户端站点调用这些接口。
这种将控件放置在容器中的模型与 OLE 一样古老,OLE 是当今 ActiveX 控件的技术模型。尽管多年来它对开发人员来说已经变得非常隐蔽,但控件/站点/容器模型仍然是控件与其主机之间交互的基础。它至今仍然存在,您每次使用 ActiveX 控件时,都会直接或间接地与它打交道。
通过客户端站点请求服务接口
正如我在本文开头引用的示例中那样,ActiveX 控件请求访问由托管应用程序中对象在客户端站点之外实现的自定义服务并不少见。客户端站点可以实现 IServiceProvider
作为处理来自控件的这些请求的一种手段,然后将对象的服务接口作为响应传递回控件。然后,控件可以直接与主机应用程序中的服务实现进行通信,而无需明确引用客户端站点或容器。为了实现这一点,控件可以查询其客户端站点以获取 IServiceProvider
,然后调用 IServiceProvider::QueryService
来请求特定的服务接口。然后,控件可以使用此接口调用服务。
在这种情况下,MFC 带来的困难是,MFC 框架为容器和客户端站点的构造和管理提供了默认实现,而内部机制的暴露非常少。尽管在大多数情况下,这种抽象非常有帮助,但有时需要对该过程进行更多控制,例如当需要将控件指向一个处理 IServiceProvider
提供服务接口的自定义客户端站点实现时。
代码示例演示了什么
本文附带的代码示例演示了如何在 MFC 中实现自定义客户端站点并将其连接到 MFC 容器框架中。它由一个 VC++ 6 工作区(MFC Client Site Override
)组成,包含两个项目:一个 MFC 应用程序(Client Site Sample
项目)和一个 ActiveX 控件(CS Test Control
项目)。Client Site Sample
演示了如何实现客户端站点和容器管理器,允许您的站点替代 MFC 的默认实现。它还演示了如何在自定义客户端站点中实现 IServiceProvider
以进行服务提供。CS Test Control
演示了 ActiveX 控件如何调用您的自定义客户端站点以获取服务接口,并提供此功能,以便您可以看到服务调用在 MFC 应用程序中的自定义客户端站点处理时的情况。
代码
以下步骤将引导您完成 Client Site Sample
项目。每个代码列表后都附有实现说明。请注意,要运行代码,可能需要进行一些小的修改。有关这些修改的信息,请参阅本文后面相关部分。
步骤 1:声明自定义客户端站点
要提供自定义客户端站点,我们需要子类化 MFC 的客户端站点封装类:COleControlSite
。代码清单 1 说明了这一点。
列表 1
// custsite.h ... // declare our custom control site to serve as the client site class CCustomControlSite:public COleControlSite { public: // constructor associates this site with the container CCustomControlSite(COleControlContainer *pCnt):COleControlSite(pCnt){} protected: // declare the ServiceProvider interface to create a nested class
// XServiceProvider on which we can implement the interface. Note that
// the IUnknown functions are not explicitly declared here because the
// macros automatically declare them for us DECLARE_INTERFACE_MAP(); BEGIN_INTERFACE_PART(ServiceProvider, IServiceProvider) // declare the interface method(s) STDMETHOD(QueryService) ( /* [in] */ REFGUID guidService, /* [in] */ REFIID riid, /* [out] */ void __RPC_FAR *__RPC_FAR *ppvObject); END_INTERFACE_PART(ServiceProvider) };
在此代码中,我们声明了自定义客户端站点 CCustomControlSite
,它继承自 MFC 的 COleControlSite
。我们定义构造函数接受一个指向控件容器的指针,该站点将与该容器关联,并将容器指针转发给基类。
步骤 2:声明客户端站点接口
现在我们有了一个客户端站点,我们可以声明站点必须支持的任何接口。在我们的示例中,我们希望客户端站点实现 IServiceProvider
以支持控件的服务查询,因此我们包含了 servprov.h
,然后声明了 IServiceProvider
及其 QueryService
成员。我们使用 MFC 的接口映射宏来简化操作,让 MFC 为我们提供(除其他外)标准的 IUnknown
实现。此步骤在清单 1 的后半部分显示。
步骤 3:实现客户端站点
现在我们来实现客户端站点。以下代码说明了这一点。
列表 2
// custsite.cpp ... BEGIN_INTERFACE_MAP(CCustomControlSite, COleControlSite) INTERFACE_PART(CCustomControlSite, IID_IServiceProvider, ServiceProvider) END_INTERFACE_MAP() ... // ServiceProvider implementation ... // IServiceProvider::QueryService // Description: support queries for service interfaces from controls STDMETHODIMP CCustomControlSite::XServiceProvider::QueryService(
REFGUID guidService, REFIID riid, void **ppvObject) { METHOD_PROLOGUE(CCustomControlSite, ServiceProvider) *ppvObject = NULL; // support the SMyService service if(guidService == SID_SMyService) { MessageBox(NULL, "QueryService for SID_SMyService",
"CCustomControlSite::XServiceProvider::QueryService", MB_OK); // support query for IMyInterface interface if(riid == IID_IMyInterface) { // in a real implementation the interface would be returned via
// ppvObject MessageBox(NULL, "QueryService for IID_IMyInterface",
"CCustomControlSite::XServiceProvider::QueryService", MB_OK); return E_NOINTERFACE; } } return E_NOINTERFACE; }
请注意,MFC 接口宏创建了一个嵌套类 (XServiceProvider
),我们只需实现其成员。QueryService
的实现会检查服务 ID 并相应地响应。我们支持一个虚构的 SMyService
服务和 IMyInterface
接口。请注意,在实际实现中,我们会通过 ppvObject
返回接口指针,但为了简单起见(我们总得停下来!),示例没有这样做。请注意,返回的接口可能来自包含应用程序中的任何对象,甚至可以来自客户端站点之外。
步骤 4:为客户端站点实现自定义控件容器管理器
MFC 通过控件容器管理器管理容器和客户端站点框架。我们必须向 MFC 提供一个自定义控件容器管理器,通过该管理器,它可以将我们的新客户端站点连接到 MFC 框架中的容器。为此,我们子类化 MFC 的 OLE 控件容器管理器类:COccManager
。以下代码说明了这一点。
列表 3
// custsite.h ... // declare our control container manager class CCustomOccManager :public COccManager { public: CCustomOccManager(){} // creates an instance of our custom control site and associates it
// with the container COleControlSite* CreateSite(COleControlContainer* pCtrlCont) { CCustomControlSite *pSite = new CCustomControlSite(pCtrlCont); return pSite; } }; ...
此代码声明了我们的自定义控件容器管理器 CCustomOccManager
,它派生自 MFC 的 COccManager
。我们重写了 COccManager::CreateSite
以接受来自 MFC 的控件容器指针,创建一个绑定到该容器的自定义客户端站点实例,然后返回该站点的指针。请注意,需要包含 occimpl.h
以支持此功能。
步骤 5:将自定义客户端站点连接到 MFC 容器
现在我们需要将 MFC 指向我们的自定义客户端站点。我们通过(在客户端站点示例应用程序的 InitInstance
中)创建自定义容器管理器实例,然后修改 MFC 现有的 AfxEnableControlContainer
调用,以使用此实例而不是默认实例来完成此操作。以下代码说明了这一点。
列表 3
// client site sample.cpp ... // Create a custom control container manager class so we can overide the
// client site CCustomOccManager *pMgr = new CCustomOccManager; // Set our control containment up but using our control container // manager class instead of MFC's default AfxEnableControlContainer(pMgr); ...
一旦到位,MFC 将使用我们的自定义容器管理器来创建我们的自定义客户端站点,该站点将依次在 MFC 内部调用 ActiveX 控件的 SetClientSite
中传递,以通知控件其站点。这建立了我们的自定义客户端站点,并支持 IServiceProvider
以实现自定义服务接口,作为控件的站点。此时,我们已成功用我们自己的客户端站点实现覆盖了控件的默认客户端站点。
运行示例代码
您可以逐行检查代码,查看 MFC、容器、客户端站点和控件之间的交互,但您需要先进行一些设置。
修改和依赖项
为简单起见,Client Site Sample
项目在 occimpl.h 的 #includes 中引用了绝对路径,您可能需要将其更改为您的 VC++ 安装文件夹,以便代码能够编译。该项目还使用 CS Test Control
项目中实现的 CSTest
控件来展示控件/客户端站点交互,因此请务必构建并注册 CSTest
。
使用示例 ActiveX 控件观察客户端站点
我包含了 CSTest
示例 ActiveX 控件,因此您可以运行客户端站点示例应用程序,并观察 MFC 如何创建自定义客户端站点,还可以看到控件如何查询自定义站点以获取 IServiceProvider
和 IMyInterface
自定义服务接口。一种方法是打开包含站点和控件项目的 MFC Client Site Override
工作区。使用调试配置构建项目,将 CS Test Control
项目指向 Client Site
项目的 exe 进行调试,在 InitInstance
中创建控件管理器处以及 CS Test Control
的 SetClientSite
函数中插入断点,然后运行 CS Test Control
并单步调试代码。
当控件查询客户端站点时,客户端站点示例将显示消息框,并且当它收到站点支持 IServiceProvider
的消息时,控件将更新其文本。以下来自 CS Test Control
项目的代码说明了如何在控件中通过重写控件的 SetClientSite
来完成此操作,MFC 容器调用 SetClientSite
来通知控件为其创建的客户端站点。请注意,在实际实现中,控件对 QueryService
的调用将返回一个接口指针(在 pMI
中),该指针可以由控件存储并随时用于直接调用服务函数。
列表 4
// cstest.cpp ... STDMETHOD(SetClientSite)(IOleClientSite *pClientSite) { if (pClientSite) { IServiceProvider *pSrvPrv = NULL; IMyInterface* pMI = NULL; pClientSite->QueryInterface(IID_IServiceProvider, (void**)&pSrvPrv); if(pSrvPrv) { m_bSupportISP = true; pSrvPrv->QueryService(SID_SMyService, IID_IMyInterface,
(void**)&pMI); pSrvPrv->Release(); } if(pMI) pMI->Release(); } return IOleObjectImpl<CTEST>::SetClientSite(pClientSite); }
那么 .NET 和 MFC7 呢?
使用 MFC7,您基本上以相同的方式完成此操作,但您确实需要将 occimpl.h
的包含更改为指向 afxocc.h
。此外,MFC7 中提供的 CDHTMLDialog
提供了一个非常有用的 CreateControlSite
成员,您可以在使用 DHTML Dialog 类时重写它,以便为每个控件创建自定义控件站点。