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





5.00/5 (7投票s)
ATL OLE DB 提供程序模板似乎依赖于您的数据保存在一个简单数组中的事实,但实际上并非如此!
实现自定义 OLE DB 行集
本文承接上篇文章上一篇文章。我们已经具备了为简单数据对象提供 ADO 记录集接口的所有框架。现在,我们只需要用一个允许我们访问对象数据的行集对象来替换向导生成的 OLE DB 行集对象。
ATL 向导为我们提供的行集对象对我们毫无用处。它适用于简单的、可以将要通过 ADO 提供的数据复制到行集数组中的数据。我们希望保留对数据的控制权,而不必被迫复制数据,因此行集需要能够就地访问我们数据对象中的数据。
起初,ATL OLE DB 提供程序模板的设计似乎完全不适合我们的需求。然而,该设计实际上非常灵活,我们可以替换两个简单的组件并提供所需的功能。
“代理”行集
标准的 OLE DB 模板行集对象 CRowsetImpl 提供对存储在行集对象本身连续数组中的数据的访问。这对于简单的示例程序来说很好,但对于我们需要的应用程序几乎无用。我们希望将数据存储在我们的简单数据对象中,将所有数据复制到一个新的行集对象中以通过 ADO 提供访问可能会过于昂贵。我们宁愿让数据保留在原处,只提供对其的访问。
幸运的是,CRowsetImpl 模板依赖于两个模板参数类来存储其数据。模板本身看起来大致如下:
template < class T, class Storage, class CreatorClass, class ArrayType = CSimpleArray<Storage>, class RowClass = CSimpleRow, class RowsetInterface = IRowsetImpl < T, IRowset, RowClass> > class CRowsetImpl : etc...
我们感兴趣替换的部件是 Storage 和 ArrayType 类。默认实现使用它们来检索行集数据。通过用实现所需功能的类替换这两个模板参数,我们可以确定行集从何处获取数据。
我们将编写一个代理行集模板。它是一个代理,因为它只是占用了行集对象的位置,并将所有有趣的调用转发给我们的数据对象。数据对象以与以前完全相同的方式存储其数据,代理行集按需就地访问数据。没有初始启动开销,尽管实际数据访问可能存在一些开销。
代理行集模板看起来像这样:
template < class DataClass, class T, class CreatorClass, class Storage = CRowsetStorageProxy<T>, class ArrayType = CRowsetArrayTypeProxy<T, Storage>, class RowClass = CSimpleRow, class RowsetInterface = IRowsetImpl < T, IRowset, RowClass> > class CProxyRowsetImpl: public CRowsetImpl< T, Storage, CreatorClass, ArrayType, RowClass, RowsetInterface >
需要注意的重要事项是为 Storage 和 ArrayType 模板参数提供的默认类。这两个代理类只是将所有请求转发给代理行集,而不是自己处理它们。然后,代理行集可以将请求传递给它所关联的数据对象。通过要求数据对象为几个简单的数据访问和信息函数提供函数体,代理行集可以处理成为 OLE DB 行集的所有技术细节,但仍然将数据和信息的请求传递给数据对象本身。
数据对象特定的函数,必须由派生自我们代理行集的类提供,如下所示:
virtual void GetColumnInformation( size_t column, DBTYPE &dbType, ULONG &size, std::string &columnName, DWORD &flags);
在构建行集的列信息结构时调用。列编号从 0 开始。Type、Size 和 Flags 字段应使用适合该数据列的值进行填充 - 有关更多详细信息,请参阅 DBCOLUMNINFO。dbType 中指定的 **数据类型** 应该是您存储数据的 **本机数据类型**。代理行集将为您处理所有数据类型之间的转换要求。
virtual void GetColumnData( size_t row, size_t column, DBTYPE &dbType, ULONG &size, void *&pData, bool &bIsUpdatable);
在需要访问特定行和列的数据时调用。行和列编号从 0 开始。Type 和 Size 是该数据元素的实际类型和大小(这些可能与 GetColumnInformation 为整个列返回的值相同,除非是可变长度字符串数据,此时此处返回的大小可以是字符串的实际长度,而不是 GetColumnInformation 返回的最大长度)。void 指针 pData 应设置为指向数据项本身的开头。代理行集将使用此指针来访问数据。指针应直接指向您的数据,您不应分配缓冲区或进行任何复制。bIsUpdatable 标志在我们将向代理行集添加功能以使其支持读/写行集而不是简单只读行集之前没有关联性。
virtual size_t GetNumColumns(); virtual size_t GetNumRows() const; virtual HRESULT AddRow(); virtual HRESULT RemoveRow(int nIndex);
其他都相当明显。
代理行集包含两个指向它所代表的对象的指针。当行集连接到对象时,它们会自动设置。您可以在派生类中访问这些指针以提供对数据对象的访问。一个指针是您数据对象的 IUnknown 指针,您不太可能需要使用它,它主要用于在代理行集对象连接到它时维护对您对象的引用。第二个指针是指向您对象本身的指针。通过此指针,您的代理行集派生类可以获得对您数据对象内部的直接访问。之所以允许这样做,是因为我们在创建代理行集时非常小心,确保它与数据对象在同一个 COM apartment 中创建。
将行集连接到您的对象
现在我们有了行集对象以及所有 ADO 和 OLE DB 框架,剩下要做的就是创建行集并将其连接到您的数据对象。
我们需要回到我们的 OLE DB 提供程序对象内部,并在命令对象的 execute 方法中拦截行集创建请求。这就是将要发生的地方……
目前,该方法可能看起来像这样:
CSimpleDataObjectRowset *pRowset;
return CreateRowset(
pUnkOuter,
riid,
pParams,
pcRowsAffected,
ppRowset,
pRowset);
ATL OLE DB 模板函数 CreateRowset 被调用,它负责构建指定类型的标准行集,然后调用行集的 execute 方法来填充它。我们不希望发生这些,所以我们可以删除上面的代码,并用一段与我们在 ADO 层中创建的自定义命令对象一起工作的代码来替换它。此命令已将我们要为其提供行集的数据对象的 IUnknown 指针传递给我们。首先,我们需要使用传递给我们的参数访问器来获取此 IUnknown 指针,然后我们就可以开始工作了……
如上所述,我们需要注意在与数据对象相同的 COM apartment 中创建行集对象。这将使我们能够使用数据对象的普通 C++ 指针直接从行集对象访问数据对象。使这一切正常工作的最简单方法是让数据对象本身创建并返回行集对象。因为数据对象将行集对象创建为 C++ 对象,所以我们知道行集与数据对象在同一个 COM apartment 中。当然,这意味着我们需要从 OLE DB 提供程序的命令对象返回到数据对象。
从命令对象进入数据对象的最简单方法是通过 COM 调用。在命令对象中,我们 QueryInterface 数据对象的 IUnknown 指针以获取 _IGetAsOLEDBRowset 接口。然后,我们调用该接口的唯一方法 GetAsRowset,并将我们自己的 IUnknown 指针以及一堆其他东西传递进去。
工作继续回到数据对象内部,我们执行 _IGetAsOLEDBRowset 接口的 GetAsRowset 方法……该方法的模板实现只是调用数据对象中的一个名为 AsRowset() 的方法,而这正是实际创建行集并将其链接到数据对象的地方。
我们已经经历了许多步骤才到达这一点,但到目前为止的所有代码都是通用的模板实现,它们可以正确执行。我们现在位于我们数据对象的一个方法中,并且我们已经从代理行集实现派生了一个类作为我们的行集类。通过一些自定义代码,如以下内容,我们可以创建我们的行集并将其链接到我们的数据。
HRESULT CMyDataObject::AsRowset( /* [in] */ IUnknown *pUnkCreator, /* [in] */ IUnknown *pUnkOuter, /* [in] */ REFIID riid, /* [out] */ LONG *pcRowsAffected, /* [out, iid_is(riid)] */ IUnknown **ppRowset) { CDataObjectRowset *pRowset = 0; HRESULT hr = CreateRowset( pUnkCreator, pUnkOuter, riid, ppRowset, pRowset); if (SUCCEEDED(hr)) { IUnknown *pUnknown = 0; hr = QueryInterface(IID_IUnknown, (void**)&pUnknown); if (SUCCEEDED(hr)) { hr = pRowset->LinkToObject(this, pUnknown, pcRowsAffected); pUnknown->Release(); } } return hr; }
然后,我们可以将行集交还给模板实现,它们将负责将对象返回给 ADO 层,ADO 层将其包装在 ADO 记录集中并返回给我们的 Visual Basic 客户端代码。我们甚至可以将代码中最复杂的部分,即创建和连接行集对象,推迟到 _IGetAsOLEDBRowset 实现模板,对 CreateRowset 的调用是一个模板成员函数,它通过我们传递给它的行集类进行参数化。它负责创建行集 COM 对象并将命令对象的属性复制到其中 - 与创建标准 ATL OLE DB 行集的方式相同。
上述完整的可运行示例代码可在此处下载。
那么,我如何让 ADO 访问我的对象?
- 将 IGetAsADORecordset.idl 和 IGetAsOLEDBRowset.idl 包含到您的对象的 IDL 文件中,并让您的对象支持 IGetAsADORecordset、_Recordset 和 IGetAsOLEDBRowset 接口。
- 在您的头文件中,将 IGetAsADORecordsetImpl 和 IGetAsOLEDBRowsetImpl 实现模板添加到您的类继承层次结构中。
- 将以下宏添加到您的类的 COM Map 中
COM_INTERFACE_ENTRY(_IGetAsOLEDBRowset) COM_INTERFACE_ENTRY_CHAIN(IGetAsADORecordsetImpl<CMyDataObject>)
- 从 CProxyRowsetImpl 派生一个类,并为所需的虚拟函数提供函数体。这些函数应使用 CProxyRowsetImpl 继承的作为受保护成员变量的指向您数据对象的指针来访问您对象的数据。
- 实现 AsRowset(),可能就像上面展示的那样。
源代码是使用 Visual Studio 6.0 SP3 构建的。使用了 7 月版的 Platform SDK。如果您没有安装 Platform SDK,您可能会发现编译失败,因为它找不到“msado15.h”。您可以通过创建一个名为该文件并包含“adoint.h”的文件来解决此问题。
请通过电子邮件将任何评论或错误报告发送给我。有关本文的更新,请在此处查看我的网站。
历史
2000 年 7 月 29 日 - 更新源代码