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
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
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
ICommandText and finally makes a get properties call… The DataList just asks for
IColumnsRowset before making a get properties call. The DataCombo calls get properties then QIs for
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.
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 :
// This is what we have now...
class CDataObjectRowset :
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:
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
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:
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:
IRowsetInfo. But we don’t support
true for these properties
false for these
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
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.
CProxyRowsetImpl<>:BuildColumnInfo()always builds a column map that includes the bookmark column, it then allows
StorageProxy_GetColumnInfoto decide if we return this column to the caller.
Additional bookmark interfaces
IRowsetLocate is the main interface that supports bookmarks there are several others, most notably
IRowsetScroll and then short lived
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
IRowsetExactScroll to our rowset’s property map.
IRowsetScroll is easy, just use a
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.
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!
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.
- Visual Basic Provider test program
- Simple Data Object with bookmark support
- Get a trial version of the Janus GridEx that is used in the test harness above
- 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. 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…