Download the Simple Data Object with bookmark support - 50 Kb
Download Visual Basic Provider test program - 5 Kb
Get a trial version of the Janus GridEx that is
used in the test harness above.
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 confuses 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 the sample 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, make 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:
class CDataObjectRowset :
public CProxyRowsetImpl<
CMyDataObject,
CDataObjectRowset,
CConversionProviderCommand>
{
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!
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