公开 COM 对象中的表格数据 - 第一部分






4.17/5 (3投票s)
ADO 似乎是将表格数据从您自己的 COM 对象中公开出来的理想方式,而 ATL OLE DB 提供程序模板可以提供帮助!
问题所在
如果您已经有一个COM对象管理着天然的表格式数据,或者您有一个COM对象包含的数据经常以表格式显示,那么利用第三方数据控件制造商在ADO方面所做的努力似乎是明智的。为什么还要费力编写自定义控件来显示您的数据,而您可以利用众多支持ADO的控件,只要您的COM对象能够提供自身的ADO视图。此外,通过为您的数据对象提供一个标准接口,其他人也能轻松地使用您的对象,而且您不必编写接口文档,因为它是ADO!
很多遗留数据对象会受益于通过ADO访问。问题在于提供ADO访问并非易事。ATL OLE DB Provider模板对于简单情况很有用,但当您希望自己的对象拥有数据而不是仅仅将其复制到行集对象内的数组中时,它们似乎就无能为力了。此外,访问数据并不容易,您需要连接到数据提供程序,然后从中获取行集,等等。
然而,可以通过扩展ATL模板来实现让您的对象保留数据所有权,并且可以隐藏所有需要绕过的步骤,以便将您的对象连接到其ADO记录集视图。
本文将首先介绍为简单数据对象提供ADO接口的机制,然后讨论编写一个可重用的OLE DB行集实现,该实现可以提供对简单数据对象数据的就地访问,但仍可在ATL OLE DB提供程序框架中使用。
一个简单的数据对象
假设我们有一个简单的数据对象,它实现了以下接口,并在内部以表格式表示其数据。
interface IMyDataObject : IUnknown { HRESULT SetColumnSize([in] long columnSize); HRESULT AddColumn( [in] BSTR columnName, [out, retval] long *index); HRESULT GetColumnName( [in] long index, [out, retval] BSTR *columnName); HRESULT AddRow([out, retval] long *index); HRESULT RemoveRow([in] long index); HRESULT Depth([out, retval] long *depth); HRESULT Width([out, retval] long *width); HRESULT SetAt( [in] long rowIndex, [in] long columnIndex, [in] BSTR value); HRESULT GetAt( [in] long rowIndex, [in] long columnIndex, [out, retval] BSTR *value); };
该对象可用于存储字符串表。从Visual Basic使用此对象很容易,但接口并不理想。您可以从这里下载简单数据对象和一个Visual Basic测试程序。
获取ADO记录集
我们将使用的ADO接口将通过自定义OLE DB提供程序访问我们的数据对象。通过选择“插入新ATL对象”并在对象向导的数据访问部分中选择“提供程序”,我们可以向简单数据对象的DLL添加一个提供程序。完成此操作后,我们可以向简单数据对象添加一个接口,使其能够返回自身的ADO记录集视图。该接口将使用ADO访问我们添加的OLE DB提供程序,并构造一个记录集返回给我们的Visual Basic客户端代码。
我们将添加的接口如下所示:
interface IGetAsADORecordset : IDispatch { [id(1), helpstring("method GetAsRecordset")] HRESULT GetAsRecordset( [in] CursorLocationEnum CursorLocation, [in] LockTypeEnum LockType, [in] CursorTypeEnum CursorType, [out, retval] VARIANT *pvRecordset); };
我们选择返回`variant`类型以获得记录集,因为这样我们就不必担心使用`importlib`导入ADO类型库。这会导致Visual Basic需要额外的`QI`调用才能从`variant`中存储的`IDispatch`指针获取正确的接口,但它使我们免于运行时绑定到ADO类型库的位置(请参阅知识库文章Q186387)。
Visual Basic客户端代码可以执行类似以下的操作:
Dim dataObject as New MyDataObject
Dim asRs as IGetAsADORecordset
Set asRs = dataObject
Dim rs as ADODB.Recordset
Set rs = asRs.GetAsRecordset( _
adUseClient, _
adLockOptimistic, _
adOpenStatic)
' now do something with the recordset we have!
我们或许应该使光标和锁定标志成为可选参数,并让它们默认为标准值,这样在大多数情况下代码会更简单。
我们可以将此接口添加到我们简单数据对象的IDL中,并遵循ATL的做法,我们可以编写一个模板来实现该接口。
由此产生的实现模板如下所示:
template < class T, const GUID* plibid = &CComModule::m_libid, WORD wMajor = 1, WORD wMinor = 0, tihclass = CComTypeInfoHolder> class ATL_NO_VTABLE IGetAsADORecordsetImpl : public IDispatchImpl< IGetAsADORecordset, &IID_IGetAsADORecordset, plibid, wMajor, wMinor, tihclass> { public: STDMETHOD(GetAsRecordset)( CursorLocationEnum cursorLocation, // [in] LockTypeEnum lockType, // [in] CursorTypeEnum cursorType, // [in] VARIANT *pvRecordset) // [out] { if (!pvRecordset) { return E_POINTER; } return E_NOTIMPL; } };
这相当简单,但由于接口是基于`dispatch`的,并且我们继承自`IDispatchImpl`,我们还需要额外的模板参数并为它们设置默认值,以便我们的用户可以根据需要调整`IDispatch`实现的*. 功能。
当然,现在我们必须将我们的数据对象连接到OLE DB提供程序,以便它可以访问我们的表格式数据并通过ADO呈现。由于我们将使用ADO连接到我们的OLE DB提供程序,我们必须以某种方式将数据对象通过ADO调用传递到OLE DB提供程序。幸运的是,ADO提供了一种执行此类操作的方法,即通过一个接受参数的Command,该参数可以是任何适合`variant`的内容。我们将简单地将数据对象的`IUnknown`指针作为ADO Command的参数传递。
我们将使用VC++的`#import`特性来简化ADO编码。产生的代码如下所示:
ADODB::_ConnectionPtr Connection("ADODB.Connection"); Connection->Open( _bstr_t( L"Provider=SimpleDataObject.ConversionProvider.1"), _bstr_t(""), _bstr_t(""), -1); ADODB::_CommandPtr Command("ADODB.Command"); Command->CommandText = _bstr_t("CONVERT"); pUnknown->AddRef(); ADODB::_ParameterPtr Param1 = Command->CreateParameter( _bstr_t("IUnknown"), ADODB::adIUnknown, ADODB::adParamInput, -1, _variant_t(pUnknown)); Command->Parameters->Append( Param1 ); Command->ActiveConnection = Connection; CComQIPtrspCommand = Command; ADODB::_RecordsetPtr Rs1("ADODB.Recordset"); _variant_t vtEmpty (DISP_E_PARAMNOTFOUND, VT_ERROR); Rs1->CursorLocation = (ADODB::CursorLocationEnum)cusorLocation; Rs1->CursorType = (ADODB::CursorTypeEnum)cursorType; Rs1->LockType = (ADODB::LockTypeEnum)lockType; Rs1->Open( _variant_t(spCommand), vtEmpty, ADODB::adOpenUnspecified, ADODB::adLockUnspecified, -1); // Return the recordset in a variant... pvRecordset->vt = VT_DISPATCH; pvRecordset->pdispVal = (IDispatch*)Rs1.Detach();
假设我们的OLE DB提供程序完成了它的工作,那么从ADO的角度来看,我们就只需要做这些了。
让一些东西工作起来...
通过稍微调整向导生成的代码,我们可以使上述代码能够返回默认的、向导生成的OLE DB提供程序返回的标准“文件系统视图”ADO记录集。
首先,我们需要为`ICommandWithParameters`添加支持,因为我们的Command对象包含一个参数。此接口的实现非常直接,因为我们的Command非常简单。在`ICommandWithParameters`的三个方法中,只有`SetParameterInfo`会被我们上面使用的Command调用——而这可以简单地返回`S_OK`。
一旦我们支持`ICommandWithParameters`,我们的ADO调用将一直通过到OLE DB提供程序的行集实现的`Execute`方法。这需要调整才能使其与我们的Command配合使用。向导生成的代码期望Command文本是文件规范,然后返回与该规范匹配的所有文件的目录列表。稍后,我们将用我们自己的行集实现替换整个行集实现,但现在只需将`szDir`变量硬编码为始终设置为“*.*”就足够了。
尽管这个记录集在任何方面都不代表我们简单数据对象中存储的数据,但它至少提供了一种快速简便的方法来测试我们正在构建的框架是否确实有效。
从QI公开记录集接口
现在我们已经有了公开记录集所需的所有代码,我们希望将其作为数据对象的“真实”接口,而不是如此明显地将其视为一个独立的数据视图。从Visual Basic的角度来看,如果我们要做的只是这样会更好:
Dim dataObject as New MyDataObject
Dim rs as ADODB.Recordset
Set rs = dataObject
' now do something with the recordset we have!
这实际上相当容易。当我们第一次被请求时,我们需要创建一个ADO记录集作为拆分接口,并将其聚合到我们的数据对象中。然后,我们可以将一些`COM_INTERFACE_ENTRY_FUNC`宏添加到我们的COM Map中,以处理我们可能被请求的各种ADO记录集接口,并处理ADO记录集对象通常期望暴露的任何其他接口……所有这些都可以直接在我们的`IGetAsADORecordset`实现中完成,这样我们只需要将我们对象的接口映射链接到`IGetAsADORecordsetImpl`映射……包含支持通过`IGetAsADORecordset`和通过QI返回ADO记录集(充满不相关的文件系统信息!)的数据对象的完整实现,可以在下载中找到。
本文 继续,在那里我们将解析默认的OLE DB行集实现,并编写一个自定义行集,允许我们的简单数据对象维护其数据的所有权。
源文件使用Visual Studio 6.0 SP3构建。使用Platform SDK的7月版。如果您没有安装Platform SDK,您可能会发现编译时会因找不到“msado15.h”而失败。您可以通过创建一个包含“adoint.h”的同名文件来解决此问题。
请通过 电子邮件 向我发送任何评论或错误报告。有关本文的更新,请在此 处 查看我的网站。
历史
2000 年 7 月 29 日 - 更新源代码