OLEDB - IRowsetLocate and Bookmarks

| 0 Comments
Adding bookmark functionality is relatively easy and it enables our ADO recordset to be used with a greater number of data bound controls.

IRowsetLocate and bookmarks
To support some of the more demanding data bound controls we need to support bookmarks. The proxy rowset that we developed in the last article already has some support for bookmarks built in, but the rowset itself doesn't expose IRowsetLocate, or the bookmark related properties, so the bookmark functionality can't be used by consumers. In this article we'll remedy that by adding support for IRowsetLocate.

A quick search for IRowsetLocate in the MSDN leads us to a rather helpful implementation of the interface which can be found in the "Enhancing the Simple Read-Only provider" section of the OLE DB Provider template documentation. Unfortunately the original implementation that was supplied had a few bugs in it, the latest version is better, but it's still buggy when you have more than 256 rows. We'll fix those bugs and then integrate the interface into the proxy rowset. Once that's done it's easy to build implementations of IRowsetScroll and IRowsetExactScroll to complete our bookmark support.

Supporting complex Data Bound controls
One of the advantages of OLE DB is that you can choose how much functionality you wish to expose. If your provider is relatively simple then you are not forced to provide all of the complex features that you would expect to find in a provider for a relational database. Although there are various "levels" of "conformance" with the OLE DB specification it's often difficult to work out exactly what functionality is required of your provider. The flexibility of the OLE DB specification is thus something of a double edged sword, whilst it's easy to conform to the spec it's equally easy for consumers to require you to implement some additional functionality before they can use your provider.

This wouldn't be so bad if all data bound controls came with documentation that stated exactly what they expected of a provider. Unfortunately I've yet to find a control that comes with such documentation. Each control has an arbitrary set of requirements that it makes on a provider. It would seem that the only way to be sure that your provider could work will all possible consumers would be to implement the entire OLE DB specification. However even that might not be enough for some of Microsoft's own controls. If you want to support read/write functionality on Microsoft's Data Grid, for example, it appears that you are required to support some interfaces that aren't present in the documentation or SDK!

If OLE DB were to use the standard COM functionality discovery mechanism, QueryInterface(), then it would be relatively easy to work out what certain consumers required of you by simply watching for the interfaces they asked you for. Unfortunately OLE DB uses a property based mechanism for functionality discovery. If you fail to answer the "what properties do you support" questions correctly then you'll never see any QI calls for the interfaces that provide the functionality that your consumers desire. I can understand why the designers did it this way: a consumer may need to discover lots of information about a provider in one go and multiple interface requests and calls would be horribly inefficient, but it makes it very difficult to experiment and find out what third party consumers require of your provider. OLEB Service Providers confuse the matter even more by stepping in and providing extra functionality for you, in certain circumstances, if it can synthesize the functionality that you're lacking from functionality that you present. Finally the ATL implementation of the property mechanism is far from easy to trace through and doesn't have any kind of debug output to show exactly what property calls are occurring.

This makes it particularly difficult to add features to your OLE DB provider as it's impossible to know how many controls you will be able to support without actually testing your code with each of the controls. Even a relatively simple piece of functionality, like bookmarks, can be difficult to implement because of the lack of documentation about what a particular control requires.

Determining what a control needs
As an example of the difficulties of adding functionality to your provider, take a look at this simple test program and run it with the object that we developed in our last article. The test is a simple Visual Basic program that creates our object and allows us to obtain a recordset from it and then connect the recordset to various data bound controls. As you will see, if you press "Make Table" and then "Get Recordset" and then press the various buttons to connect the recordset to the controls each control reacts differently to our minimalist recordset implementation: Only the MSHFlexGrid works. The Data List and Data Combo remain blank. The Linked Edit works, but then that's a "simple data bound control" - one that's only bound to a single row - so we would expect it to work... The Data Grid tells us that it needs bookmarks and the Janus GrixEx just reports that we're an "invalid recordset".

Contrast this with the results obtained when we check the "Client" check box in the Cursor Location frame. When using the client cursor engine ADO steps in and implements the missing functionality for us. This is great, and if we really want to use client side cursors then our work here is done. However, there's a major problem with client side cursors - they're client side. In this context that means that all of the effort that we went to so that our data object could retain ownership of its data and only convert it on demand was wasted. The ADO cursor engine simply fetches all of our data from our object into the cursor engine and adds functionality to the rowset implementation... Not ideal.

We can try and work out what functionality is required of us by watching the debug strings output our provider as we attempt to attach it to each control. Add _ATL_DEBUG_QI to the project settings for the provider and do a rebuild all. Then set up Visual Basic as the debug target for the OLE DB provider and start a debug run. Load the test harness project into Visual Basic and run it. You should then see the debug string output displayed in the Visual C++ debugger. Unfortunately the results are somewhat misleading. As I indicated above, whilst we do see some QI calls, most of the negotiation appears to occur through a requests for the rowset's properties (the Janus GridEx doesn't even do that!).

The Data Grid QI's for IConnectionPointContainer (probably seeing if we support change notifications), then IColumnsInfo and ICommandText and finally makes a get properties call... The DataList just asks for ICommandText and IColumnsRowset before making a get properties call. The DataCombo calls get properties then QIs for IConnectionPointContainer, IColumnsInfo and ICommandText, makes another get properties call and then QIs for IColumnsRowset. None of this would lead us to believe that bookmarks and IRowsetLocate were the feature that was lacking...

Only the Microsoft Data Grid has given us any meaningful information about what it requires of us and that was via an error message! From that we can look up bookmark support in the OLE DB documentation and discover that we must implement IRowsetLocate and answer correctly for several property values... As we'll see, implementing bookmarks on our rowset will make it work with some of the controls in the test harness program, the others will at least give us more hints at what else they require. It's unfortunate that we could only have discovered this fact by trial and error.

Supporting IRowsetLocateImpl
To add support for Bookmarks we need to change our DataObjectRowset object so that it derives from a CProxyRowsetImpl that itself has IRowsetLocate, rather than IRowset, as its base class. As mentioned before, we can leverage (steal) an implementation from the "Enhancing the Simple Read-Only provider" section of the OLE DB Provider template documentation.

The resulting changes to our DataObjectRowset look something like this:

// This is what we did have...
  
