Lon Fisher
Visual C++ Development Team
August 1998
Summary: Discusses how to create an OLE DB provider using the OLE DB Provider Templates. An example of creating a provider using the templates and testing it is included, as well as instructions for extending the provider beyond the template support. (39 printed pages) Includes:
OLE DB is a new data access methodology from Microsoft, an important part of the Universal Data Access strategy. The design allows high performance data access from any data source. Any tabular data is viewable through OLE DB regardless of whether it came from a database. This flexibility gives developers a tremendous amount of power.
OLE DB's design includes the concept of a consumer and provider. Figure 1 shows a graphical representation of the OLE DB system. The consumer represents the traditional client. The provider places data into a tabular format and returns it to the client.
Figure 1. OLE DB architecture
A provider is a set of COM components that contains a series of interfaces. Because these are standard interfaces, any OLE DB consumer can access data from any provider. Since providers are COM objects, consumers can access them in any language (C++, Basic, Java, and so on). Implementing so many COM interfaces can be tedious. Fortunately, the OLE DB Templates in Visual C++ can help you create a provider.
This article discusses how to create an OLE DB provider using the OLE DB Provider Templates. It presents an example of creating a provider using the templates and testing it and shows how you can extend the provider beyond the template support.
For the purposes of this article, we are using the OLE DB Provider Templates from Visual C++ 6.0. Extensive changes were made between the Visual C++ 5.0 Technology Preview and Visual C++ 6.0 provider templates. The article also includes an appendix (Appendix D) containing information on porting a provider from the Visual C++ 5.0 Technology Preview to Visual C++ 6.0.
To get the most from this article, you should have some familiarity with database functionality. You should also have a basic understanding of COM and the Active Template Library (ATL). Appendix C at the end of this document contains links to other valuable information for provider developers.
OLE DB Providers are a set of COM objects that transfer data from a durable source to a consumer. The OLE DB Provider places that data in a tabular format in response to calls from a consumer. Providers can be simple or complex. A provider may return a table, allow the client to determine the format of that table, or perform operations on that data.
Each provider implements a standard set of COM objects to handle requests from the client. A provider may implement optional COM objects to provide additional functionality. Figure 2 shows the different COM objects a provider may implement.
Figure 2. COM Components in an OLE DB Provider
Table 1. COM components and their uses
Component | Interfaces | Comments |
Data Source | [mandatory] IDBCreateSession
[mandatory] IDBInitialize [mandatory] IDBProperties [mandatory] IPersist [optional] IDBDataSourceAdmin [optional] IDBInfo [optional] IPersistFile [optional] ISupportErrorInfo |
Connection from the consumer to the provider. The object is used to specify properties on the connection such as User ID, Password, and data source name. The object can also be used to administer a data source (create, update, delete, tables, and so on). |
Session | [mandatory] IGetDataSource
[mandatory] IOpenRowset [mandatory] ISessionProperties [optional] IDBCreateCommand [optional] IDBSchemaRowset [optional] IIndexDefinition [optional] ISupportErrorInfo [optional] ITableDefinition [optional] ITransaction [optional] ITransactionJoin [optional] ITransactionLocal [optional] ITransactionObject |
The session object represents a single conversation between a consumer and provider. It is somewhat similar to the ODBC HSTMT in that there can be many simultaneous sessions active.
The session object is the primary link to get to OLE DB functionality. In order to get to a command, transaction, or rowset object, you go through the session object. |
Command | [mandatory] IAccessor
[mandatory] IColumnsInfo [mandatory] ICommand [mandatory] ICommandProperties [mandatory] ICommandText [mandatory] IConvertType [optional] IColumnsRowset [optional] ICommandPrepare [optional] ICommandWithParameters [optional] ISupportErrorInfo |
The command object handles operations on data such as queries. It can handle parameterized or non-parameterized statements.
The command object is also responsible for handling bindings for parameters and output columns. A binding is a structure that contains information about how a column, in a rowset, should be retrieved. It contains information such as ordinal, data type, length, status, and so on. |
Rowset | [mandatory] IAccessor
[mandatory] IColumnsInfo [mandatory] IConvertType [mandatory] IRowset [mandatory] IRowsetInfo [mandatory] IRowsetIdentity [optional] IColumnsRowset [optional] IConnectionPointContainer [optional] IRowsetChange [optional] IRowsetLocate [optional] IRowsetResynch [optional] IRowsetScroll [optional] IRowsetUpdate [optional] ISupportErrorInfo |
The rowset object represents the data from the data source. The object is responsible for the bindings of that data and any basic operations (update, fetch, movement, and so on) on the data. You will always have a rowset object to contain and manipulate data. |
Transaction | [mandatory] IConnectionPointContainer;
[mandatory] ITransaction [optional] ISupportErrorInfo |
The transaction object defines an atomic unit of work on a data source and determines how those units of work relate to each other. This object is not directly supported by the OLE DB Provider Templates (that is, you create your own object). |
Each COM component represents a series of COM interfaces. Some COM interfaces are mandatory and others are optional. By implementing the mandatory interfaces, a provider guarantees a minimum level of functionality that any client should be able to use. By implementing the optional interfaces, a provider can have more functionality and a richer feel to the client. The client should always call QueryInterface to determine if a provider supports a given interface.
The main reason to create an OLE DB provider is to take advantage of the Universal Data Access strategy. Some of the advantages of participating in the UDA strategy are:
There are 43 COM interfaces in the OLE DB v1.1 specification. (Note: There is now a version 2.0 of the OLE DB specification that contains additional interfaces. See www.microsoft.com/data for more information.) That can make developing providers tedious. Fortunately, the OLE DB Provider Templates contain canned implementations for all of the required interfaces. The OLE DB Provider Templates are an extension to ATL and can save you much work.
The OLE DB Provider Templates give you the following:
We will create an OLE DB provider that reads strings out of a text file. The provider allows the user to execute a command to retrieve the contents of the file. The provider does not allow the client to update or change the data (that is, it is a read-only provider).
Begin by running the ATL COM AppWizard. The ATL COM AppWizard is located in the File | New dialog under the projects tab. Use the following steps to create the project:
Once you have created the project, you should bring up the ATL Object Wizard. You can find the Object Wizard by doing an Insert | New ATL Object from the menu.
Figure 3. ATL Object Wizard
Select the OLE DB Provider entry, click Next, and you will get a property page as shown in Figure 4. The tab allows you to set the names for the provider. It allows you set the class names and filenames for the various COM objects within the provider. We will call our provider MyProvider.
Go ahead and type it into the "Short Name" dialog. Leave the object filenames alone as we will use them later.
Note The Provider Wizard generates the data source, session, command, and rowset objects. If you wish to create the transaction object, you will need to do this yourself.
Figure 4. Names Dialog for OLE DB Provider Wizard
In this section, we examine some of the important code snippets from the generated code. The Provider Wizard generates several files for the COM components.
/////////////////////////////////////////////////////////////////////////
// CMyProviderSource
class ATL_NO_VTABLE CMyProviderSource :
public CComObjectRootEx<CComSingleThreadModel>,
public CComCoClass<CMyProviderSource, &CLSID_MyProvider>,
public IDBCreateSessionImpl<CMyProviderSource, CMyProviderSession>,
public IDBInitializeImpl<CMyProviderSource>,
public IDBPropertiesImpl<CMyProviderSource>,
public IPersistImpl<CMyProviderSource>,
public IInternalConnectionImpl<CMyProviderSource>
Figure 5. CMyProviderSource inheritance chain
The provider classes use multiple inheritance. Figure 5 shows the inheritance chain for the data source object. All of the COM components derive from CComObjectRootEx and CComCoClass. CComObjectRootEx provides all of the implementation for the IUnknown interface. It can handle any threading model. CComCoClass handles any error support required. If you want to send richer error information to the client, you can use some of the Error APIs in CComCoClass.
The data source object also inherits from several "Impl" classes. Each class provides the implementation for an interface. The data source object implements the IPersist, IDBProperties, IDBInitialize, and IDBCreateSession interfaces. Each of these interfaces is required by OLE DB to implement the data source object. The developer can choose to support or not support a particular functionality by inheriting or not inheriting from one of these "Impl" classes. If you wished to support the IDBDataSourceAdmin interface, you would inherit from the IDBDataSourceAdminImpl class to get the functionality required.
BEGIN_COM_MAP(CMyProviderSource)
COM_INTERFACE_ENTRY(IDBCreateSession)
COM_INTERFACE_ENTRY(IDBInitialize)
COM_INTERFACE_ENTRY(IDBProperties)
COM_INTERFACE_ENTRY(IPersist)
COM_INTERFACE_ENTRY(IInternalConnection)
END_COM_MAP()
Figure 6. Data Source interface map
Whenever the client calls QueryInterface for an interface on the data source, it goes through the COM map shown in Figure 6. The COM_INTERFACE_ENTRY macros are from ATL and tell implementation of QueryInterface in CComObjectRootEx to return the appropriate interfaces.
BEGIN_PROPSET_MAP(CMyProviderSource)
BEGIN_PROPERTY_SET(DBPROPSET_DATASOURCEINFO)
PROPERTY_INFO_ENTRY(ACTIVESESSIONS)
PROPERTY_INFO_ENTRY(DATASOURCEREADONLY)
PROPERTY_INFO_ENTRY(BYREFACCESSORS)
PROPERTY_INFO_ENTRY(OUTPUTPARAMETERAVAILABILITY)
PROPERTY_INFO_ENTRY(PROVIDEROLEDBVER)
PROPERTY_INFO_ENTRY(DSOTHREADMODEL)
PROPERTY_INFO_ENTRY(SUPPORTEDTXNISOLEVELS)
PROPERTY_INFO_ENTRY(USERNAME)
END_PROPERTY_SET(DBPROPSET_DATASOURCEINFO)
BEGIN_PROPERTY_SET(DBPROPSET_DBINIT)
PROPERTY_INFO_ENTRY(AUTH_PASSWORD)
PROPERTY_INFO_ENTRY(AUTH_PERSIST_SENSITIVE_AUTHINFO)
PROPERTY_INFO_ENTRY(AUTH_USERID)
PROPERTY_INFO_ENTRY(INIT_DATASOURCE)
PROPERTY_INFO_ENTRY(INIT_HWND)
PROPERTY_INFO_ENTRY(INIT_LCID)
PROPERTY_INFO_ENTRY(INIT_LOCATION)
PROPERTY_INFO_ENTRY(INIT_MODE)
PROPERTY_INFO_ENTRY(INIT_PROMPT)
PROPERTY_INFO_ENTRY(INIT_PROVIDERSTRING)
PROPERTY_INFO_ENTRY(INIT_TIMEOUT)
END_PROPERTY_SET(DBPROPSET_DBINIT)
CHAIN_PROPERTY_SET(CMyProviderCommand)
END_PROPSET_MAP()
Figure 7. Data source property map
The property map specifies all of the properties designated by the provider. Properties in OLE DB are grouped. The data source object has two groups of properties: one for the DBPROPSET_DATASOURCEINFO and one for the DBPROPSET_DBINIT. The DBPROPSET_DATASOURCEINFO set corresponds to properties about the provider and its data source. The DBPROPSET_DBINIT set corresponds to properties used at initialization. The OLE DB Provider Templates handle these sets via the PROPERTY_SET macros. The macros create a block that contains an array of properties. Whenever the client calls the IDBProperties interface, the provider uses the property map.
A developer does not need to implement every property in the specification. If you do not want to support a property, you can remove it from the map. If you wish to support a property, just add it into the map using a PROPERTY_INFO_ENTRY macro. The macro corresponds to the UPROPINFO structure, as shown in Figure 8.
struct UPROPINFO
{
DBPROPID dwPropId;
ULONG ulIDS;
VARTYPE VarType;
DBPROPFLAGS dwFlags;
union
{
DWORD dwVal;
LPOLESTR szVal;
};
DBPROPOPTIONS dwOption;
};
Figure 8. The UPROPINFO structure
Each element in the structure represents information to handle the property. It contains a DBPROPID to determine the GUID and ID for the property. It also contains entries to determine the type and value of the property.
If you wish to change the default value of a property, then you can use either the PROPERTY_INFO_ENTRY_VALUE or PROPERTY_INFO_ENTRY_EX macros. These macros allow you to specify a value for a corresponding property. To learn more about a property and its values, you should consult the OLE DB specification. The PROPERTY_INFO_ENTRY_VALUE macro is a shorthand notation that allows you to change the value. The PROPERTY_INFO_ENTRY_VALUE macro calls the PROPERTY_INFO_ENTRY_EX macro. This macro allows you to add or change all of the attributes in the UPROPINFO structure.
If you wish to define your own property set, then you can add one by making an additional BEGIN_PROPSET_MAP/END_PROPSET_MAP combination. You need to define a GUID for the property set and then define your own properties. If you have provider-specific properties, add them to a new property set instead of using an existing one. That way you won't run into problems when the next version of OLE DB ships.
The MyProviderSess.H file contains the declaration and implementation for the OLE DB session object. The data source object creates the session object. It represents one conversation between a consumer and provider. There can be many simultaneous sessions open for one data source. Figure 9 shows the inheritance list.
/////////////////////////////////////////////////////////////////////////
// CMyProviderSession
class ATL_NO_VTABLE CMyProviderSession :
public CComObjectRootEx<CComSingleThreadModel>,
public IGetDataSourceImpl<CMyProviderSession>,
public IOpenRowsetImpl<CMyProviderSession>,
public ISessionPropertiesImpl<CMyProviderSession>,
public IObjectWithSiteSessionImpl<CMyProviderSession>,
public IDBSchemaRowsetImpl<CMyProviderSession>,
public IDBCreateCommandImpl<CMyProviderSession, CMyProviderCommand>
Figure 9. CMyProviderSession inheritance chain
The session object inherits from IGetDataSource, IOpenRowset, ISessionProperties, and IDBCreateCommand. The IGetDataSource interface allows a session to retrieve the data source that created it. This is useful if you need to get properties from the data source that you created or other information that the data source can provide. The ISessionProperties interface handles all of the properties for the session. The IOpenRowset and IDBCreateCommand interfaces are used to do the database work. If the provider supports commands, it will implement the IDBCreateCommand interface. It is used to create the command object that can execute commands. The provider always implements the IOpenRowset object. It is used to generate a simple rowset from a provider. It will be a default rowset (like select * from foo) from a provider.
The wizard also generates three session classes: CMyProviderSessionColSchema, CMyProviderSessionPTSchema, and CMyProviderSessionTRSchema. These sessions are used for schema rowsets. Schema rowsets allow the provider to return metadata to the consumer without the consumer having to execute a query or fetch data. Fetching metadata can be far quicker than discovering a provider's capabilities.
The OLE DB specification requires that providers implementing the IDBSchemaRowset interface support three schema rowset types: DBSCHEMA_COLUMNS, DBSCHEMA_PROVIDER_TYPES, and DBSCHEMA_TABLES. The wizard generates implementations for each of these schema rowsets. Each class generated by the wizard contains an Execute function. In this Execute function, you can return data to the provider about which tables, columns, and data types you support. This data is usually known at compile time.
The CMyProviderCommand class is the implementation for the provider command object. It provides the implementation for the IAccessor, ICommandText, and ICommandProperties interfaces. The IAccessor interface is exactly the same as in the rowset. The command object uses the accessor to specify bindings for parameters. The rowset object uses them to specify bindings for output columns. The ICommandText interface is a useful way to specify a command textually. We will use the ICommandText interface later when we add our own code. We will also override the ICommand::Execute() method. The ICommandProperties interface handles all of the properties for the command and rowset objects.
// CMyProviderCommand
class ATL_NO_VTABLE CMyProviderCommand :
class ATL_NO_VTABLE CMyProviderCommand :
public CComObjectRootEx<CComSingleThreadModel>,
public IAccessorImpl<CMyProviderCommand>,
public ICommandTextImpl<CMyProviderCommand>,
public ICommandPropertiesImpl<CMyProviderCommand>,
public IObjectWithSiteImpl<CMyProviderCommand>,
public IConvertTypeImpl<CMyProviderCommand>,
public IColumnsInfoImpl<CMyProviderCommand>
Figure 10. CMyProviderCommand inheritance chain
The IAccessor interface manages all of the bindings used in commands and rowsets. The consumer calls IAccessor::CreateAccessor() with an array of DBBINDING structures. Each DBBINDING structure contains the information of how the column bindings should be handled (type, length, and so on). The provider receives the structures, then determines how the data should be transferred and whether any conversions are necessary. The IAccessor interface is used in the command object to handle any parameters in the command.
The command object also provides an implementation of IColumnsInfo. OLE DB requires the IColumnsInfo interface. The interface allows the consumer to retrieve information about parameters from the command. The rowset object uses the IColumnsInfo interface to return information about the output columns to the provider.
The provider also contains an interface called IObjectWithSite. The IObjectWithSite interface was implemented in ATL 2.0 and allows the implementor to pass information about itself to its child. The command object uses the IObjectWithSite information to tell any generated rowset objects about who created them.
The wizard generates an entry for the rowset object. In our case, it is called CMyProviderRowset. The CMyProviderRowset class inherits from an OLE DB Provider class called CRowsetImpl. The CRowsetImpl class implements all of the necessary interfaces for the rowset object. Figure 11 shows the inheritance chain for the CRowsetImpl class.
template <class T, class Storage, class CreatorClass,
class ArrayType = CSimpleArray<Storage>,
class RowClass = CSimpleRow,
class RowsetInterface = IRowsetImpl < T, IRowset, RowClass> >
class CRowsetImpl :
public CComObjectRootEx<CreatorClass::_ThreadModel>,
public IAccessorImpl<T>,
public IRowsetIdentityImpl<T, RowClass>,
public IRowsetCreatorImpl<T>,
public IRowsetInfoImpl<T, CreatorClass::_PropClass>,
public IColumnsInfoImpl<T>,
public IConvertTypeImpl<T>,
public RowsetInterface
Figure 11. The CRowsetImpl class
CRowsetImpl also uses the IAccessor and IColumnsInfo interfaces. It uses these interfaces for output fields in tables. The class also provides an implementation for IRowsetIdentity. IRowsetIdentity allows the consumer to determine if two rows are identical. The IRowsetInfo interface implements properties for the rowset object. The IConvertType interface allows the provider to resolve differences between data types requested by the consumer and those used by the provider.
The IRowset interface actually handles data retrieval. The consumer first calls a method called GetNextRows to return a handle to a row, known as an HROW. The consumer then calls IRowset::GetData
with that HROW to retrieve the requested data.
CRowsetImpl also takes several template parameters. These parameters allow the developer to determine how the CRowsetImpl class will handle data. The ArrayType argument allows the developer to determine what storage mechanism is used to store the row data. The RowClass parameter specifies what class contains an HROW.
The RowsetInterface parameter also allows the developer to use the IRowsetLocate or IRowsetScroll interfaces. The IRowsetLocate and IRowsetScroll interfaces both inherit from IRowset. Therefore, the OLE DB Provider Templates must provide special handling for these interfaces. If you wish to use either of these interfaces (we'll use IRowsetLocate later in this paper), then you need to use this parameter.
The wizard creates a class to contain one row of data. In our provider, it is called CMyProviderWindowsFile. Figure 12 shows the code for CMyProviderWindowsFile. The wizard-generated code lists all of the files in a directory by using the WIN32_FIND_DATA structure. CMyProviderWindowsFile inherits from the WIN32_FIND_DATA structure.
/////////////////////////////////////////////////////////////////////
// MyProviderRS.H
class CMyProviderWindowsFile:
public WIN32_FIND_DATA
{
public:
BEGIN_PROVIDER_COLUMN_MAP(CMyProviderWindowsFile)
PROVIDER_COLUMN_ENTRY("FileAttributes", 1, dwFileAttributes)
PROVIDER_COLUMN_ENTRY("FileSizeHigh", 2, nFileSizeHigh)
PROVIDER_COLUMN_ENTRY("FileSizeLow", 3, nFileSizeLow)
PROVIDER_COLUMN_ENTRY("FileName", 4, cFileName)
PROVIDER_COLUMN_ENTRY("AltFileName", 5, cAlternateFileName)
END_PROVIDER_COLUMN_MAP()
};
Figure 12. CmyProviderWindowsFile
CMyProviderWindowsFile also contains a map describing the columns in the provider's rowset. The provider column map contains one entry for each field in the rowset using the PROVIDER_COLUMN_ENTRY macros. The macros specify column name, ordinal, and offset to a structure entry. The entries in Figure 12 contain offsets into the WIN32_FIND_DATA structure. When the consumer calls IRowset::GetData, data is transferred in one contiguous buffer. Rather than making the developer do pointer arithmetic, the map allows the developer to specify a data member.
The CMyProviderRowset class also contains the Execute function. The Execute function is what actually reads the data in from the native source. Figure 13 shows the wizard-generated Execute function. The function uses the Win32® FindFirstFile() and FindNextFile() APIs to retrieve information about the files in the directory and place them in instances of the CMyProviderWindowsFile class.
The directory to search in is represented by the m_strCommandText variable. The m_strCommandText variable contains the text represented by the ICommandText interface in the command object. If no directory is specified, it uses the current directory.
The method creates one entry for each file (corresponding to a row) and places it in the m_rgRowData data member. The CRowsetImpl class defines the m_rgRowData data member. The data in this array represents the entire table and is used throughout the templates.
/////////////////////////////////////////////////////////////////////
// MyProviderRS.H
HRESULT Execute(DBPARAMS * pParams, LONG* pcRowsAffected)
{
USES_CONVERSION;
BOOL bFound = FALSE;
HANDLE hFile;
LPTSTR szDir = (m_strCommandText == _T("")) ? _T("*.*") :
OLE2T(m_strCommandText);
CMyProviderWindowsFile wf;
hFile = FindFirstFile(szDir, &wf);
if (hFile == INVALID_HANDLE_VALUE)
return DB_E_ERRORSINCOMMAND;
LONG cFiles = 1;
BOOL bMoreFiles = TRUE;
while (bMoreFiles)
{
if (!m_rgRowData.Add(wf))
return E_OUTOFMEMORY;
bMoreFiles = FindNextFile(hFile, &wf);
cFiles++;
}
FindClose(hFile);
if (pcRowsAffected != NULL)
*pcRowsAffected = cFiles;
return S_OK;
}
Figure 13. The Execute method in CmyProviderWindowsFile
Start your provider by getting an idea of what data you will be sending to the consumer and under what conditions. It is especially important to determine if you need to support commands, transactions, and other optional objects. A good design up front will speed implementation and testing.
For our provider, we will use the default-generated code from the wizard and modify a few sections. Our provider will read data in from a text file. We will start by modifying the CMyProviderWindowsFile class. Figure 14 shows how the class should appear after modification.
////////////////////////////////////////////////////////////////////////
// MyProviderRS.h
class CTextData
{
public:
DWORD dwBookmark;
char szField1[16];
char szField2[16];
BEGIN_PROVIDER_COLUMN_MAP(CMyProviderWindowsFile)
PROVIDER_COLUMN_ENTRY_STR("Field1", 1, szField1)
PROVIDER_COLUMN_ENTRY_STR("Field2", 2, szField2)
END_PROVIDER_COLUMN_MAP()
};
Figure 14. The CTextData class
The szCommand and szText members represent each of two strings we will read from the file. The dwBookmark member will be used in the next section. The map entries correspond to the two fields. The PROVIDER_COLUMN_ENTRY_STR macro. The macro tells consumers that it contains a variable length field. Some consumers may depend upon this information.
We will need to modify all instances of the CMyProviderWindowsFile class to CTextData. You can do this by using a search and replace. We will also modify the Execute method to retrieve data from the text file.
/////////////////////////////////////////////////////////////////////////
// MyProviderRS.H
// Class: CMyProviderRowset
HRESULT CMyProviderRowset::Execute(DBPARAMS * pParams, LONG * pcRowsAffected)
{
USES_CONVERSION;
FILE* pFile;
TCHAR szString[256];
TCHAR szFile[MAX_PATH];
size_t nLength;
ObjectLock lock(this);
// From a filename, passed in as a command text, scan
// the file placing data in the data array.
if (m_strCommandText == (BSTR)NULL)
{
ATLTRACE("No filename specified");
return E_FAIL;
}
// Open the file.
_tcscpy(szFile, OLE2T(m_strCommandText));
if (szFile[0] == _T('\0') || ((pFile = fopen(&szFile[0], "r")) == NULL))
{
ATLTRACE("Could not open file");
return DB_E_NOTABLE;
}
// Scan and parse the file. The file should contain
// two strings per record.
LONG cFiles = 0;
while (fgets(szString, 256, pFile) != NULL)
{
nLength = strlen(szString);
szString[nLength-1] = '\0'; // Strip off trailing CR/LF
CTextData am;
_tcscpy(am.szField1, szString);
if (fgets(szString, 256, pFile) != NULL)
{
nLength = strlen(szString);
szString[nLength-1] = '\0'; // Strip off trailing CR/LF
_tcscpy(am.szField2, szString);
}
if (!m_rgRowData.Add(am))
{
ATLTRACE("Couldn't add data to array");
fclose(pFile);
return E_FAIL;
}
}
if (pcRowsAffected != NULL)
*pcRowsAffected = cFiles;
return S_OK;
}
Figure 15. Execute method in CmyProviderRowset
The function then opens the file and then uses fgets
to retrieve two strings. For each set of strings it creates an instance of the CTextFile class and places it into an array.
We will change the Execute methods in the schema rowset classes. We'll modify the code for the table schema to return a single filename. We could use any text file. That would mean a lot of work to find every text file on a drive. In this case, we'll use one filename that the user can modify. The code appears in the following segment.
/////////////////////////////////////////////////////////////////////
// MyProviderSess.H
// Class: CMyProviderSessionTRSchemaRowset
HRESULT Execute(LONG* pcRowsAffected, ULONG, const VARIANT*)
{
USES_CONVERSION;
CTextFile tf;
CTABLESRow trData;
lstrcpyW(trData.m_szType, OLESTR("TABLE"));
lstrcpyW(trData.m_szDesc, OLESTR("The Text File Table"));
TCHAR szFile[255];
tcscpy(szFile, _T("c:\\code\\myprov\\sample.txt"));
lstrcpynW(trData.m_szTable, T2OLE(szFile),
SIZEOF_MEMBER(CTABLESRow, m_szTable);
if (!m_rgRowData.Add(trData))
return E_OUTOFMEMORY;
*pcRowsAffected = 1;
return S_OK;
}
Figure 16. The CMyProviderSessionTRSchemaRowset Execute method
The code uses a class called CTABLESRow. CTABLESRow is defined by the OLE DB Provider Templates and contains all of the data members for the DBSCHEMA_TABLES rowset. We fill in a couple of the data fields to indicate the table name, description, and type. We add the instance of the structure to the m_rgRowData array.
The columns schema rowset works much the same as the tables. We fill out a structure called CCOLUMNSRow. We create two instances of the structure, one for each of the text columns. The code for the column schema rowset is shown below.
/////////////////////////////////////////////////////////////////////
// MyProviderSess.H
// Class: CMyProviderSessionColSchemaRowset
HRESULT Execute(LONG* pcRowsAffected, ULONG, const VARIANT*)
{
USES_CONVERSION;
CCOLUMNSRow trData[2];
// 'NULL' out everything before we begin.
memset(trData, 0, sizeof(CCOLUMNSRow)*2);
// Fill out common fields between columns.
for (long l=0; l<2; l++)
{
lstrcpyW(trData[l].m_szTableName,
OLESTR("c:\\code\\myprov\\sample.txt"));
trData[l].m_ulOrdinalPosition = l+1;
trData[l].m_bIsNullable = VARIANT_FALSE;
trData[l].m_bColumnHasDefault = VARIANT_FALSE;
trData[l].m_ulCharMaxLength = 16;
trData[l].m_ulColumnFlags = 0;
trData[l].m_nDataType = DBTYPE_STR;
// Add to the row data.
m_rgRowData.Add(trData[l]);
}
// Set up column names.
lstrcpyW(trData[0].m_szColumnName, OLESTR("Field1"));
lstrcpyW(trData[1].m_szColumnName, OLESTR("Field2"));
// Set up description columns
lstrcpyW(trData[0].m_szDescription, OLESTR("Field1"));
lstrcpyW(trData[1].m_szDescription, OLESTR("Field2"));
*pcRowsAffected = 2; // for the four columns
return S_OK;
}
Figure 17. The CMyProviderSessionColSchemaRowset Execute method
The final code snippet is for the provider types. We return only one type for the string. We use the CPROVIDER_TYPERow class from the provider templates. We fill out the structure and add it to the m_rgRowData member. The code for the provider type schema rowset is shown below. To find information on any of the schema rowsets, consult the OLE DB specification.
/////////////////////////////////////////////////////////////////////
// MyProviderSess.H
// Class: CMyProviderSessionPTSchemaRowset
HRESULT Execute(LONG* pcRowsAffected, ULONG, const VARIANT*)
{
USES_CONVERSION;
CPROVIDER_TYPERow trData;
memset(&trData, 0, sizeof(CPROVIDER_TYPERow));
lstrcpyW(trData.m_szName, OLESTR("String"));
trData.m_nType = DBTYPE_STR;
trData.m_ulSize = 16;
trData.m_bIsLong = VARIANT_FALSE;
trData.m_bIsNullable = VARIANT_FALSE;
trData.m_bCaseSensitive = VARIANT_FALSE;
trData.m_bSearchable = DB_UNSEARCHABLE;
m_rgRowData.Add(trData);
*pcRowsAffected = 1;
return S_OK;
}
Figure 18. The CMyProviderSessionPTSchemaRowset Execute method
To test a provider, you need a consumer. It helps if the consumer can match up with the provider. The OLE DB Consumer Templates are a thin wrapper around OLE DB and match with the provider COM objects. Since the source is shipped with the consumer templates, it is easy debug a provider with them. The consumer templates are also a very small and fast way to develop consumer applications.
We will create a default MFC AppWizard application for our test consumer. The test application is a simple dialog with OLE DB Consumer Template code added. To create the application, follow these steps:
Note You can ignore the OLE automation step if you add a CoInitialize in CTestProvApp::InitInstance.
Figure 19. Example dialog for test dialog
If you bring up the resource file and list the dialog (IDD_TESTPROV_DIALOG), you should see the dialog shown in Figure 19. We will place two listboxes in the dialog, one for each string in the rowset. Turn off the sort property for both listboxes (press alt-enter while a listbox is selected, select the styles tab, and unclick the sort checkbox). We will also place a run button on the dialog as well to actually fetch the file. The finished dialog should look like Figure 20.
Figure 20. The finished dialog in TestProv
Open the header file for the dialog class (in this case TestProvDlg.H). Add the following code to the header file (outside of any class declarations).
////////////////////////////////////////////////////////////////////////
// TestProvDlg.h
class CProvider
{
// Attributes
public:
char szField1[16];
char szField2[16];
// Binding Maps
BEGIN_COLUMN_MAP(CProvider)
COLUMN_ENTRY(1, szField1)
COLUMN_ENTRY(2, szField2)
END_COLUMN_MAP()
};
Figure 21. The user record in TestProvDlg.h
The code represents a "user record" that defines what columns will be in the rowset. When the client calls IAccessor::CreateAccessor, it uses these entries to specify which columns to bind. The OLE DB consumer templates allow you to dynamically bind columns as well. The COLUMN_ENTRY macros are the client-side version of the PROVIDER_COLUMN_ENTRY macros. The two COLUMN_ENTRY macros specify the ordinal, type, length, and data member for the two strings.
Add a handler function for the Run button (by doing control-double-click on the Run button). Place the following code in the function:
///////////////////////////////////////////////////////////////////////
// TestProvDlg.cpp
void CtestProvDlg::OnRun()
{
CCommand<CAccessor<CProvider> > table;
CDataSource source;
CSession session;
if (source.Open(CLSID_MyProvider, NULL) != S_OK)
return;
if (session.Open(source) != S_OK)
return;
if (table.Open(session, _T("c:\\code\\myprov\\sample.txt")) != S_OK)
return;
while (table.MoveNext() == S_OK)
{
m_ctlString1.AddString(table.szField1);
m_ctlString2.AddString(table.szField2);
}
}
Figure 22. The OnRun handler in TestProvDlg.cpp
The CCommand, CDataSource, and CSession classes all belong to the OLE DB Consumer Templates. Each class mimics a COM object in the provider. The CCommand object takes the CProvider class we just declared in the header file as a template parameter. The CProvider parameter represents bindings we will use to access the data from the provider.
if (source.Open("MyProvider.MyProvider.1", NULL) != S_OK)
return;
if (session.Open(source) != S_OK)
return;
if (table.Open(session, _T("c:\\code\\myprov\\sample.txt")) != S_OK)
return;
Figure 23. Open code for the data source, session, and command
The lines to open each of the classes create each COM object in the provider. To locate the provider, we use the ProgID of the provider. You can get the ProgID from the system registry or by looking in the MyProvider.rgs file (in the provider's directory and search for the ProgID key).
The foo.txt file is in the sample code directory with this article. To create a file of your own, use an editor and type an even number of strings hitting return between each string. Do not forget to change the pathname if you move the file.
We pass in the string, c:\\public\\testprov\foo.txt", in the table.Open line. If you step into the Open call, you will see that this string is passed to the SetCommandText method in the provider. If you remember, we used that string in our ICommandText::Execute() function.
while (table.MoveNext() == S_OK)
{
m_ctlString1.AddString(table.szField1);
m_ctlString2.AddString(table.szField2);
}
Figure 24. The data fetch and movement code
To fetch the data, we just call MoveNext() on the table. The MoveNext() API calls IRowset::GetNextRows(),
the GetRowCount() and GetData() functions. When there are no more rows (that is, when the current position in the rowset is greater than GetRowCount()), the loop terminates.
Note When there are no more rows, providers return DB_S_ENDOFROWSET. The DB_S_ENDOFROWSET value is not an error. You should always check against S_OK to cancel a data fetch loop and not use the SUCCEEDED macro.
You should now be able to build and test the program.
We can do many other things with providers. We can make them update, handle transactions, and perform more robust row fetching algorithms. While we do not cover all of these features in this article, it is useful to know how to add them.
In this section, we will add the IRowsetLocate interface to the CMyProviderRowset class. In almost all cases, you will start by adding an interface to an existing COM object. You can then test it by adding more calls from the consumer templates. Our example demonstrates:
The IRowsetLocate interface inherits from the IRowset interface. We want to use the implementation in the OLE DB Provider Templates for IRowset. To add the IRowsetLocate interface, we have the CMyProviderRowset inherit from the IRowsetLocateImpl class. We will provide the implementation for the IRowsetLocateImpl class (see Figure 27).
Adding the IRowsetLocate interface is a bit different from most interfaces. To make the VTABLEs line up, the OLE DB Provider Templates have a template parameter to handle the derived interface. Figure 25 shows the new inheritance chain. The fourth, fifth, and sixth parameters are all added. We use the defaults for the fourth and fifth parameters but specify IRowsetLocateImpl as the sixth parameter. The IRowsetLocateImpl class is our own but it also takes two template parameters. The template parameters hook up the IRowsetLocate interface to the CMyProviderRowset class. To add most interfaces, you can skip this step and move to the next one. Only the IRowsetLocate and IRowsetScroll interfaces need to be handled in this way.
//////////////////////////////////////////////////////////////////////
// MyProviderRS.h
// CMyProviderRowset
class CMyProviderRowset : public CRowsetImpl< CMyProviderRowset,
CTextData, CMyProviderCommand, CSimpleArray<CTextData>,
CSimpleRow,
IRowsetLocateImpl<CMyProviderRowset, IRowsetLocate> >
Figure 25. The new inheritance list for CmyProviderRowset
We then need to tell the CMyProviderRowset to QueryInterface for the IRowsetLocate interface. We add the line COM_INTERFACE_ENTRY (IRowsetLocate) to the map. The map should look like Figure 26. We also need to hook our map into the CRowsetImpl class. We add in the COM_INTERFACE_ENTRY_CHAIN macro to hook in the CRowsetImpl map. We also create a typdef called _RowsetBaseClass that consists of the inheritance information. This typedef is arbitrary and can be ignored.
////////////////////////////////////////////////////////////////////////
// MyProviderRS.h
typedef CRowsetImpl< CMyProviderRowset, CTextData, CMyProviderCommand, CSimpleArray<CTextData>, CSimpleRow, IRowsetLocateImpl<CMyProviderRowset, IRowsetLocate> > _RowsetBaseClass;
BEGIN_COM_MAP(CMyProviderRowset)
COM_INTERFACE_ENTRY(IRowsetLocate)
COM_INTERFACE_ENTRY_CHAIN(_RowsetBaseClass)
END_COM_MAP()
Figure 26. The new interface map for CmyProviderRowset
We will then go ahead and implement the IRowsetLocateImpl interface. The interface definition can be found in the OLE DB SDK (see Appendix C). Look in the OLEDB.H file in the OLE DB SDK (includes directory) and locate the definition. To create the new interface, do the following:
////////////////////////////////////////////////////////////////////////
// RowLoc.h
////////////////////////////////////////////////////////////////////////
// class IRowsetLocateImpl
template <class T, class RowsetInterface,
class RowClass = CSimpleRow,
class MapClass = CSimpleMap < RowClass::KeyType, RowClass* >,
class BookmarkKeyType = LONG, class BookmarkType = LONG,
class BookmarkMapClass = CSimpleMap < BookmarkKeyType, BookmarkType > >
class ATL_NO_VTABLE IRowsetLocateImpl : public IRowsetImpl<T, RowsetInterface, RowClass, MapClass> {
public:
STDMETHOD (Compare)(HCHAPTER hReserved, ULONG cbBookmark1,
const BYTE * pBookmark1, ULONG cbBookmark2, const BYTE *
pBookmark2, DBCOMPARE * pComparison)
{
return S_OK;
}
STDMETHOD (GetRowsAt)(HWATCHREGION hReserved1, HCHAPTER hReserved2,
ULONG cbBookmark, const BYTE * pBookmark, LONG lRowsOffset,
LONG cRows, ULONG * pcRowsObtained, HROW ** prghRows)
{
return S_OK;
}
STDMETHOD (GetRowsByBookmark)(HCHAPTER hReserved, ULONG cRows,
const ULONG rgcbBookmarks[], const BYTE * rgpBookmarks[],
HROW rghRows[], DBROWSTATUS rgRowStatus[])
{
return S_OK;
}
STDMETHOD (Hash)(HCHAPTER hReserved, ULONG cBookmarks,
const ULONG rgcbBookmarks[], const BYTE * rgpBookmarks[],
DWORD rgHashedValues[], DBROWSTATUS rgBookmarkStatus[])
{
ATLTRACENOTIMPL("IRowsetLocateImpl::GetRowsByBookmark");
}
};
Figure 27. The definition for the IRowsetLocateImpl class
////////////////////////////////////////////////////////////////////////
// RowLoc.h
////////////////////////////////////////////////////////////////////////
// class IRowsetLocateImpl
template <class T, class RowsetInterface,
class RowClass = CSimpleRow,
class MapClass = CSimpleMap < RowClass::KeyType, RowClass* >,
class BookmarkKeyType = LONG, class BookmarkType = LONG,
class BookmarkMapClass = CSimpleMap < BookmarkKeyType, BookmarkType > >
class ATL_NO_VTABLE IRowsetLocateImpl : public IRowsetImpl<T, RowsetInterface, RowClass, MapClass> {
public:
STDMETHOD (Compare)(HCHAPTER hReserved, ULONG cbBookmark1,
const BYTE * pBookmark1, ULONG cbBookmark2, const BYTE *
pBookmark2, DBCOMPARE * pComparison)
{
ATLTRACE("IRowsetLocateImpl::Compare");
HRESULT hr = ValidateBookmark(cbBookmark1, pBookmark1);
if (hr != S_OK)
return hr;
hr = ValidateBookmark(cbBookmark2, pBookmark2);
if (hr != S_OK)
return hr;
if (pComparison == NULL)
return E_INVALIDARG;
// Return the value based on the bookmark values.
if (*pBookmark1 == *pBookmark2)
*pComparison = DBCOMPARE_EQ;
if (*pBookmark1 < *pBookmark2)
*pComparison = DBCOMPARE_LT;
if (*pBookmark1 > *pBookmark2)
*pComparison = DBCOMPARE_GT;
return S_OK;
}
STDMETHOD (GetRowsAt)(HWATCHREGION hReserved1, HCHAPTER hReserved2,
ULONG cbBookmark, const BYTE * pBookmark, LONG lRowsOffset,
LONG cRows, ULONG * pcRowsObtained, HROW ** prghRows)
{
ATLTRACE("IRowsetLocateImpl::GetRowsAt");
// Check bookmark.
HRESULT hr = ValidateBookmark(cbBookmark, pBookmark);
if (hr != S_OK)
return hr;
// Check the other pointers.
if (pcRowsObtained == NULL || prghRows == NULL)
return E_INVALIDARG;
// Set the current row position to the bookmark. Handle any
// normal values.
m_iRowset = *pBookmark;
if (*pBookmark == DBBMK_FIRST)
m_iRowset = 1;
if (*pBookmark == DBBMK_LAST)
m_iRowset = m_pCommandHelper->GetRowCount();
// Call IRowsetImpl::GetNextRows to actually get the rows.
return GetNextRows(hReserved2, lRowsOffset,
cRows, pcRowsObtained, prghRows);
}
STDMETHOD (GetRowsByBookmark)(HCHAPTER hReserved, ULONG cRows,
const ULONG rgcbBookmarks[], const BYTE * rgpBookmarks[],
HROW rghRows[], DBROWSTATUS rgRowStatus[])
{
HRESULT hr = S_OK;
ATLTRACE("IRowsetLocateImpl::GetRowsByBookmark");
if (rgcbBookmarks == NULL || rgpBookmarks == NULL ||
rghRows == NULL)
return E_INVALIDARG;
if (cRows == 0)
return S_OK; // No rows fetched in this case.
bool bErrors = false;
for (ULONG l=0; l<cRows; l++)
{
// Validate each bookmark before fetching the row.
// Note, it is an error for the bookmark to be one of
// the standard values.
hr = ValidateBookmark(rgcbBookmarks[l],
rgpBookmarks[l]);
if (hr != S_OK)
{
bErrors = TRUE;
if (rgRowStatus != NULL)
rgRowStatus[l] = DBROWSTATUS_E_INVALID;
}
// Fetch the row, we now that it is a valid row after
// validation.
ULONG ulRowsObtained = 0;
if (CreateRow((long)*rgpBookmarks[l], ulRowsObtained,
&rghRows[l]) != S_OK)
{
bErrors = TRUE;
}
else
{
if (rgRowStatus != NULL)
rgRowStatus[l] = DBROWSTATUS_S_OK;
}
}
if (bErrors)
return DB_S_ERRORSOCCURRED;
else
return hr;
}
STDMETHOD (Hash)(HCHAPTER hReserved, ULONG cBookmarks,
const ULONG rgcbBookmarks[], const BYTE * rgpBookmarks[],
DWORD rgHashedValues[], DBROWSTATUS rgBookmarkStatus[])
{
ATLTRACENOTIMPL("IRowsetLocateImpl::GetRowsByBookmark");
}
// Implementation
protected:
HRESULT ValidateBookmark(ULONG cbBookmark, const BYTE* pBookmark)
{
if (cbBookmark == 0 || pBookmark == NULL)
return E_INVALIDARG;
// All of our bookmarks are DWORDs, if they are anything other
// than sizeof(DWORD) then we have an invalid bookmark.
if (cbBookmark != sizeof(DWORD))
{
ATLTRACE("Bookmarks are invalid length");
return DB_E_BADBOOKMARK;
}
// If the contents of our bookmarks are less than 0 or greater
// than rowcount, then they are invalid.
UINT nRows = m_pCommandHelper->GetRowCount();
if ((*pBookmark <= 0 || *pBookmark > nRows)
&& *pBookmark != DBBMK_FIRST && *pBookmark !=
DBBMK_LAST)
{
ATLTRACE("Bookmark has invalid range");
return DB_E_BADBOOKMARK;
}
return S_OK;
}
};
Figure 28. The completed implementation of IrowsetLocateImpl
The IRowsetLocate interface requires the implementation of bookmarks. Bookmarks are indices into a rowset that allow high-speed access of data. The provider determines what bookmarks can uniquely identify a row. In our case, we use the index of the m_rgRowData array. By doing the array index we can quickly fetch a specified row.
Since we can receive a bookmark in any format (i.e. bookmarks can be any length) or any index value, we have a function ValidateBookmark() to ensure the value is useful. The function first checks to see that we have a valid bookmark pointer. We then check to see that the bookmark is the size of a DWORD. We specified that the dwBookmark entry would be a DWORD in Figure 13. Finally, we check that the index points to a valid location in the array.
Note There are specific bookmark values—DBMRK_FIRST and DBMRK_LAST—that are valid.
The Compare() method takes two bookmarks and determines if one is less than, greater than, or equal to the other. Our function validates both bookmarks and then compares the array indices against one another.
The GetRowsAt() function works the same as the IRowset::GetNextRows method except that it fetches records from a bookmark. We validate the bookmark and then actually call IRowsetImpl::GetNextRows. If we have a DBMRK_FIRST or DBMRK_LAST value, we set it to the first or last row in the array.
The GetRowsByBookmark() function fetches one row at a number of locations. It is useful to get the first, twenty-third, seventy-second, and so on rows from a rowset. We validate each of the bookmarks and then call a function CreateRow() in IRowsetImpl. CreateRow() creates a row if necessary and places it in the array specified by the user. If an error occurs and the user specified a status array then we put an appropriate error code in.
The Hash() method is used to translate bookmark values into actual row handles. We have a fixed value for our bookmark so we don't really require this method. The implementation is used only as a reference.
The last thing we do is to handle the IColumnsInfo::GetColumnsInfo() call. We would normally use the PROVIDER_COLUMN_ENTRY macros to do this. However, a consumer may or may not want to use bookmarks. We must be able to change the columns the provider returns depending upon whether the consumer asks for a bookmark or not.
To handle the IColumnsInfo::GetColumnsInfo() call, delete the PROVIDER_COLUMN map in the CTextData class. The PROVIDER_COLUMN_MAP macro defines a function called GetColumnInfo().
We need to define our own GetColumnInfo()
function. The function prototype should look like:
////////////////////////////////////////////////////////////////////////
// MyProviderRS.H
class CTextData
{
…
// NOTE: Be sure you removed the PROVIDER_COLUMN_MAP!
static ATLCOLUMNINFO* GetColumnInfo(CMyProviderRowset* pThis,
ULONG* pcCols);
static ATLCOLUMNINFO* GetColumnInfo(CMyProviderCommand* pThis,
ULONG* pcCols);
…
};
Figure 29. The GetColumnInfo declaration in CtextData
We then implement the GetColumnInfo()
function in the MyProviderRS.cpp file. Add the code in Figure 30 to your file.
////////////////////////////////////////////////////////////////////
// MyProviderRS.cpp
template <class TInterface>
ATLCOLUMNINFO* CommonGetColInfo(IUnknown* pPropsUnk, ULONG* pcCols)
{
static ATLCOLUMNINFO _rgColumns[5];
ULONG ulCols = 0;
CComQIPtr<TInterface> spProps = pPropsUnk;
CDBPropIDSet set(DBPROPSET_ROWSET);
set.AddPropertyID(DBPROP_BOOKMARKS);
DBPROPSET* pPropSet = NULL;
ULONG ulPropSet = 0;
HRESULT hr;
if (spProps)
hr = spProps->GetProperties(1, &set, &ulPropSet, &pPropSet);
// Check the property flag for bookmarks, if it is set, set the
// zero ordinal entry in the column map with the bookmark
// information.
if (pPropSet)
{
CComVariant var = pPropSet->rgProperties[0].vValue;
CoTaskMemFree(pPropSet->rgProperties);
CoTaskMemFree(pPropSet);
if ((SUCCEEDED(hr) && (var.boolVal == VARIANT_TRUE)))
{
ADD_COLUMN_ENTRY_EX(ulCols, OLESTR("Bookmark"), 0,
sizeof(DWORD), DBTYPE_BYTES,
0, 0, GUID_NULL, CAgentMan, dwBookmark,
DBCOLUMNFLAGS_ISBOOKMARK)
ulCols++;
}
}
// Next set the other columns up.
ADD_COLUMN_ENTRY_EX(ulCols, OLESTR("Field1"), 1, 16, DBTYPE_STR,
0xFF, 0xFF, GUID_NULL, CTextData, szField1)
ulCols++;
ADD_COLUMN_ENTRY_EX(ulCols, OLESTR("Field2"), 2, 16, DBTYPE_STR,
0xFF, 0xFF, GUID_NULL, CTextData, szField2)
ulCols++;
if (pcCols != NULL)
*pcCols = ulCols;
return _rgColumns;
}
ATLCOLUMNINFO* CTextData::GetColumnInfo(CMyProviderCommand* pThis,
ULONG* pcCols)
{
return CommonGetColInfo<ICommandProperties>(pThis->GetUnknown(),
pcCols);
}
ATLCOLUMNINFO* CAgentMan::GetColumnInfo(RUpdateRowset* pThis, ULONG* pcCols)
{
return CommonGetColInfo<IRowsetInfo>(pThis->GetUnknown(), pcCols);
}
Figure 30. Implementation of _GetColumnInfo in MyProviderRS.cpp
Our GetColumnInfo() function first checks to see if a property called DBPROP_IRowsetLocate is set. OLE DB has properties for each of the optional interfaces off the rowset object. If the consumer wishes to use one of these optional interfaces, it sets a property to true. The provider can then check this property and take special action based upon it.
In our implementation, we get the property by using the pointer to the command object. This pointer represents the rowset or command class. Since we use templates here we have to pass this in as a void pointer or the code will not compile.
We specify a static array to contain the column information. If the consumer does not want the bookmark column, we waste an entry in the array. We could dynamically allocate this array if we wanted to, but we would need to make sure to destroy it properly. We use our own macros—ADD_COLUMN_ENTRY and ADD_COLUMN_ENTRY_EX—to insert the information into the array. The macros are shown in Figure 31. You can add the macro to the MyProviderRS.H file.
////////////////////////////////////////////////////////////////////////
// MyProviderRS.h
#define ADD_COLUMN_ENTRY(ulCols, name, ordinal, colSize, type, precision, scale, guid, dataClass, member) \
_rgColumns[ulCols].pwszName = (LPOLESTR)name; \
_rgColumns[ulCols].pTypeInfo = (ITypeInfo*)NULL; \
_rgColumns[ulCols].iOrdinal = (ULONG)ordinal; \
_rgColumns[ulCols].dwFlags = 0; \
_rgColumns[ulCols].ulColumnSize = (ULONG)colSize; \
_rgColumns[ulCols].wType = (DBTYPE)type; \
_rgColumns[ulCols].bPrecision = (BYTE)precision; \
_rgColumns[ulCols].bScale = (BYTE)scale; \
_rgColumns[ulCols].cbOffset = offsetof(dataClass, member);
#define ADD_COLUMN_ENTRY_EX(ulCols, name, ordinal, colSize, type, precision, scale, guid, dataClass, member, flags) \
_rgColumns[ulCols].pwszName = (LPOLESTR)name; \
_rgColumns[ulCols].pTypeInfo = (ITypeInfo*)NULL; \
_rgColumns[ulCols].iOrdinal = (ULONG)ordinal; \
_rgColumns[ulCols].dwFlags = flags; \
_rgColumns[ulCols].ulColumnSize = (ULONG)colSize; \
_rgColumns[ulCols].wType = (DBTYPE)type; \
_rgColumns[ulCols].bPrecision = (BYTE)precision; \
_rgColumns[ulCols].bScale = (BYTE)scale; \
_rgColumns[ulCols].cbOffset = offsetof(dataClass, member); \
memset(&(_rgColumns[ulCols].columnid), 0, sizeof(DBID)); \
_rgColumns[ulCols].columnid.uName.pwszName = (LPOLESTR)name;
Figure 31. The ADD_COLUMN_ENTRY macro in MyProviderRS.H
To test the code in the consumer, we need to make a few changes to the OnRun handler. The code should look like Figure 32. The first change to the function is that we add code to add a property to the property set. The code sets the DBPROP_IRowsetLocate property to true. That tells the provider that we want the bookmark column.
//////////////////////////////////////////////////////////////////////
// TestProv Consumer Application in TestProvDlg.cpp
void CTestProvDlg::OnRun()
{
CCommand<CAccessor<CProvider> > table;
CDataSource source;
CSession session;
if (source.Open("MyProvider.MyProvider.1", NULL, NULL, NULL,
NULL) != S_OK)
return;
if (session.Open(source) != S_OK)
return;
CDBPropSet propset(DBPROPSET_ROWSET);
propset.AddProperty(DBPROP_IRowsetLocate, true);
if (table.Open(session, _T("c:\\public\\testprf2\\foo.txt"),
&propset) != S_OK)
return;
CBookmark<4> tempBookmark;
ULONG ulCount=0;
while (table.MoveNext() == S_OK)
{
DBCOMPARE compare;
if (ulCount == 2)
tempBookmark = table.bookmark;
HRESULT hr = table.Compare(table.dwBookmark, table.dwBookmark,
&compare);
if (FAILED(hr))
ATLTRACE(_T("Compare failed: 0x%X\n"), hr);
else
_ASSERTE(compare == DBCOMPARE_EQ);
m_ctlString1.AddString(table.szField1);
m_ctlString2.AddString(table.szField2);
ulCount++;
}
table.MoveToBookmark(tempBookmark);
m_ctlString1.AddString(table.szField1);
m_ctlString2.AddString(table.szField2);
}
Figure 32. New OnRun handler in TestProv
The while loop contains code to call the Compare() method in the IRowsetLocate interface. The code we have should always pass since we are comparing the same bookmarks. We also store one bookmark in a temporary variable so that we can use it after the while loop finishes to call the MoveToBookmark() function in the consumer templates. The MoveToBookmark() function calls the GetRowsAt() method in IRowsetLocate.
We also need to update the user record in the consumer. We should add an entry in the class to handle a bookmark and an entry in the COLUMN_MAP. Figure 33 shows this.
///////////////////////////////////////////////////////////////////////
// TestProvDlg.cpp
class CProvider
{
// Attributes
public:
CBookmark<4> bookmark; // Add this line
char szCommand[16];
char szText[16];
// Binding Maps
BEGIN_ACCESSOR_MAP(CProvider, 1)
BEGIN_ACCESSOR(0, true) // auto accessor
BOOKMARK_ENTRY(4, bookmark) // Add this line
COLUMN_ENTRY(1, DBTYPE_STR, 16, szField1)
COLUMN_ENTRY(2, DBTYPE_STR, 16, szField2)
END_ACCESSOR()
END_ACCESSOR_MAP()
};
Figure 33. The updated user record in TestProv
Once you have updated the code, you should be able to build and execute the provider with the IRowsetLocate interface.
In order to make providers more consistent, the Data Access Group provides a set of OLE DB Conformance Tests. The tests check all aspects of your provider and give you reasonable assurance that your provider functions as expected. You can find the OLE DB Conformance Tests on the Microsoft Data Access SDK (MSDASDK). The SDK ships with Visual Studio® 6.0 and Visual C++ 6.0. This section focuses on things you should do to pass the conformance tests. For information on running the OLE DB Conformance Tests, consult the SDK.
The Visual C++ 6.0 Provider Templates added a number of hooking functions to allow you to check values and properties. Most of these functions are new to Visual C++ 6.0 and were added in response to using the conformance tests.
Note You will need to add several validation functions for your provider to pass the OLE DB Conformance Tests.
Our provider requires two validation routines. The first routine is called ValidateCommandID.
The ValidateCommandID method is part of your rowset class. It is called during the creation of the rowset by the provider templates. The sample uses this routine to tell consumers that we do not support indexes. The first call is to the CRowsetImpl::ValidateCommandID class (note that we use the _RowsetBaseClass typedef we added in Figure 26 so we don't have to type that long line of template arguments). The next thing we do is to return DB_E_NOINDEX if the index parameter is not NULL (this would indicate the consumer wants to use an index on us). For more information on command IDs, check out the OLE DB Specification and look for IOpenRowset::OpenRowset.
/////////////////////////////////////////////////////////////////////
// MyProviderRS.H
// Class: CMyProviderRowset
HRESULT ValidateCommandID(DBID* pTableID, DBID* pIndexID)
{
HRESULT hr = _RowsetBaseClass::ValidateCommandID(pTableID, pIndexID);
if (hr != S_OK)
return hr;
if (pIndexID != NULL)
return DB_E_NOINDEX; // We don't support indexes.
return S_OK;
}
Figure 34. Validation routines for the OLE DB conformance tests
The second routine is necessary for our bookmark support. Some properties in OLE DB are "chained." When one property changes, another property changes as well. Bookmark support contains several properties that are chained. We add an override called OnPropertyChanged() to the rowset class to handle the properties.
/////////////////////////////////////////////////////////////////////
// MyProviderRS.H
// Class: CMyProviderRowset
HRESULT OnPropertyChanged(ULONG iCurSet, DBPROP* pDBProp)
{
ATLASSERT(pDBProp != NULL);
DWORD dwPropertyID = pDBProp->dwPropertyID;
if (dwPropertyID == DBPROP_IRowsetLocate ||
dwPropertyID == DBPROP_LITERALBOOKMARKS ||
dwPropertyID == DBPROP_ORDEREDBOOKMARKS)
{
CComVariant var = pDBProp->vValue;
if (var.boolVal == VARIANT_TRUE)
{
// Set the bookmarks property as these are chained.
CComVariant bookVar(true);
CDBPropSet set(DBPROPSET_ROWSET);
set.AddProperty(DBPROP_BOOKMARKS, bookVar);
return SetProperties(1, &set);
}
}
return S_OK;
}
Figure 35. The OnPropertyChanged method
The provider templates call the method whenever someone changes a property on the DBPROPSET_ROWSET group. If you want to handle properties for other groups, you would add them to the appropriate object (for example, DBPROPSET_SESSION checks would go in the CMyProviderSession class).
The code first checks to see if the property is linked to another. If the property is being chained, it will set the DBPROP_BOOKMARKS property to true. Appendix C of the OLE DB Reference contains information on properties. This information will also tell you if the property is chained to another one.
You may also wish to add in the IsValidValue routine to your code. The templates call IsValidValue when attempting to set a property. You would override this method if you require additional processing when setting a property value. You can have one of these methods for each property set.
OLE DB providers are the way you deliver data to the Universal Data Access strategy. They are a series of COM components that react to interface calls from a consumer. Each component contains several interfaces. Not all of the interfaces need to be implemented. The provider can support a minimal amount of functionality or a full-blown production-quality provider by implementing more interfaces.
The OLE DB Provider Templates, shipping with Visual C++ 6.0, are a great way to build providers. You get the support of the wizards and implementation for 20 of the most common and required interfaces. In fact, you can just run the wizard and get a fully functional provider.
Adding features to the wizard-generated code usually involves adding support for COM interfaces. By using some of the support built into the OLE DB Provider Templates, adding functionality can be easier because of the support classes.
Interfaces | Fully Supported |
IAccessor | Yes |
IColumnsInfo | Yes |
ICommand | Yes |
ICommandProperties | Yes |
ICommandText | Yes |
IConvertType | Yes |
IDBCreateCommand | Yes |
IDBCreateSession | Yes |
IDBDataSourceAdmin | No |
IDBInfo | No |
IDBInitialize | Yes |
IDBProperties | Yes |
IGetDataSource | Yes |
IOpenRowset | Yes |
IPersist | Yes |
IRowset | Yes |
IRowsetChange | No |
IRowsetInfo | Yes |
ISessionProperties | Yes |
ITransactionLocal | No |
IDBSchemaRowset | Yes |
This section contains some useful web sites you may wish to visit.
www.microsoft.com/data/: This is the main Universal Data Access web site. It contains sample software, white papers, and more.
www.microsoft.com/data/oledb/: This is the web site for the OLE DB SDK. You can download the OLE DB specification from this site.
http://msdn.microsoft.com/visualc/: The definitive Microsoft resource for information on Visual C++; watch this site for information on Visual C++ 6.0 including additional technical white papers.
There have been several changes in the OLE DB Provider Templates from the Visual C++ 5.0 Technology Preview to Visual C++ 6.0. Most of the changes are around the rowset and property handling. Also, most of the changes were necessitated by the OLE DB Conformance Tests.
The major change between the Technology Preview and the Visual C++ 6.0 templates is the removal of the command helper. The command helper class evolved into more support in CRowsetImpl and in the data class. The reason for the change was to facilitate multiple rowsets on a provider. Multiple rowsets can exist to support things like schema rowsets or multiple static rowsets.
The command helper strategy would have required the user to copy the command helper, rowset, and the command class for each new rowset type of the provider. That would add a great deal of code size to the provider. The new strategy requires only a new rowset and data class for each supported rowset.
Many of the wizards for OLE DB and ADO require that there be some information available about the rowset without opening it. This is usually supported by metadata. OLE DB handles metadata through schema rowsets. The OLE DB Provider Templates provide implementation for the IDBSchemaRowset interface.
We did some work in the property code to make adding properties easier. In the Technology Preview, you specified a property with the syntax of:
PROPERTY_INFO_ENTRY(IAccessor, VT_BOOL, DBPROPFLAGS_ROWSET |
DBPROPFLAGS_READ, VARIANT_TRUE, 0)
In the Visual C++ 6.0 templates, we put in the defaults for each of the properties so you can specify the properties more easily. The new syntax looks like:
PROPERTY_INFO_ENTRY(IAccessor)
The PROPERTY_INFO_ENTRY macro specifies a default value for the property. If you wish to set your own value, you can use the PROPERTY_INFO_ENTRY_VALUE macro and specify the appropriate property data type.
If you used the Visual C++ Technology Preview, you might be wondering how to move the provider to Visual C++ 6.0. Here's a checklist of things to do to port your provider: