- Table of Contents
- Copyright
- About the Authors
- About the Contributors
- Acknowledgments
- Tell Us What You Think!
- Introduction
- How to Use This Book
- What You Need to Use This Book
- What's New in Visual C++ 6.0
- Contacting the Main Author
- Part I: Introduction
- Chapter 1. The Visual C++ 6.0 Environment
- Part II: MFC Programming
- Chapter 2. MFC Class Library Overview
- Chapter 3. MFC Message Handling Mechanism
- Chapter 4. The Document View Architecture
- Chapter 5. Creating and Using Dialog Boxes
- Chapter 6. Working with Device Contexts and GDI Objects
- Chapter 7. Creating and Using Property Sheets
- Chapter 8. Working with the File System
- Chapter 9. Using Serialization with File and Archive Objects
- Part III: Internet Programming with MFC
- Chapter 10. MFC and the Internet Server API (ISAPI)
- Chapter 11. The WinInet API
- Chapter 12. MFC HTML Support
- Part IV: Advanced Programming Topics
- Chapter 13. Using the Standard C++ Library
- Chapter 14. Error Detection and Exception Handling Techniques
- Chapter 15. Debugging and Profiling Strategies
- Chapter 16. Multithreading
- Chapter 17. Using Scripting and Other Tools to Automate the Visual C++ IDE
- Part V: Database Programming
- Chapter 18. Creating Custom AppWizards
- Chapter 19. Database Overview
- Chapter 20. ODBC Programming
- Chapter 21. MFC Database Classes
- Chapter 22. Using OLE DB
- OLE DB Architecture
- Developing an OLE DB Application
- Retrieving Column Information
- Using Transactions
- Using Enumerators
- Summary
- Chapter 23. Programming with ADO
- Part VI: MFC Support for COM and ActiveX
- Chapter 24. Overview of COM and Active Technologies
- Chapter 25. Active Documents
- Chapter 26. Active Containers
- Chapter 27. Active Servers
- Chapter 28. ActiveX Controls
- Part VII: Using the Active Template Library
- Chapter 29. ATL Architecture
- Chapter 30. Creating COM Objects Using ATL
- Chapter 31. Creating ActiveX Controls Using ATL
- Chapter 32. Using ATL to Create MTS and COM+ Components
- Part VIII: Finishing Touches
- Chapter 33. Adding Windows Help
- Part IX: Appendix
Developing an OLE DB Application
OLE DB development used to be quite difficult. Many OLE DB developers were forced to use the slower ActiveX Data Objects (ADO) interface to access OLE DB data sources simply because of the development time and training required to use OLE DB.
Visual C++ 6.0 changed all that. Now, OLE DB consumers can be developed using either the MFC or the ATL AppWizards. ATL executables, new to version 6, are quite smaller than corresponding MFC applications and tend to run more quickly. MFC may be slightly easier to develop, but MFC uses the same ATL classes developed in the ATL AppWizard to access OLE DB data.
This section takes you through a development process to add OLE DB functionality to an existing ATL application.
Adding an ATL OLE DB Consumer Object
To follow this example, begin a new ATL COM EXE project. Create a dialog box to contain database column information and a menu to aid in navigation. This will usually take some time; if you want to see an example or skip this step, the starting configuration for this example is contained in the Chap22Start directory.
After you've set up a new ATL COM EXE application, click on Insert, New ATL Object from the Visual Studio menu. This opens the ATL Object Wizard dialog box. Select Data Access under Category and choose Consumer, as shown in Figure 22.2.
Figure 22.2 The ATL Object Wizard makes it easy to create a new OLE DB Consumer.
Click Next. The ATL Object Wizard Properties dialog box opens. Immediately click the Select Datasource button, as shown in Figure 22.3.
Figure 22.3 The ATL Object Wizard enables you to add properties or select data sources for your OLE DB Consumer.
After clicking the Select Datasource button, the Data Link Properties dialog box opens. Here, you select which OLE DB provider you want to write a consumer for. Usually, the choice is the ever-popular Microsoft OLE DB Provider for ODBC Drivers, as shown in Figure 22.4.
Figure 22.4 The Microsoft OLE DB Provider for ODBC Drivers enables you to access any ODBC data source as if it were an OLE DB data source.
When you click the Next button or the Connection tab, the Data Link Properties dialog box enables you to choose the name of your data source, as well as to enter username, password, and catalog information, if appropriate. For this example, VCUnleashed was chosen as the ODBC data source.
When you click OK, a list of tables available in that data source opens inside the Select Database Table dialog box. In Figure 22.5, the Employee table was chosen. When you choose which table or tables you want to access with your OLE DB consumer, click OK.
Figure 22.5 The Select Database Table dialog box enables you to choose the table for your OLE DB data source.
When you return to the ATL Wizard Properties dialog box, you can see that the names of your header file, class, and OLE DB accessor are filled in. Choose whether you want Table or Command for your data source (usually Command) and whether you want change, insert, and delete capabilities added to your OLE DB rowset (you usually do). When you're finished, click OK. A new OLE DB consumer is then generated and placed inside your Visual C++ project.
Consumer Components
Now your ATL project has a newly generated OLE DB consumer class. For many applications, you won't need to change this project. However, in this application, you need to make some changes to the consumer. This section describes the components and the changes that were made, and why.
CRowset
The main goal of an OLE DB consumer is to allow the developer to access OLE DB data in a rowset format. The CRowset class contains functionality similar to the CRecordset class in the MFC ODBC library. Table 22.1 shows some popular methods contained within the CRowset class. For a more complete list, check out the MSDN documentation.
Table 22.1. Popular CRowset Methods
| Method | Description |
| Close | Releases a rowset |
| Delete | Deletes rows from the rowset |
| GetData | Retrieves data from the rowset's copy of the row |
| Insert | Creates and inserts a new row |
| MoveFirst | Repositions the next-fetch location to the first record |
| MoveLast | Repositions the next-fetch location to the last record |
| MoveNext | Increments the next-fetch location to the next record |
| MovePrev | Decrements the next-fetch location to the last record |
| SetData | Updates the rowset |
CAccessor and CAccessorRowset
In order to work with the data in a rowset, you generally want to use accessor objects. Accessors are used to tell OLE DB about the structure of your client application's buffers, which are used either to hold column data from a rowset or values for command parameters.
The CAccessor class is an ATL template class. All accessors created by Visual C++ are derived from CAccessor. In our example, the accessor that is used to build our rowset is based on the CEmployeeAccessor class, which contains four components:
- The accessor should contain the data definition for your rowset. Usually, the ATL Object Wizard generates the data definition. In our example, the employee name, salary, and department are all declared. This enables the rowset to store information that it retrieves into the accessor. The data definition used for the accessor is shown in gray:
class CEmployeeAccessor { public: //Autoincrement fields interfere with the Update function. //Therefore, all references to EmpID have been removed //from this program by Chuck Wood TCHAR m_EmpName[51]; DB_NUMERIC m_Salary; TCHAR m_Dept[11];
- The
BEGIN_COLUMN_MAP and COLUMN_ENTRY
macros enable you to bind database columns to the columns defined in the data definition. In the next code snippet, three columns use COLUMN_ENTRY macros to link columns generated by SQL to C++ class variables:
BEGIN_COLUMN_MAP(CEmployeeAccessor) COLUMN_ENTRY(1, m_EmpName) COLUMN_ENTRY_PS(2, 19, 4, m_Salary) COLUMN_ENTRY(3, m_Dept) END_COLUMN_MAP()
- The DEFINE_COMMAND macro enables you to assign a default command for your accessor. In our example, a SELECT statement is issued that selects three columns from the Employee table:
DEFINE_COMMAND(CEmployeeAccessor, _T(" SELECT EmpName, Salary, Dept FROM Employee")) - Finally, for convenience, the ALT Object Wizard generates a ClearRecord
function that clears data from the accessor. This is useful when creating new blank records for insertion:
void ClearRecord() { memset(this, 0, sizeof(*this)); }
CAccessorRowset encapsulates a rowset and its associated accessors in a single class. By such encapsulation, the developer can access any CRowset or CAccessor method or variable. The CAccessorRowset class is inherited by the CRowset and CAccessor classes, and is the parent class to the CCommand class and the CTable class. Figure 22.6 shows the structure of the five main OLE DB classes and how they relate to each other through inheritance.
Figure 22.6 The five main OLE DB classes are strongly interrelated.
As you can see in Figure 22.6, the CAccessor class is inherited from a base class that is defined with a template. (CEmployeeAccessor in our example.) This base class enables the CAccessor class to not only contain accessor functionality, but also to contain the data definition and SELECT statements that are used by the rowset.
CCommand
The CCommand class is inherited from the CAccessorRowset class. The CCommand class is hardly ever instantiated, but rather serves as an ancestor for other classes that can then define how their OLE DB consumer functions.
The CCommand class is inherited from a CAccessorRowset that is built from a CRowset class (CAccessor) and the superclass of the CAccessor class used for this application's data definition (CEmployeeAccessor):
class CEmployee : public CCommand<CAccessor<CEmployeeAccessor> >
The CCommand class has two main functions:
- Data source and session variables are declared and used. The data source contains properties that are used to define the OLE DB connection. The session variable (m_session) contains methods that can control transactions or issue new commands. In the following function, the OLE DB driver for ODBC data sources (MSDASQL) and the VCUnleashed data source are used:
HRESULT OpenDataSource() { HRESULT hr; CDataSource db; CDBPropSet dbinit(DBPROPSET_DBINIT); dbinit.AddProperty(DBPROP_AUTH_PERSIST_SENSITIVE_AUTHINFO, false); dbinit.AddProperty(DBPROP_INIT_DATASOURCE, OLESTR("VCUnleashed")); dbinit.AddProperty(DBPROP_INIT_PROMPT, (short)4); dbinit.AddProperty(DBPROP_INIT_LCID, (long)1033); hr = db.Open(_T("MSDASQL"), &dbinit); if (FAILED(hr)) return hr; return m_session.Open(db); } CSession m_session; - A rowset is created using properties defined by the developer. Notice in bold in the next block of code that new properties can be added. In this case, DBPROP_ CANFETCHBACKWARDS enables backward scrolling so that the MovePrev() function will work:
HRESULT OpenRowset() { // Set properties for open CDBPropSet propset(DBPROPSET_ROWSET); propset.AddProperty(DBPROP_IRowsetChange, true); propset.AddProperty(DBPROP_UPDATABILITY, DBPROPVAL_UP_CHANGE | DBPROPVAL_UP_INSERT | DBPROPVAL_UP_DELETE ); //Property added by Chuck Wood propset.AddProperty(DBPROP_CANFETCHBACKWARDS, true); // Use a NULL to access the default SQL defined // in the DEFINE_COMMAND macro in the accessor return CCommand<CAccessor<CEmployeeAccessor> > ::Open(m_session, NULL, &propset); }
The CCommand class builds a CAccessorRowset based on an SQL command. In the Open() statement, a NULL is passed where an SQL command should go. The NULL instructs CCommand to use the default SQL found in the DEFINE_COMMAND macro in the accessor.
Data Source Properties
For some providers, you might be able to use the default properties of the provider and move right on to initializing the provider. However, in most cases, you should specify one or more properties before initializing the provider.
Properties for a data source object are divided into two property sets: The DBPROPSET_DBINIT property set contains properties that can be set before the data source is initialized, and the DBPROPSET_DATASOURCE property set contains properties that can be set after the data source is initialized.
The DBPROPSET_DBINIT property set contains the following individual properties:
- DBPROP_INIT_DATASOURCE specifies the name of the data source to connect to.
- DBPROP_AUTH_USERID specifies the user ID that is used to connect to the data source.
- DBPROP_AUTH_PASSWORD specifies the password to be used when connecting to the data source.
- DBPROP_INIT_HWND specifies the window handle to be used as a parent of any dialogs that the data source might need to present to the user, prompting for additional information.
- DBPROP_INIT_MODE specifies the access permissions for the data source. This can be used to set options such as read-only mode.
-
DBPROP_INIT_PROMPT specifies whether the user will be prompted for additional initialization information. This can be set to one of the following values:
- DBPROMPT_PROMPT, in which the provider always prompts the user for initialization information.
- DBPROMPT_COMPLETE prompts the user only if additional information is required.
- DBPROMPT_COMPLETEREQUIRED prompts the user only if additional information is required, and the user is allowed to enter only the missing required information.
- DBPROMPT_NOPROMPT does not prompt the user.
- DBPROP_INIT_TIMEOUT specifies the number of seconds to wait for initialization to complete.
- DBPROP_AUTH_CACHE_AUTHINFO is used to tell the provider whether it is allowed to cache sensitive information, such as passwords.
- DBPROP_AUTH_ENCRYPT_PASSWORD is used to determine whether the password must be encrypted when it is sent to the data source.
- DBPROP_AUTH_INTEGRATED is a string used to specify the authentication service that will be used to authenticate the user.
- DBPROP_AUTH_MASK_PASSWORD specifies that the password will be masked when sent to the data source. This provides a weaker form of encryption than DBPROP_AUTH_ENCRYPT_PASSWORD.
- DBPROP_AUTH_PERSIST_ENCRYPTED specifies that the data source object will persist authentication information in encrypted form.
- DBPROP_AUTH_PERSIST_SENSITIVE_AUTHINFO allows the data source to persist authentication information.
- DBPROP_INIT_IMPERSONATION_LEVEL specifies the level of impersonation that the server can use when impersonating the client.
- DBPROP_INIT_LCID specifies the preferred locale for text that is returned to the consumer.
- DBPROP_INIT_LOCATION specifies the location (such as server name) of the data source.
- DBPROP_INIT_PROTECTION_LEVEL specifies the level of authentication protection in communications between the client and the server.
- DBPROP_INIT_PROVIDERSTRING is used to specify a provider-specific string containing additional connection information.
In addition, you can use the DBPROPSET_DATASOURCE property set to set the DBPROP_ CURRENTCATALOG property, which determines which catalog or database is to be used within the data source. For more detail on the acceptable values for the properties listed here, see the OLE DB specification.
CCommand Properties
The properties that you can define for a property set are as follows:
- DBPROP_ABORTPRESERVE determines whether the rowset is preserved after an aborted transaction.
- DBPROP_APPENDONLY is used to create a rowset that is used only for appending new rows.
- DBPROP_BLOCKINGSTORAGEOBJECTS determines whether using storage objects will block other rowset methods.
- DBPROP_BOOKMARKS determines whether the rowset supports bookmarks.
- DBPROP_BOOKMARKSKIPPED determines whether bookmarks for inaccessible rows can be passed to IRowsetLocate::GetRowsAt().
- DBPROP_BOOKMARKTYPE specifies the type of bookmark used.
- DBPROP_CACHEDEFERRED determines whether the consumer will cache column data.
- DBPROP_CANFETCHBACKWARDS determines whether the rowset can fetch backwards.
- DBPROP_CANHOLDROWS determines whether the rowset will cache rows or transfer them immediately.
- DBPROP_CANSCROLLBACKWARDS determines whether the rowset can scroll backwards.
- DBPROP_CHANGEINSERTEDROWS determines whether the rowset allows updates to newly inserted rows.
- DBPROP_COLUMNRESTRICT determines whether access rights are determined on a column-by-column basis.
- DBPROP_COMMANDTIMEOUT indicates the number of seconds before a command times out.
- DBPROP_COMMITPRESERVE determines whether the rowset is preserved after a transaction is committed.
- DBPROP_DEFERRED determines whether the rowset will fetch the data for a row immediately or defer until an accessor is used on the columns.
- DBPROP_DELAYSTORAGEOBJECTS determines whether storage objects are used in delayed update mode.
- DBPROP_IColumnsRowset provides more detailed information about the columns in a rowset.
- DBPROP_IConnectionPointContainer provides an interface for creating connection points for receiving COM notifications.
- DBPROP_IRowsetChange is used to insert, delete, and update rows in the rowset.
- DBPROP_IRowsetIdentity is used to compare two row handles.
- DBPROP_IRowsetLocate is used to perform scrolling within a rowset.
- DBPROP_IRowsetResynch is used to resynchronize the rowset data to the data source.
- DBPROP_IRowsetScroll is used to perform approximate scrolling within the rowset.
- DBPROP_IRowsetUpdate is used to work with delayed updates.
- DBPROP_ISupportErrorInfo is used for advanced error reporting with OLE DB error objects.
Using a Consumer
After creating a consumer, you need to use it to interact with the user. In our example, all the user interaction is handled through the CChap22Dialog class in the Chap22Dialog header file (Chap22Dialog.h). This section shows the code you write to effectively interact with an OLE DB consumer.
Opening and Closing a Rowset
The first step to using a consumer is to include the header file for the consumer .hin the class that needs OLE DB support:
//Added by Chuck Wood for OLE DB header file support #include "Employee.h"
Next, you need a class variable to contain the CRowset information. In this example, the CEmployee class, inherited from CCommand, is defined using the m_Set variable at the beginning of the class definition, as shown in gray:
class CChap22Dialog :
public CAxDialogImpl<CChap22Dialog>
{
public:
CEmployee m_Set; //Added by Chuck Wood for DB support
After being defined, the rowset can be opened and closed. In the CChap22Dialog constructor and destructor, the rowset is opened and closed, as shown by the lines of code in bold:
CChap22Dialog() {
m_Set.Open();
DoModal();
}
~CChap22Dialog() {
m_Set.Close();
}
Updating and Inserting into Rowsets
OLE DB has two routines for updating the database. The first, CRowset.SetData(), is used to update an existing rowset from the class variables defined in the CAccessor (CEmployeeAccessor). The second, CRowset.Insert(), inserts a new row into the rowset and the database using variables defined in the CAccessor. Unlike ODBC, there is no AddNew() function or edit mode. Instead, the programmer usually controls whether an add is in progress and issues the appropriate command depending on whether an insert or update is needed. The following steps can be implemented to handle inserting and deleting inside an ATL program:
- Declare a Boolean flag used to indicate whether an insert is in progress. In the constructor, set this flag to FALSE. These commands are shown in gray:
BOOL m_bInserting; //Added by Chuck Wood for insert support CChap22Dialog() { m_Set.Open(); m_bInserting = FALSE; DoModal(); } -
The MFC contains an UpdateData() routine used for displaying information from an ODBC recordset to the dialog box and for taking information from the dialog box and updating a recordset. Similar functionality would be useful in an ATL application. The UpdateData() function shown next checks a saveChanges flag to see whether you want to save changes from the dialog box (TRUE, the default) or display contents of the rowset to the dialog box (FALSE). The insert and update functionality is shown in gray:
//Functions written by Chuck Wood to aid in database support void UpdateData(BOOL saveChanges=TRUE) { //Written by Chuck Wood for Visual C++ Unleashed //Mimics the MFC UpdateData function char salaryHolder[25]; if (saveChanges) { GetDlgItemText(IDC_EMPNAME, m_Set.m_EmpName, 51); GetDlgItemText(IDC_DEPT, m_Set.m_Dept, 11); GetDlgItemText(IDC_SALARY, salaryHolder, 25); //Currency conversions (DB_NUMERIC) are a real pain double salary = atof(salaryHolder); //Adjust salary for scale salary *= pow(10, m_Set.m_Salary.scale); //Initialize val array to zero for (int loop = 0; loop < 16; loop++) { //initialize salary in DB to zero m_Set.m_Salary.val[loop] = 0; } for (loop = 0; salary > 0; loop++) { //Adjust hexadecimal power double trunc = floor(salary / (16*16)); m_Set.m_Salary.val[loop] = salary - (trunc*16*16); salary = trunc; } HRESULT hr = 0; if (m_bInserting) { hr = m_Set.Insert(); //Add new row } else { hr = m_Set.SetData(); //Update row } m_bInserting = FALSE; if (FAILED(hr)) { showErrors(); } } else { //Write data from the database to the dialog box SetDlgItemText(IDC_EMPNAME, m_Set.m_EmpName); SetDlgItemText(IDC_DEPT, m_Set.m_Dept); //Currency conversions (DB_NUMERIC) are a real pain double salary = 0; //Initialize salary for (int loop = 0; loop < 16; loop++) { //Adjust hexadecimal power double power = pow(16, loop*2); salary += (m_Set.m_Salary.val[loop] * power); } salary /= pow(10.0, m_Set.m_Salary.scale); sprintf (salaryHolder, "%.2f", salary); SetDlgItemText(IDC_SALARY, salaryHolder); } }Now the update functionality can be accessed from anywhere in the program. For instance, when the dialog box is closed, an UpdateData() can be called to make sure any final updates are recorded:
LRESULT OnCancel(WORD wNotifyCode, WORD wID, HWND hWndCtl, BOOL& bHandled) { UpdateData(); EndDialog(wID); return 0; }
-
Finally, write an insert routine. This routine is called when the user requests a new record. It updates any changes that the user has made in the dialog box using the UpdateData() routine and then calls an insertRecord() function to set up an insert:
RESULT OnInsert(WORD wNotifyCode, WORD wID, HWND hWndCtl, BOOL& bHandled) { UpdateData(); //Update database insertRecord(); //Set up an insert return 0; }The insertRecord() function clears the dialog box fields using the ATL Object Wizard–generated ClearRecord function. It then displays the blank fields using a UpdateData(FALSE) call and sets the insert flag:
Lvoid insertRecord() { //Set up inserting m_Set.ClearRecord(); //Clear dialog box fields UpdateData(FALSE); //Clear window m_bInserting = TRUE; //Initialize add flag }
Navigating Through a Rowset
Navigating through a rowset involves using the CRowset.MoveFirst(), CRowset.MovePrev(), CRowset.MoveNext(), and CRowset.MoveLast() functions. Each of these functions is called through menu routines that enable the user to navigate through the rowset.
The OnFirst routine is split into two functions. The menu function updates the current record and then calls the firstRecord() function to display the first record:
//The rest of this class added by Chuck Wood for OLE DB functionality
LRESULT OnFirst(WORD wNotifyCode, WORD wID, HWND hWndCtl, BOOL& bHandled)
{
UpdateData(); //Update database
firstRecord();
return 0;
}
void firstRecord() { //Go to the first record
if (m_Set.MoveFirst() == S_OK) {
UpdateData(FALSE); //Display record
}
else {
showMessage("No records found. Inserting new Record");
insertRecord();
}
}
The reason the OnFirst() function is split into two functions is because the OnInitDialog() routine needs to display the first record without updating any existing records:
LRESULT OnInitDialog(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
{
firstRecord(); //Display first record
return 1; // Let the system set the focus
}
The rest of the navigation functions don't need to be split:
LRESULT OnPrev(WORD wNotifyCode, WORD wID, HWND hWndCtl, BOOL& bHandled) {
showMessage("");
UpdateData(); //Update database
if (m_Set.MovePrev() == S_OK) {
UpdateData(FALSE); //Display record
}
else {
showMessage("No previous records found");
OnFirst(wNotifyCode, wID, hWndCtl, bHandled);
}
return 0;
}
LRESULT OnNext(WORD wNotifyCode, WORD wID, HWND hWndCtl, BOOL& bHandled) {
showMessage("");
UpdateData(); //Update database
if (m_Set.MoveNext() == S_OK) {
UpdateData(FALSE); //Display record
}
else {
showMessage("No more records found");
OnLast(wNotifyCode, wID, hWndCtl, bHandled);
}
return 0;
}
LRESULT OnLast(WORD wNotifyCode, WORD wID, HWND hWndCtl, BOOL& bHandled) {
UpdateData(); //Update database
if (m_Set.MoveLast() == S_OK) {
UpdateData(FALSE); //Display record
}
else {
showMessage("No records found. Inserting new Record");
insertRecord();
}
return 0;
}
Deleting Rows from a Rowset
Deleting rows from a rowset (and, hence, from a database table) is always problematic. Unless transactions are used, there is no way to undo a delete. Make sure that the user didn't accidentally press the delete option and that there really is a record present to delete, and not a newly inserted record soon to be deleted. The following steps can be used for a delete:
- Make sure the user really wants a delete:
if (MessageBox("Are you sure you want to delete?", "Confirm Delete", MB_YESNO) != IDYES) { return 0; // Get outta here } - Make sure that an actual record exists to be deleted. If you are in the middle of an insert and you issue a delete, the newly inserted record won't be deleted, but rather the last record viewed before the insert will be deleted. This could cause some serious problems for the user:
if (m_bInserting) { m_bInserting = FALSE; //Just reset flag, ... UpdateData(FALSE); // display current record, ... return 0; // and get outta here } - Delete the record using the CRowset.Delete() method:
m_Set.Delete();
- Now that the current row is deleted, it is no longer valid. Try moving to a different row, or if there are no more rows, inserting a new record using the insertRecord() function:
if (FAILED(m_Set.MoveNext())) { if (FAILED(m_Set.MoveLast())) { insertRecord(); return 0; } } UpdateData(FALSE); //Display record
These steps can be seen in the OnDelete() function that is called when the user chooses the delete menu option:
LRESULT OnDelete(WORD wNotifyCode, WORD wID, HWND hWndCtl, BOOL& bHandled)
{
showMessage("");
if (MessageBox("Are you sure you want to delete?",
"Confirm Delete",
MB_YESNO) != IDYES) {
return 0; // Get outta here
}
//See if you can just cancel add
if (m_bInserting) {
m_bInserting = FALSE; //Just reset flag, ...
UpdateData(FALSE); // display current record, ...
return 0; // and get outta here
}
if (FAILED(m_Set.Delete())) {
//Delete failed. Display messages.
MessageBox("Could not delete record", "Database Error");
showMessage("Could not delete record");
return 0; // Get outta here
}
//Find a new record
if (FAILED(m_Set.MoveNext())) {
if (FAILED(m_Set.MoveLast())) {
insertRecord();
return 0;
}
}
UpdateData(FALSE); //Display record
return 0;
}
Now you're finished with this OLE DB application. If you've been following the ex ample, your application should open a dialog box similar to the one seen in Figure 22.7.
Figure 22.7 It's easy to create OLE DB applications using the ATL AppWizard and the ATL Object Wizard.
Catching OLE DB Errors
Database errors are generated by the database engine. These errors often give the developer more insight into any potential problems still present in an application.
The main class for processing multiple database errors is the CDBErrorInfo class. This class provides support for one or more OLE DB error records that are returned to the user. To use this class, perform the following steps:
- Call CDBErrorInfo.GetErrorRecords()
to retrieve the number of OLE DB database errors:
ULONG numErrors = 0; CDBErrorInfo errorInfo; errorInfo.GetErrorRecords(m_Set.m_spCommand, IID_ICommandPrepare, &numErrors);
- If the number of errors is successfully retrieved, retrieve error information into an IErrorInfo interface using the CDBErrorInfo.GetErrorInfo() function in a loop. Use the IErrorInfo.GetDescription() method to retrieve each error message:
BSTR sDescription = NULL; IErrorInfo *pErrorInfo = NULL; LCID lcid = GetUserDefaultLCID(); for (ULONG loop = 0; loop < numErrors; loop++) { errorInfo.GetErrorInfo(loop, lcid, &pErrorInfo); pErrorInfo->GetDescription(&sDescription); }
These steps can be viewed in the showErrors function:
void showErrors() {
CDBErrorInfo errorInfo;
IErrorInfo *pErrorInfo = NULL;
ULONG numErrors = 0;
if (FAILED(errorInfo.GetErrorRecords(m_Set.m_spCommand,
IID_ICommandPrepare, &numErrors))) {
MessageBox("Error information was not retrievable", "Database
Error");
return;
}
if (FAILED(GetErrorInfo(0, &pErrorInfo))) {
MessageBox("Error information was not retrievable", "Database
Error");
return;
}
char message[4096];
strcpy (message, "");
LCID lcid = GetUserDefaultLCID();
for (ULONG loop = 0; loop < numErrors; loop++) {
if (FAILED(errorInfo.GetErrorInfo(loop, lcid, &pErrorInfo))) {
continue;
}
BSTR sDescription = NULL;
pErrorInfo->GetDescription(&sDescription);
sprintf(message, "%s%S", message, sDescription);
SysFreeString(sDescription); //Clean up
pErrorInfo->Release();
}
MessageBox(message, "Database Error");
}
This function is handy for collecting errors for any failed database operation. Simply use the FAILED macro to test the HRESULT of any database operation and show the errors if a failure occurs:
HRESULT hr = {some database operation}
if (FAILED(hr)) {
showErrors();
}
Retrieving Column Information | Next Section

Account Sign In
View your cart