class CDataObjectRowset : 
   public CProxyRowsetImpl<
      CMyDataObject, 
      CDataObjectRowset, 
      CConversionProviderCommand>
{
  
// This is what we have now...
  
class CDataObjectRowset : 
   public CProxyRowsetImpl<
      CMyDataObject, 
      CDataObjectRowset, 
      CConversionProviderCommand,
      CRowsetStorageProxy<CDataObjectRowset>, 
      CRowsetArrayTypeProxy<
         CDataObjectRowset, 
         CRowsetStorageProxy<CDataObjectRowset> >,
      CSimpleRow,
      IRowsetLocateImpl < CDataObjectRowset > >
{

It's times like this that you begin to wish that you'd chosen a different order for the default template parameters! The storage and array proxy classes and the simple row object were all defaulted in our original implementation. Now, as we need to replace the final template parameter, we must copy the default values from the CProxyRowsetImpl template and just replace the base class parameter.

So, our rowset now derives from our implementation of IRowsetLocate, we now need to hook it up to our interface map. Since all of the interfaces are currently handled by the CProxyRowsetImpl class we need to add a com map to our CDataObjectRowset that chains to the one that's present in the CProxyRowsetImpl and then add support for IRowsetLocate. Much like this:

BEGIN_COM_MAP(CDataObjectRowset)
   COM_INTERFACE_ENTRY(IRowsetLocate)
   COM_INTERFACE_ENTRY_CHAIN(ProxyRowsetClass)
END_COM_MAP()

To make the chaining easier we've added a typedef to the CProxyRowsetImpl so that derived classes can use "ProxyRowsetClass" rather than having to specify the template and all the template parameters again!

This is all well and good, but unless we tell our consumers that we support bookmarks via the correct OLE DB properties they'll never request the IRowsetLocate interface.

Adjusting the property map
At first sight, you could be confused into thinking that the default rowset properties that ATL supplies you with includes support for bookmarks. After all, the rowset's property map looks something like this:

BEGIN_PROPSET_MAP(CConversionProviderCommand)
   BEGIN_PROPERTY_SET(DBPROPSET_ROWSET)
      PROPERTY_INFO_ENTRY(IAccessor)
      PROPERTY_INFO_ENTRY(IColumnsInfo)
      PROPERTY_INFO_ENTRY(IConvertType)
      PROPERTY_INFO_ENTRY(IRowset)
      PROPERTY_INFO_ENTRY(IRowsetIdentity)
      PROPERTY_INFO_ENTRY(IRowsetInfo)
      PROPERTY_INFO_ENTRY(IRowsetLocate)
      PROPERTY_INFO_ENTRY(BOOKMARKS)
      PROPERTY_INFO_ENTRY(BOOKMARKSKIPPED)
      PROPERTY_INFO_ENTRY(BOOKMARKTYPE)
      PROPERTY_INFO_ENTRY(CANFETCHBACKWARDS)
      PROPERTY_INFO_ENTRY(CANHOLDROWS)
      PROPERTY_INFO_ENTRY(CANSCROLLBACKWARDS)
      PROPERTY_INFO_ENTRY(LITERALBOOKMARKS)
      PROPERTY_INFO_ENTRY(ORDEREDBOOKMARKS)
   END_PROPERTY_SET(DBPROPSET_ROWSET)
END_PROPSET_MAP()

All is not what it seems. If you look in atldb.h you'll see that the property info entry macro expands to include some "default" flags, types and values that are specific for each property supplied. These are used to fill in the property map. What can be confusing is that specifying a property info entry for IRowset results in flags that say you DO support the interface whereas specifying a property info entry for IRowsetLocate results in flags that say you DONT support the interface! This is hardly intuitive. To determine which properties you support from the property map you must search through atldb.h and cross reference the other macros that it contains. I feel it would be far better if all of these PROPERTY_INFO_ENTRY macros were in fact PROPERTY_INFO_ENTRY_VALUE macros (these force you to specify the value of the property, you can then see, at a glance, if you do or do not support a property etc.) of course I understand why it's done this way, it makes the wizard easier to code...

So, what the default property map actually says is this:

We do support the following interfaces: IAccessor, IColumnsInfo, IConvertType, IRowset, IRowsetIdentity, IRowsetInfo. But we don't support IRowsetLocate.

We answer true for these properties CanFetchBackwards, CanHoldRows and CanScrollBackwards. But false for these Bookmarks, BookmarkSkipped, BookmarkType, LiteralBookmarks and OrderedBookmarks.

Obvious, isn't it.

So, it should just be a case of us using the PROPERTY_INFO_ENTRY_VALUE macro and specifying the correct values, such as VARIANT_TRUE for our IRowsetLocate property? It would be nice if it were this easy. Unfortunately the flags specified if we do that means that the property is read only. OLE DB properties are used by both the provider to indicate the functionality that it supports and by the consumer to indicate the functionality it requires. If we were to use the PROPERTY_INFO_ENTRY_VALUE macro we would end up forcing the consumer to accept that we must provide IRowsetLocate. It's better for us to make the property read/write so that the consumer can read the property and see that we support the interface and then write to the property and set it to false if it doesn't require us to support it. This may allow us to optimise some memory usage as we will know that we will never be asked for the interface...

So, to specify our bookmark properties we need to use the mother of all property map macros; PROPERTY_INFO_ENTRY_EX... See the code for the resulting map entries.

The problems with IRowsetLocateImpl
Now that we have all the code in place for our implementation of IRowsetLocate to be requested, we just have to fix a couple of bugs in the code that we're stealing.

The current version of IRowsetLocateImpl that can be found here suffers from one or two problems. Firstly, the bookmarks are declared as being DWORDs yet inside IRowsetLocate they are treated as BYTEs this leads to the implementation failing if a rowset has more than 256 rows. We fix this problem by casting the bookmark pointer to a DWORD pointer before dereferencing them. Secondly there are some rather dubious locking practices employed which can cause the object to be locked and then left in a locked state if an error occurs.

An older version of this implementation (from a previous version of the same sample) additionally had a off by one error on the case where the bookmark requested was DB_BMKLST.

See the code sample for the fixed implementation.

Integration with the proxy rowset
Our bookmark implementation is almost complete. The proxy rowset class already provided some support for bookmarks so we don't need to change anything. The support is included in the following places:

  • CProxyRowsetImpl<>::OnPropertyChanged() handles various rowset properties being set, and sets associated properties as required.
  • CProxyRowsetImpl<>::BookmarksRequired() is a helper function that can be called to determine if we need to return a rowset that contains a column with the bookmarks in.
  • CProxyRowsetImpl<>::InternalGetColumnData() handles and populates requests for data from the bookmark column.
  • CProxyRowsetImpl<>:StorageProxy_GetColumnInfo() handles adjusting the column information that we return to optionally include the bookmark column if required.

and finally:

  • CProxyRowsetImpl<>:BuildColumnInfo() always builds a column map that includes the bookmark column, it then allows StorageProxy_GetColumnInfo to decide if we return this column to the caller.

Additional bookmark interfaces
Whilst IRowsetLocate is the main interface that supports bookmarks there are several others, most notably IRowsetScroll and then short lived IRowsetExactScroll.

IRowsetScroll allows for consumers to obtain rows located at approximate positions within a rowset, it can be used when exact positioning is not required. It is derived from IRowsetLocate and an implementation can be found in IRowsetScrollImpl.h.

IRowsetExactScroll appears to have been introduced in OLE DB version 2.0 and then became deprecated in OLE DB version 2.1 - though controls often still ask for it. To include support for IRowsetExactScroll you have to be using version 2.x of the Data Access SDK. If you are using version 2.1 or later then you have to include a "deprecated" define to get the interface brought in - it's unfortunate that the define used is not named something more OLE DB specific... IRowsetExactScroll is derived from IRowsetScroll. Because of how we've implemented IRowsetScroll::GetApproximatePosition() - it gets rows at an exact position as our bookmarks are also row numbers - it's trivial to implement IRowsetExactScroll as it can simply call through to IRowsetScroll. Our implementation of IRowsetExactScroll can be found, not too surprisingly, in IRowsetExactScrollImpl.h.

The sample code assumes we're using the MDAC SDK v2.1 or later, so includes the #DEFINE deprecated. Check the OleDb.H file and search for OLEDBVER to get some idea of what version you're using...

As we're using OLE DB v2.1 we may as well have our provider advertise the fact. To do this we need to make a change to the DataSource object's property map. Locate the PROPERTY_INFO_ENTRY macro for the PROVIDEROLEDBVER property and replace it with a PROPERTY_INFO_ENTRY_VALUE macro for that property and specify a value of "2.1".

We can now add support for IRowsetScroll and IRowsetExactScroll to our rowset's property map. IRowsetScroll is easy, just use a PROPERTY_INFO_ENTRY_EX macro. IRowsetExactScroll is slightly more complex as there's no support for this property built in to the ATL wizard and headers - this isnt too much of a problem, we can use the macro as normal, but we have to add an entry to the string table that ATL provided us with. All of the properties that ATL supports have entries in a string table that's added to your project by the wizard. We need to add an entry for IDS_DBPROP_IRowsetExactScroll to this string table for the macro to work.

In the sample we add support for IRowsetExactScroll to the DataObjectRowset object by replacing the IRowsetLocateImpl<> that we used above with IRowsetExactScrollImpl<> and adding corresponding entries to the COM map.

Even with IRowsetExactScroll support the Data Grid, Data List and Data Combo fail to display data. The Data List and Data Combo query for, and use, IRowsetExactScroll and all of them create an accessor, but none of them actually pull any data! This is disappointing to say the least. Especially since the controls now silently fail and give us no obvious indication of what it is we need to do to get them to work...

Still, at least we have the Janus grid working correctly. Chances are that other non Microsoft controls might work with us also!

Download
The following source 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".

If your system drive isn't D:\ then you'll have to change the #import statements in IGetAsADORecordsetImpl.h and IGetAsADORecordset.cpp.

Revision history

  • 16th October 1999 - Initial revision at www.jetbyte.com.
  • 25th October 1999 - Changed the zip files so that they include the correct version of the VB test program...
  • 22nd July 2000 - Bug fix, IGetAsADORecordsetImpl.h, line 77, _Recordset15 should be Recordset15. Thanks to Nie Jiantao for reporting this.
  • 2nd October 2000 - Fixed some build configuration errors. Thanks to Charles Finley for reporting these.
  • 12th October 2005 - reprinted at www.lenholgate.com.

Other articles in the series
  • Objects via ADO - ADO seems to be the ideal way to expose tabular data from your own COM objects and the ATL OLE DB Provider templates can help!
  • Custom Rowsets - 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!
  • IRowsetLocate and Bookmarks - Adding bookmark functionality is relatively easy and it enables our ADO recordset to be used with a greater number of data bound controls.
  • Updating data through an ADO recordset - The ATL OLE DB Provider templates only seem to support read-only rowsets, and making them support updating of data isn't as easy as you'd expect!
  • Client Cursor Engine updates - Making the ADO Client Cursor Engine believe that your rowset is updateable involves jumping through a few extra hoops...
  • Disconnected Recordsets - If you are going to use the client cursor engine then often it's a good idea to disconnect your recordset...

Leave a comment