Download Simple Data Object with full ADO support - 40 KbImplementing a custom OLE DB rowset
This article continues from where we left off in the previous article. We have all of the framework in place to provide ADO recordset interfaces on our simple data object. All we need to do now is replace the wizard-generated OLE DB rowset object with one that allows us to access our object's data.
The rowset object that the ATL wizard has provided us with is of no use to us. It's fine for simple data where you can copy the data to be made available via ADO into the rowset's array. We want to retain control of our data and not be forced to create a copy of it, so the rowset needs to access our data in-place in our data object.
At first it may seem that the design of the ATL OLE DB provider templates is completely inappropriate for our needs. However, the design is actually very flexible and we can replace two simple components and provide the functionality that we require.
The "proxy" rowset
The standard OLE DB template rowset object, CRowsetImpl, provides access to data that's stored in a contiguous array within the rowset object itself. This is fine for simple example programs but almost useless for our required application. We want to keep our data stored within our simple data object, it may be too costly to copy all of the data into a new rowset object just to provide access via ADO. We'd rather leave the data where it is and just provide access to it.
Luckily the CRowsetImpl template relies on two template parameter classes for storing its data. The template itself looks something like this:
template <
class T,
class Storage,
class CreatorClass,
class ArrayType = CSimpleArray<Storage>,
class RowClass = CSimpleRow,
class RowsetInterface = IRowsetImpl < T, IRowset, RowClass> >
class CRowsetImpl : etc...
The pieces we're interested in replacing are the Storage and ArrayType classes. These are used by the default implementation to retrieve the rowset data from. By replacing these two template parameters with classes that implement the required functionality we can dictate where the rowset gets its data from.
We'll write a proxy rowset template. It's a proxy because it just takes the place of a rowset object and forwards all of the interesting calls to our data object. The data object stores its data in exactly the same way as before and the proxy rowset accesses the data in-place, on demand. There's no initial startup overhead involved, though there's probably a little overhead in the actual data access.
The proxy rowset template looks like this:
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 >
The important things to note are the classes that are defaulted for the Storage and ArrayType template parameters. These two proxy classes simply forward all requests to the proxy rowset rather than fielding them themselves. The proxy rowset can then pass requests on to the data object that it's associated with. By requiring the data object to provide function bodies for several simple data access and information functions the proxy rowset can deal with all of the technicalities of being an OLE DB rowset but still pass the requests for data and information through to the data object itself.
The functions that are data object specific, and must be provided by the class derived from our proxy rowset are as follows:
virtual void GetColumnInformation(
size_t column,
DBTYPE &dbType,
ULONG &size,
std::string &columnName,
DWORD &flags);
Called when building the column information structure for the rowset. Column numbering starts at 0. The type size and flags fields should be filled in with the values that are appropriate for this column of data - see DBCOLUMNINFO for more details. The data type specified in dbType should be the native data type that you are storing your data as. The proxy rowset will handle all data conversion requirements to and from this data type for you.
virtual void GetColumnData(
size_t row,
size_t column,
DBTYPE &dbType,
ULONG &size,
void *&pData,
bool &bIsUpdatable);
Called when access to the data in a particular row and column is required. Row and column numbering starts at 0. Type and size are the actual types and sizes of this element of the data (these are likely to be the same as those returned from GetColumnInformation for the column as a whole except in the case of variable length string data when the size returned here can be the actual length of the string, rather than the maximum length that's returned from GetColumnInformation. The void pointer, pData, should be set to point at the start of the data item itself. The proxy rowset will use this pointer to access the data. The pointer should be set to point straight at your data, you shouldnt allocate a buffer or do any copying. The bIsUpdatable flag isn't relavant until we add functionality to the proxy rowset in a later article to make it support read/write rowsets rather than simple read only rowsets.
virtual size_t GetNumColumns();
virtual size_t GetNumRows() const;
virtual HRESULT AddRow();
virtual HRESULT RemoveRow(int nIndex);
The others are all fairly obvious.
The proxy rowset contains two pointers that refer to the object that it is representing. These are set up automatically when the rowset is connected to the object. You can access these from within your derived class to provide access to your data object. One pointer is a pointer to your data object's IUnknown, you're unlikely to need to use this, it's only really for maintaining a reference on your object whilst the proxy rowset object is connected to it. The second pointer is a pointer to your object itself. Through this your proxy rowset derived class can get direct access to your data object's internals. It's allowed to do this because we take great care when creating the proxy rowset to make sure it's created in the same COM apartment as the data object.
Connecting the rowset to your object
Now that we have a rowset object and all of the ADO and OLE DB framework in place all that's left is to create the rowset and connect it to your data object.
We need to move back inside our OLE DB provider objects and intercept the rowset creation request at the command object's execute method. This is where the action will happen...
At present the method probably looks something like this:
CSimpleDataObjectRowset *pRowset;
return CreateRowset(
pUnkOuter,
riid,
pParams,
pcRowsAffected,
ppRowset,
pRowset);
The ATL OLE DB template function CreateRowset is being called which does the work of building a standard rowset of the type specified and then calls the rowset's execute method to populate it. We don't want any of this to happen, so we can rip out the above and replace it with some code that works with the custom command object we created in the ADO layer. This command has passed us the IUnknown pointer of the data object that we're providing a rowset onto. First we need to get hold of this IUnknown pointer using the parameter accessor that we are passed, then we can get to work...
As mentioned above we need to take care to create the rowset object in the same COM apartment as the data object itself. This will allow us to access the data object directly from the rowset object using a normal C++ pointer to the data object. The easiest way of making all of this work is for us to ask the data object itself to create and return the rowset object. Because the data object creates the rowset object as a C++ object we know that the rowset is in the same COM apartment as the data object. Of course, this means we need to get from the OLE DB provider's command object back into the data object.
The easiest way into the data object from the command object is via a COM call. In the command object we Query Interface on the data object's IUnknown pointer for the _IGetAsOLEDBRowset interface. We then call the interface's only method, GetAsRowset and pass our own IUnknown pointer in, along with a bunch of other stuff.
The work continues back inside the data object as we execute the GetAsRowset method of the _IGetAsOLEDBRowset interface... The template implementation of this method simply calls into the data object on a method called AsRowset() and this is where the work of actually creating the rowset and linking it to the data object actually occurs.
We've jumped through a lot of hoops to get to this point, but all of the code up to now has been generic template implementations which do the right thing. We're now inside a method on our data object and we have derived a class from the proxy rowset implementation to act as our rowset class. With a little custom code like the following, we can create our rowset and link it to our data.
HRESULT CMyDataObject::AsRowset(
IUnknown *pUnkCreator,
IUnknown *pUnkOuter,
REFIID riid,
LONG *pcRowsAffected,
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;
}
We can then hand the rowset back to the template implementations and they will take care of returning the object to the ADO layer which will wrap it in an ADO recordset and return it to our Visual Basic client code. We can even defer the most complex part of the code, that of creating and wiring up the rowset object, back to the _IGetAsOLEDBRowset implementation template, the call to CreateRowset is a template member function which is paramaterised on the rowset class we pass into it. It handles creating the rowset COM object and copying the command object's properties into it - in the same way that the standard ATL OLE DB rowset is created.
The complete working example code for the above can be downloaded from here.
So, how do I give ADO access to my object?
All of this assumes that you have an OLE DB conversion provider that will do the conversion for you. If you are only doing this for one object, package the conversion provider inside the same DLL as the object, if you're doing lots of objects like this, create a separate provider dll and have all your objects use the one provider.
The source was built using Visual Studio
6.0 SP3. Using the July edition of the Platform SDK. If you don't have the
Platform SDK installed then you may find that the compile will fail looking for
"msado15.h". You can fix this problem by creating a file of that name that
includes "adoint.h".
Please send any comments or bug reports to me
via email. For any updates to this article, check my site here.
History
29 July 2000 - updated source