Click here to Skip to main content
15,867,686 members
Articles / Desktop Programming / MFC
Article

Exposing tabular data from your COM object - Part 2

Rate me:
Please Sign up or sign in to vote.
5.00/5 (7 votes)
28 Jul 2000CPOL 94.8K   1.1K   33   6
The ATL OLE DB Provider templates appear to rely on the fact that your data is kept in a simple array, but that's not really the case at all!
  • Download Simple Data Object with full ADO support - 40 Kb
  • Implementing 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(
       /* [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;
    }

    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?

    • Include IGetAsADORecordset.idl and IGetAsOLEDBRowset.idl in your object's IDL file, and have your object support the IGetAsADORecordset, _Recordset and IGetAsOLEDBRowset interfaces.
    • Add the IGetAsADORecordsetImpl and IGetAsOLEDBRowsetImpl implementation templates to your class's inheritance hierarchy in your header file.
    • Add the following macros to your class's COM Map:
      COM_INTERFACE_ENTRY(_IGetAsOLEDBRowset)
      COM_INTERFACE_ENTRY_CHAIN(IGetAsADORecordsetImpl<CMyDataObject>) 
    • Derive a class from CProxyRowsetImpl and provide function bodies for the virtual functions required. These should use the pointer to your data object that's available to them as a protected member variable inherited from CProxyRowsetImpl to access your object's data.
    • Implement AsRowset(), probably just as show above.
    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

    License

    This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


    Written By
    Software Developer (Senior) JetByte Limited
    United Kingdom United Kingdom
    Len has been programming for over 30 years, having first started with a Sinclair ZX-80. Now he runs his own consulting company, JetByte Limited and has a technical blog here.

    JetByte provides contract programming and consultancy services. We can provide experience in COM, Corba, C++, Windows NT and UNIX. Our speciality is the design and implementation of systems but we are happy to work with you throughout the entire project life-cycle. We are happy to quote for fixed price work, or, if required, can work for an hourly rate.

    We are based in London, England, but, thanks to the Internet, we can work 'virtually' anywhere...

    Please note that many of the articles here may have updated code available on Len's blog and that the IOCP socket server framework is also available in a licensed, much improved and fully supported version, see here for details.

    Comments and Discussions

     
    GeneralMemory leaks in OLE DB Provider - FinalRelease never called Pin
    larsibam29-Apr-04 3:45
    larsibam29-Apr-04 3:45 
    GeneralRe: Memory leaks in OLE DB Provider - FinalRelease never called Pin
    Len Holgate29-Apr-04 3:56
    Len Holgate29-Apr-04 3:56 
    Sorry, I've no idea. Thankfully it's been a very long time since I've had to mess with OLE DB.

    Len Holgate
    www.jetbyte.com
    The right code, right now.
    GeneralCan't get ado to expose my ole db records Pin
    larsibam19-Mar-04 6:03
    larsibam19-Mar-04 6:03 
    GeneralRe: Can't get ado to expose my ole db records Pin
    Len Holgate21-Mar-04 5:42
    Len Holgate21-Mar-04 5:42 
    GeneralRe: Can't get ado to expose my ole db records Pin
    larsibam23-Mar-04 22:46
    larsibam23-Mar-04 22:46 
    Generalcomdll Pin
    16-Aug-01 20:34
    suss16-Aug-01 20:34 

    General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

    Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.