In Chapter 13, "Understanding MFC's DAO Classes," you looked at the new MFC 4 DAO data access classes. This chapter introduces you to these classes in actual working code. You'll create several examples of DAO applications using as many features of DAO as you can.
This chapter is divided into three parts. In the first part you'll create a standard DAO application using AppWizard. In the second part, you'll look at using each of the DAO MFC classes:
CDaoDatabase
CDaoException
CDaoFieldExchange
CDaoQueryDef
CDaoRecordset
CDaoRecordView
CDaoTableDef
CDaoWorkspace
In the final part of this chapter, you'll create a program that returns information about an Access database using calls to DAO. If possible, you should let AppWizard design the shell of your application. Generally, for simple front-end applications, AppWizard provides an excellent framework for your program and requires only minor filling out by youusually only the user interface. For more complex applications, you probably will use AppWizard to create the application's shell and add the DAO support later by hand.
NOTE
This chapter uses two sample programs. The first part of this chapter uses the Addresses program, and the second part uses the DAO Direct program. Both of these programs use the Addresses Access database, found in the CHAPTR14 directory on the CD that comes with this book.
In this, the first part of the chapter, you'll create an application using AppWizard, with support for DAO. Using DAO in this manner lets you create an application that looks (and feels) like an ODBC application, both from the programmer's viewpoint and from the user's viewpoint.
The main differences in a stock AppWizard DAO application and a stock AppWizard ODBC application are the names of the classes used. Generally, the functions and functionalities of DAO and ODBC are virtually identical. For example, there is a one-to-one correspondence between DAO classes and the ODBC classes, as shown in Table 14.1.
DAO Class | ODBC Class | Functionality |
CDaoDatabase | CDatabase | A CDaoDatabase (and CDatabase) class object represents a connection to a database through which you can operate on the data. |
CDaoException | CDBException | The CDaoException class object represents an exception condition raised by one of the MFC DAO database classes. |
CDaoFieldExchange | CFieldExchange | The CDaoFieldExchange (and CFieldExchange) class object supports record field exchange (DFX). Typically directly used when you're writing data exchange routines for custom data types. Otherwise, AppWizard and ClassWizard will manipulate this class for you. Technical Note 53, available under MFC in Books Online, contains the necessary information for writing your own custom DFX routines. |
CDaoQueryDef | No ODBC equivalent | A CDaoQueryDef class object represents a query definition that is typically saved in a database. |
CDaoRecordset | CRecordset | A CDaoRecordset (and CRecordset) class object represents a set of records selected from a data source. |
CDaoRecordView | CRecordView | A CDaoRecordView (and CRecordView) class object is used to represent a view that displays database records in controls (such as in a CFormView-based application). |
CDaoTableDef | No ODBC equivalent | A CDaoTableDef class object represents the stored definition of a base table or an attached table. |
CDaoWorkspace | No ODBC equivalent | A CDaoWorkspace class object manages a named, password-protected database session from logon to logoff by a given user. |
Since DAO is intended to work with Access databases (after all, DAO uses the Access Jet database engine), you need to have at least one Access database installed on your system prior to using AppWizard in order to generate a DAO MFC application. Finding an Access database isn't difficult. If nothing else is available, you can use one of the sample databases (such as Northwind.mdb, typically found in the \MSOffice\Access\Samples directory) to get started writing DAO applications.
NOTE
It's not necessary to register your Access database (using ODBC) when you're creating or using a DAO application. DAO doesn't use ODBC.
When creating your application, you will have to select the basic user interface. Select either MDI or SDI, but not dialog-based, because a dialog-based application can't have database access when generated using AppWizard. Once the user interface is configured, you must select the degree of database support. You have the standard levels regardless of whether your application will be ODBC or DAO. The levels that may be selected include header files only, database view without file support, and database view with file support. If your application won't be using a separately saved document file, selecting database view without file support will create the best application with the least amount of clutter. Selecting header files only will create an application that may have DAO or ODBC support added later.
NOTE
Which came first, the chicken or the egg? In creating your application, you face your first dilemma: You need an existing database in order for AppWizard to generate your DAO application, but the application will generate the database using File | New.
The solution is relatively simple: You will create a template database using Access. This will allow you to design your database using Access's tools. Then later you can add code to the application to create new databases as needed.
A template database is needed in order for Visual C++ 4's AppWizard to create a DAO project. I used Access 7 to create a new database using Access's Database Wizard. The Database Wizard's Address Book template was chosen as the type of database desired because it fit the needs of your sample application, a names database.
Access's Database Wizard lets you add and remove columns from a database and define the look and feel of the data entry forms. The columns can be customized to your application's needs. (Having the right mix of columns can be a sticky wicket: What's right for one user usually isn't adequate for another user.) You're not going to use the data entry forms, so the form style isn't relevant in this example. The database is called Address Book, and it contains two tables: Addresses and Switchboard. The Switchboard table is used to manage the forms in the Access version of this database and is of no use to a Visual C++ programmer. Ignore the Switchboard table, but don't remove it.
Once the database is created, it's a good idea to print a record of the Addresses table that is created using Access's Tools | Analyze | Documentor menu selection. This will provide you with a printed, permanent record of the Addresses table.
Using the Documentor utility, you will have a report that can be printed (or exported to a document). It will list each column's attributes, as shown in Listing 14.1.
Listing 14.1. Access's Documentor output for two columns in the Addresses table.
Columns Name Type Size AddressID Number (Long) 4 Allow Zero Length: False Attributes: Fixed Size, Auto-Increment Caption: Address ID Collating Order: General Column Hidden: False Column Order: Default Column Width: Default Ordinal Position: 0 Required: False Source Field: AddressID Source Table: Addresses FirstName Text 50 Allow Zero Length: False Attributes: Variable Length Caption: First Name Collating Order: General Column Hidden: False Column Order: Default Column Width: Default Ordinal Position: 2 Required: False Source Field: FirstName Source Table: Addresses
The entire listing for the Addresses table is only six pages long, so it's easily printed. When exported as a Word document (in RTF format, readable by Windows 95's WordPad program), it's saved as doc_rptObjects.rtf in your My Documents directory. This report appears in the CHAPTR14 directory on the CD that comes with this book so that you can peruse it without having to run Access.
NOTE
Although the Access Database Wizard creates a complete database application (a basic application, however) when using DAO to create databases, you will create only the database, not an application. For example, DAO typically isn't used to create forms that may be part of an Access database.
Once you've created your application's database, you can use Visual C++'s AppWizard to create the database application itself.
Once you've created an initial database for your application, you can use AppWizard to create the DAO application's shell. You should follow typical database application creation steps, making sure to select a data source in the AppWizard database support wizard dialog (Step 2 of 6), as shown in Figure 14.1.
Figure 14.1. AppWizard's Step 2 of 6 database support wizard dialog.
Clicking the Data Source button will display the Database Options dialog box, shown in Figure 14.2, in which you should select DAO as the datasource. Click the ... button.
Figure 14.2. The Database Options dialog box.
A select files dialog box appears, as shown in Figure 14.3, listing Access database files.
Figure 14.3. The DAO Open dialog box.
After you select an Access database, you must select a table to use. After you close the Database Options dialog box, you will see the Select Database Tables dialog box, shown in Figure 14.4. It will have a list of all the tables that are contained within the database that was selected. For this example, there are two tables. You will be working with the Addresses table exclusively.
Figure 14.4. Selecting the Addresses table in the Select Database Tables dialog box.
After you've selected your DAO Access database and table (variables will be bound for each column in the selected table), you can set whatever other options you would like your application to support. For most applications, you should include support for OLE controls, because several of the stock OLE controls are most useful for database applications.
After generating your application, you should do a test build to make sure that the creation was successful.
The second step in a DAO program is to bind variables to the user interface dialog controls. A DAO program uses a CDaoRecordView class object for a user interface. The CDaoRecordView object works much like the standard ODBC CRecordView object: Design and lay out a dialog box with controls to match the database table's columns.
In this example (a simple address book application), you will affix a number of controls to the dialog box (you won't use every column in the database table) to create a basic functional program. Later, you can add dialog boxes for other, infrequently used controls if you want to.
NOTE
There are other alternatives to using a separate dialog box for infrequently used fields that would otherwise clutter up a good user interface. One alternative is to use an MDI model. One window would service the main columns, and other windows would hold other, infrequently used columns in the database. A second alternative would be a tab dialog (like Visual C++'s options dialog boxes).
Figure 14.5 shows the dialog box controls that are used to allow the user to enter (and retrieve) data in your database.
Figure 14.5. A typical user interface for a DAO program.
As soon as you've created your user interface, you can go on to the more advanced parts of your programming. Since this chapter is about using DAO, it doesn't cover topics such as printing, but you will need to add report generation facilities to virtually any application you create.
One primary functionality you need is to be able to handle the application's File menu. In a typical application, you would be able to open different files (or databases) and create a new file (database) for the user.
Opening (and closing for MDI applications, if applicable) a database file using DAO is a simple process. In the sample program, you close your database by adding a handler in your document class (CAddressDoc in the sample program) for the menu selections New and Open. Since the sample program is SDI, you don't implement a Close menu item. The Open selection will close an open database before you open a new one.
Since default DAO applications created using AppWizard have a "hardwired" default database name and SQL parameter, you must first change these constants to CString class variables. Here's an example of what AppWizard creates (the constants appear in bold):
CAddressesSet::CAddressesSet(CDaoDatabase* pdb) : CDaoRecordset(pdb) { //{{AFX_FIELD_INIT(CAddressesSet) m_AddressID = 0; m_FirstName = _T(""); m_LastName = _T(""); m_SpouseName = _T(""); m_Address = _T(""); m_City = _T(""); m_StateOrProvince = _T(""); m_PostalCode = _T(""); m_Country = _T(""); m_EmailAddress = _T(""); m_HomePhone = _T(""); m_WorkPhone = _T(""); m_WorkExtension = _T(""); m_MobilePhone = _T(""); m_FaxNumber = _T(""); m_SendCard = FALSE; m_Nickname = _T(""); m_Notes = _T(""); m_Hobbies = _T(""); m_nFields = 20; //}}AFX_FIELD_INIT m_nDefaultType = dbOpenDynaset; } CString CAddressesSet::GetDefaultDBName() { return _T("I:\\Database Developers Guide with Visual C++ 4" "\\Source CD\\CHAPTR14\\Address Book.mdb"); } CString CAddressesSet::GetDefaultSQL() { return _T("[Addresses]"); }
You make your database and SQL strings modifiable by adding two CStrings to your document class and initializing them as shown in the following code fragment (changes appear in bold):
CAddressesSet::CAddressesSet(CDaoDatabase* pdb) : CDaoRecordset(pdb) { //{{AFX_FIELD_INIT(CAddressesSet) m_AddressID = 0; m_FirstName = _T(""); m_LastName = _T(""); m_SpouseName = _T(""); m_Address = _T(""); m_City = _T(""); m_StateOrProvince = _T(""); m_PostalCode = _T(""); m_Country = _T(""); m_EmailAddress = _T(""); m_HomePhone = _T(""); m_WorkPhone = _T(""); m_WorkExtension = _T(""); m_MobilePhone = _T(""); m_FaxNumber = _T(""); m_SendCard = FALSE; m_Nickname = _T(""); m_Notes = _T(""); m_Hobbies = _T(""); m_nFields = 20; //}}AFX_FIELD_INIT m_nDefaultType = dbOpenDynaset; m_DefaultDBName = _T("I:\\Database Developers Guide with Visual C++ 4" "\\Source CD\\CHAPTR14\\Address Book.mdb"); m_DefaultSQL = _T("[Addresses]"); } CString CAddressesSet::GetDefaultDBName() { return m_DefaultDBName; } CString CAddressesSet::GetDefaultSQL() { return m_DefaultSQL; }
Don't forget to add m_DefaultDBName and m_DefaultSQL to your class definition's header file. This way, you can close the current database and then open a new one by modifying DefaultDBName and calling the CDaoRecordset::Open() function. First you create a handler for the File | Open menu selection in the CAddressesDoc class.
For example, in your document class, your handler for opening a new database is implemented as the following code fragment shows. To create this functionality, you must use ClassWizard to add a handler for the File | Open menu selection and then add code to that handler:
void CAddressesDoc::OnFileOpen() { // TODO: Add your command handler code here // The m_addressSet member variable is a pointer to the CAddressSet class. // First, get a database file to be opened! CFileDialog dlg(TRUE, NULL, "*.mdb", OFN_OVERWRITEPROMPT, "Access Database Files (*.mdb) | *.mdb | " "All Files (*.*) | *.* ||"); if (dlg.DoModal()) {// User did not cancel the open file dialog: TRACE("File selected is '%s'\n", dlg.GetPathName()); if (m_addressesSet.IsOpen()) {// If open (avoid errors and crashes!), then close: m_addressesSet.Close(); if (m_addressesSet.m_pDatabase != NULL) {// Close the DB too. If not closed, old DB is reopened! m_addressesSet.m_pDatabase->Close(); } } // Save the user's database filename and then (re)open: m_addressesSet.m_DefaultDBName = dlg.GetPathName(); m_addressesSet.Open(AFX_DAO_USE_DEFAULT_TYPE); // Refresh the view to reflect the *new* database's data: CFrameWnd * pFrame = (CFrameWnd *)(AfxGetApp()->m_pMainWnd); CView * pView = pFrame->GetActiveView(); if (pView) { pView->UpdateData(FALSE); } } else {// User aborted. Do nothing, or clean up if appropriate. } }
This code is simple. First you get the name for the new database from the user. If the user cancels the dialog box, you simply do nothing. Otherwise, you close the currently open recordset and the database. Both must be closed to enable the opening of a new database. Next you take the user's new database name (the fully qualified path name) and stuff it into your m_addressesSet.m_DefaultDBName variable. You will use this name when you reopen your database.
Once you have a database name saved, you call your recordset's Open() function to do the actual open: m_addressesSet.Open(AFX_DAO_USE_DEFAULT_TYPE). Because you closed the original database, Open() will reopen the database using the database name returned by GetDefaultDBName(). If you had closed only your recordset and not the database, a call to Open() would simply have used the existing, still open, database.
Finally, after you've opened the new database, you must update the user interface. You do this by retrieving the current view (a simple process for any SDI application) and call the UpdateData() function with a parameter of FALSE to force an update of the application's data that is being presented to the user. The only thing special about this code is that it shows how to get a pointer to the application's view class from any point in an application:
CFrameWnd * pFrame = (CFrameWnd *)(AfxGetApp()->m_pMainWnd); CView * pView = pFrame->GetActiveView(); if (pView) { // pView is a valid pointer to the view class } else { // pView is not valid: perhaps we have no view! }
NOTE
You might have noticed that since this application's user interface is specific to the Address database, opening a database with a different table structure (NorthWind.MDB, for example) won't work. It's beyond the purpose of this example to add the complexity required to open and display any database. To test the database open functionality, I've included two versions of the Address database on the CD that comes with this book. These database files are located in the CHAPTR14 folder. The database called Address Book.mdb contains a standard set of five dummy records. The database called Special Address.mdb contains the same five records, but each first name is prefixed with the word SPECIAL.
A necessary functionality in almost any application is to be able to create a new "document." In the case of a database program, a new document is actually a new database. You could simply not implement this functionality, or you could supply an empty database that the user (or your application) could copy to a new name. This isn't a very elegant solution. It looks kludgy and lacks professionalism. But with ODBC, this was often the way it was done.
NOTE
One trick of the trade was to take a new, empty database file and save it as a binary resource in the base application. When the user selected File | New, this binary resource was written out as a disk file and given the name that the user selected. When ODBC didn't provide a way for an application to create an Access database, it was a solution that was invisible to the user. DAO doesn't require you to use these tricks, because you can create an Access database directly.
NOTE
Several online documents are available that discuss using DAO to create Access databases. One topic that you can search for in Books Online is "Creating a Database with DAO."
To create a new database, follow these steps:
You can add a new table to an existing database by following steps 2 through 5. You can add a new column to an existing table in a database by following steps 3 through 5. Here is your sample code:
void CAddressesDoc::OnFileNew() { // TODO: Add your command handler code here // Create a new Access database, then open the doggie! // First, get a filename to use. Warn if we are overwriting an existing file: CFileDialog dlg(FALSE, NULL, "*.mdb", OFN_OVERWRITEPROMPT, "Access Database Files (*.mdb) | *.mdb | " "All Files (*.*) | *.* ||"); if (dlg.DoModal()) {// User did not cancel the open file dialog: TRACE("File selected is '%s'\n", dlg.GetPathName()); // Close the existing database (if open): if (m_addressesSet.IsOpen()) {// If open (avoid errors and crashes!), then close: m_addressesSet.Close(); if (m_addressesSet.m_pDatabase != NULL) {// Close the DB too. If not closed, old DB is reopened! m_addressesSet.m_pDatabase->Close(); } } // Create, create, it's off to create we go! // Notice: we are using CDaoDatabase's Create() function. m_addressesSet.m_DefaultDBName = dlg.GetPathName(); if (m_addressesSet.m_pDatabase != NULL) {// We are stuffed if m_pDatabase is NULL! m_addressesSet.m_pDatabase->Create( m_addressesSet.m_DefaultDBName, dbLangGeneral, dbVersion30); // Define the table (connected to our database)... CDaoTableDef TableDef(m_addressesSet.m_pDatabase); TableDef.Create(_T("Addresses")); // Define columns (hard-code them): TableDef.CreateField(_T("AddressID"), dbLong, 4, dbAutoIncrField); TableDef.CreateField(_T("FirstName"), dbText, 50, dbVariableField); TableDef.CreateField(_T("LastName"), dbText, 50, dbVariableField); TableDef.CreateField(_T("SpouseName"), dbText, 50, dbVariableField); TableDef.CreateField(_T("Address"), dbText, 255, dbVariableField); TableDef.CreateField(_T("City"), dbText, 50, dbVariableField); TableDef.CreateField(_T("StateOrProvince"), dbText, 20, dbVariableField); TableDef.CreateField(_T("PostalCode"), dbText, 20, dbVariableField); TableDef.CreateField(_T("Country"), dbText, 50, dbVariableField); TableDef.CreateField(_T("EmailAddress"), dbText, 50, dbVariableField); TableDef.CreateField(_T("HomePhone"), dbText, 30, dbVariableField); TableDef.CreateField(_T("WorkPhone"), dbText, 30, dbVariableField); TableDef.CreateField(_T("WorkExtension"), dbText, 20, dbVariableField); TableDef.CreateField(_T("MobilePhone"), dbText, 30, dbVariableField); TableDef.CreateField(_T("FaxNumber"), dbText, 30, dbVariableField); TableDef.CreateField(_T("Birthdate"), dbDate, 8, dbFixedField); TableDef.CreateField(_T("SendCard"), dbBoolean, 1, dbVariableField); TableDef.CreateField(_T("Nickname"), dbText, 30, dbVariableField); TableDef.CreateField(_T("Notes"), dbMemo, 0, dbVariableField); TableDef.CreateField(_T("Hobbies"), dbText, 255, dbVariableField); TableDef.Append(); // Close the database, then reopen it using the CDaoRecordset class m_addressesSet.m_pDatabase->Close(); m_addressesSet.Open(AFX_DAO_USE_DEFAULT_TYPE); // Refresh the view to reflect the *new* database's data: CFrameWnd * pFrame = (CFrameWnd *)(AfxGetApp()->m_pMainWnd); CView * pView = pFrame->GetActiveView(); if (pView) { pView->UpdateData(FALSE); } } } }
As in other listings, the code added manually appears in bold. Notice the sequence of events:
m_addressesSet.m_pDatabase->Create( m_addressesSet.m_DefaultDBName, dbLangGeneral, dbVersion30);
CDaoTableDef TableDef(m_addressesSet.m_pDatabase); TableDef.Create(_T("Addresses"));Create one table (and its fields) at a time.
TableDef.CreateField(_T("AddressID"), dbLong, 4, dbAutoIncrField); TableDef.CreateField(_T("FirstName"), dbText, 50, dbVariableField);This code shows only two fields, but the sample program actually creates 19 columns (called fields) in the table.
TableDef.Append();Each table must be saved using the Append() member function.
After you've created the database, tables, and table columns, you can close the database and reopen it. Although it's not strictly necessary to close a database after creating it, it makes sense to start off from a known statefreshly opened.
If you've been working along with this book, you now can create a database and open it. Here lies the wrinkle: Your application can open a database, but there is no provision to add new records! This shortcoming rather limits the program's usefulness. The next step is to create code to add records to the database.
After you've created the database's tables and columns, you should also create any indexes your application will need. Although it's not strictly necessary to have indexes in a database, they can greatly enhance performance.
In the example, you will index the same columns that Microsoft did when Access's Database Wizard created your template database. Here are the indexes and the columns used by the indexes:
PrimaryKey using the field AddressID, which will be the primary key
EmailAddress using the field EmailAddress
LastName using the field LastName
PostalCode using the field PostalCode
NOTE
Indexes can have one or more fields. In the example, each index has only one field. Some database applications have indexes that comprise multiple fields. For a multiple-field index, you would define your CDaoIndexFieldInfo object as an array and indicate the number of fields used in the CDaoIndexInfo::m_nFields member.
To create an index, you must allocate a CDaoIndexInfo object and either a single CDaoIndexFieldInfo if your index will have only one field or an array of type CDaoIndexFieldInfo if your index will have more than one field. For example, your program uses this code:
// Finally, create the (optional) index fields. The example has // four sets of indexes, each having only one field: CDaoIndexInfo idx; CDaoIndexFieldInfo fld; // Only one field per index in the example
After you've allocated your index and index field objects, you must fill them in. To create your primary key, use this code:
// Fill in your structure with revelant information, then build // the index. CDaoTableDef::CreateIndex() calls Append // automatically, unlike CDaoTableDef::CreateField(), which does not. idx.m_strName = _T("PrimaryKey"); // Primary idx.m_pFieldInfos = &fld; // Primary idx.m_nFields = 1; // Primary idx.m_bPrimary = TRUE; // Secondary idx.m_bUnique = TRUE; // Secondary idx.m_bClustered = FALSE; // Secondary idx.m_bIgnoreNulls = FALSE; // Secondary idx.m_bRequired = TRUE; // Secondary idx.m_bForeign = FALSE; // Secondary idx.m_lDistinctCount = 5; // All - returned, not set! fld.m_strName = _T("AddressID"); // Key field fld.m_bDescending = FALSE; // Ascending TableDef.CreateIndex(idx); // Create primary index
The comments //Primary, //Secondary, and //All refer to which fields are returned when you call the CDaoTableDef::GetIndexInfo() or CDaoRecordset::GetIndexInfo() functions. You must fill in all fields (except for CDaoIndexInfo::m_lDistinctCount, which is only used to return information about an existing index).
CAUTION
By design, a database can have one, and only one, primary key. The primary key is often used by the database engine to manage records in the table. A database table can have other, alternative keys if desired.
To create the secondary keys, use this code:
idx.m_strName = _T("EmailAddress"); // Primary idx.m_pFieldInfos = &fld; // Primary idx.m_nFields = 1; // Primary idx.m_bPrimary = FALSE; // Secondary idx.m_bUnique = FALSE; // Secondary idx.m_bClustered = FALSE; // Secondary idx.m_bIgnoreNulls = FALSE; // Secondary idx.m_bRequired = FALSE; // Secondary idx.m_bForeign = FALSE; // Secondary idx.m_lDistinctCount = 5; // All - returned, not set! fld.m_strName = _T("EmailAddress"); // Key field fld.m_bDescending = FALSE; // Ascending TableDef.CreateIndex(idx); // Create secondary index idx.m_strName = _T("LastName"); // Primary idx.m_pFieldInfos = &fld; // Primary idx.m_nFields = 1; // Primary idx.m_bPrimary = FALSE; // Secondary idx.m_bUnique = FALSE; // Secondary idx.m_bClustered = FALSE; // Secondary idx.m_bIgnoreNulls = FALSE; // Secondary idx.m_bRequired = FALSE; // Secondary idx.m_bForeign = FALSE; // Secondary idx.m_lDistinctCount = 5; // All - returned, not set! fld.m_strName = _T("LastName"); // Key field fld.m_bDescending = FALSE; // Ascending TableDef.CreateIndex(idx); // Create secondary index idx.m_strName = _T("PostalCode"); // Primary idx.m_pFieldInfos = &fld; // Primary idx.m_nFields = 1; // Primary idx.m_bPrimary = FALSE; // Secondary idx.m_bUnique = FALSE; // Secondary idx.m_bClustered = FALSE; // Secondary idx.m_bIgnoreNulls = FALSE; // Secondary idx.m_bRequired = FALSE; // Secondary idx.m_bForeign = FALSE; // Secondary idx.m_lDistinctCount = 5; // All - returned, not set! fld.m_strName = _T("PostalCode"); // Key field fld.m_bDescending = FALSE; // Ascending TableDef.CreateIndex(idx); // Create secondary index // Done creating the indexes...
There's no need to call Append(), because this is done automatically by MFC's DAO implementation. Of course, in the preceding code, you should make sure that your parameters are set to reflect your database's design.
To add records to a DAO database, you follow procedures that are very similar to those used with ODBC. To keep this functionality separate from your database creation, you add record addition, deletion, and refresh in the view class instead of in the recordset class. You could place this functionality in either class, but the view class is a better place for record addition, deletion, and refresh.
To add this functionality, I've added three new selections to the Record menu: Add, Delete, and Refresh. I also used ClassWizard to add a handler for OnMove(). To make the user interface easier, I also added three buttons to the toolbar to perform these functions. Visual C++ 4 makes adding toolbar buttons easy. It takes more time to design and draw the button bitmaps than it does to actually implement them!
After adding the new menu selections, handlers are generated using ClassWizard for each one. As noted earlier, the handlers are added to the view class, keeping them separate from other code used in this sample program. Listing 14.2 shows the view class implementation. Additions and modifications appear in bold.
Listing 14.2. The view implementation class file.
// AddressesView.cpp : implementation of the CAddressesView class // #include "stdafx.h" #include "Addresses.h" #include "AddressesSet.h" #include "AddressesDoc.h" #include "AddressesView.h" #ifdef _DEBUG #define new DEBUG_NEW #undef THIS_FILE static char THIS_FILE[] = __FILE__; #endif ///////////////////////////////////////////////////////////////////////////// // CAddressesView IMPLEMENT_DYNCREATE(CAddressesView, CDaoRecordView) BEGIN_MESSAGE_MAP(CAddressesView, CDaoRecordView) //{{AFX_MSG_MAP(CAddressesView) ON_COMMAND(ID_RECORD_ADD, OnRecordAdd) ON_COMMAND(ID_RECORD_DELETE, OnRecordDelete) ON_COMMAND(ID_RECORD_REFRESH, OnRecordRefresh) //}}AFX_MSG_MAP // Standard printing commands ON_COMMAND(ID_FILE_PRINT, CDaoRecordView::OnFilePrint) ON_COMMAND(ID_FILE_PRINT_DIRECT, CDaoRecordView::OnFilePrint) ON_COMMAND(ID_FILE_PRINT_PREVIEW, CDaoRecordView::OnFilePrintPreview) END_MESSAGE_MAP() ///////////////////////////////////////////////////////////////////////////// // CAddressesView construction/destruction CAddressesView::CAddressesView() : CDaoRecordView(CAddressesView::IDD) { //{{AFX_DATA_INIT(CAddressesView) m_pSet = NULL; //}}AFX_DATA_INIT // TODO: add construction code here m_bAddMode = FALSE; } CAddressesView::~CAddressesView() { } void CAddressesView::DoDataExchange(CDataExchange* pDX) { CDaoRecordView::DoDataExchange(pDX); //{{AFX_DATA_MAP(CAddressesView) DDX_FieldText(pDX, IDC_ADDRESS, m_pSet->m_Address, m_pSet); DDV_MaxChars(pDX, m_pSet->m_Address, 255); DDX_FieldText(pDX, IDC_CITY, m_pSet->m_City, m_pSet); DDV_MaxChars(pDX, m_pSet->m_City, 50); DDX_FieldText(pDX, IDC_EMAIL, m_pSet->m_EmailAddress, m_pSet); DDV_MaxChars(pDX, m_pSet->m_EmailAddress, 50); DDX_FieldText(pDX, IDC_FAX, m_pSet->m_FaxNumber, m_pSet); DDV_MaxChars(pDX, m_pSet->m_FaxNumber, 30); DDX_FieldText(pDX, IDC_FIRSTNAME, m_pSet->m_FirstName, m_pSet); DDV_MaxChars(pDX, m_pSet->m_FirstName, 50); DDX_FieldText(pDX, IDC_HOMEPHONE, m_pSet->m_HomePhone, m_pSet); DDV_MaxChars(pDX, m_pSet->m_HomePhone, 30); DDX_FieldText(pDX, IDC_LASTNAME, m_pSet->m_LastName, m_pSet); DDV_MaxChars(pDX, m_pSet->m_LastName, 50); DDX_FieldText(pDX, IDC_NOTES, m_pSet->m_Notes, m_pSet); DDX_FieldText(pDX, IDC_POSTALCODE, m_pSet->m_PostalCode, m_pSet); DDV_MaxChars(pDX, m_pSet->m_PostalCode, 20); DDX_FieldText(pDX, IDC_STATE, m_pSet->m_StateOrProvince, m_pSet); DDV_MaxChars(pDX, m_pSet->m_StateOrProvince, 20); DDX_FieldText(pDX, IDC_WORKPHONE, m_pSet->m_WorkPhone, m_pSet); DDV_MaxChars(pDX, m_pSet->m_WorkPhone, 30); //}}AFX_DATA_MAP } BOOL CAddressesView::PreCreateWindow(CREATESTRUCT& cs) { // TODO: Modify the Window class or styles here by modifying // the CREATESTRUCT cs return CDaoRecordView::PreCreateWindow(cs); } void CAddressesView::OnInitialUpdate() { m_pSet = &GetDocument()->m_addressesSet; CDaoRecordView::OnInitialUpdate(); } ///////////////////////////////////////////////////////////////////////////// // CAddressesView printing BOOL CAddressesView::OnPreparePrinting(CPrintInfo* pInfo) { // Default preparation return DoPreparePrinting(pInfo); } void CAddressesView::OnBeginPrinting(CDC* /*pDC*/, CPrintInfo* /*pInfo*/) { // TODO: Add extra initialization before printing } void CAddressesView::OnEndPrinting(CDC* /*pDC*/, CPrintInfo* /*pInfo*/) { // TODO: Add cleanup after printing } ///////////////////////////////////////////////////////////////////////////// // CAddressesView diagnostics #ifdef _DEBUG void CAddressesView::AssertValid() const { CDaoRecordView::AssertValid(); } void CAddressesView::Dump(CDumpContext& dc) const { CDaoRecordView::Dump(dc); } CAddressesDoc* CAddressesView::GetDocument() // Non-debug version is inline { ASSERT(m_pDocument->IsKindOf(RUNTIME_CLASS(CAddressesDoc))); return (CAddressesDoc*)m_pDocument; } #endif //_DEBUG ///////////////////////////////////////////////////////////////////////////// // CAddressesView database support CDaoRecordset* CAddressesView::OnGetRecordset() { return m_pSet; } ///////////////////////////////////////////////////////////////////////////// // CAddressesView message handlers BOOL CAddressesView::OnMove(UINT nIDMoveCommand) { // TODO: Add your specialized code here and/or call the base class // Retrieve the current record set: CDaoRecordset* pRecordset = OnGetRecordset(); if (m_bAddMode) {// Do special handling if you are adding records: if (!UpdateData()) { // Could not get updated data from input dialog: return FALSE; } try {// Hope it works... pRecordset->Update(); } catch (CDaoException* e) {// ...but if it doesn't... AfxMessageBox(e->m_pErrorInfo->m_strDescription); e->Delete(); return FALSE; } pRecordset->Requery(); UpdateData(FALSE); m_bAddMode = FALSE; return TRUE; } else {// If not adding records, just move around: return CDaoRecordView::OnMove(nIDMoveCommand); } } void CAddressesView::OnRecordAdd() { // TODO: Add your command handler code here // If already in add mode, complete previous new record add: if (m_bAddMode) {// If adding, first add the current record: OnMove(ID_RECORD_FIRST); } // Switch into the Add mode (even if already adding): OnGetRecordset()->AddNew(); m_bAddMode = TRUE; UpdateData(FALSE); return; } void CAddressesView::OnRecordDelete() { // TODO: Add your command handler code here CDaoRecordset* pRecordset = OnGetRecordset(); try {// It better work... pRecordset->Delete(); } catch (CDaoException* e) {// ...but if it didn't... AfxMessageBox(e->m_pErrorInfo->m_strDescription); e->Delete(); return; } // Move to the next record after the one just deleted pRecordset->MoveNext(); // Ensure that the move is valid: if (pRecordset->IsEOF()) { pRecordset->MoveLast(); } if (pRecordset->IsBOF()) { pRecordset->SetFieldNull(NULL); } UpdateData(FALSE); return; } void CAddressesView::OnRecordRefresh() { // TODO: Add your command handler code here if (m_bAddMode) {// Was the user adding a record? If so, exit add mode. try {// It better work... OnGetRecordset()->CancelUpdate(); OnGetRecordset()->Move(0); } catch (CDaoException* e) {// ...but if it didn't... AfxMessageBox(e->m_pErrorInfo->m_strDescription); e->Delete(); return; } m_bAddMode = FALSE; } // Restore the user's original values (as necessary): UpdateData(FALSE); return; }
To manage the process of record addition, you must add a flag to your view class. I've used a Boolean variable called m_bAddMode, which is set to TRUE whenever the user is adding a new record to the database. This variable is initialized in the constructor to FALSE.
In addition to the code to manage the three new functions (add, delete, and refresh), you also must add code to manage the task of record navigation. This is necessary because if the user starts to add a new record and then selects one of the record navigation functions (next, previous, first, or last record), you must add the new record before you perform the navigation action requested by the user.
You do this using the add record flag m_bAddMode, which, if TRUE, tells you that a record is being added. When you're in add mode, you must save the record being added (if the database supports updatingthat is, if it isn't read-only). The record is saved using the CDaoRecordset::Update() function (use exception handling to catch errors), which will add the current record to the database. Once the database has been updated, it is then requeried, the view is updated, and you return.
If the user isn't currently in the add record mode, you simply do the necessary move using the default CDaoRecordView::OnMove(nIDMoveCommand) handler:
CDaoRecordset* pRecordset = OnGetRecordset(); if (m_bAddMode) {// Do special handling if you are adding records: if (!UpdateData()) { // Could not get updated data from input dialog: return FALSE; } try {// Hope it works... pRecordset->Update(); } catch (CDaoException* e) {// ...but if it doesn't... AfxMessageBox(e->m_pErrorInfo->m_strDescription); e->Delete(); return FALSE; } pRecordset->Requery(); UpdateData(FALSE); m_bAddMode = FALSE; return TRUE; } else {// If not adding records, just move around: return CDaoRecordView::OnMove(nIDMoveCommand); }
The add record handler checks the add flag. If the user is already in the add record mode, it adds the current record and then prepares for a new added record. A user who needs to add several records would simply select add prior to each record to be added:
if (m_bAddMode) {// If adding, first add the current record: OnMove(ID_RECORD_FIRST); } // Switch into the Add mode (even if already adding): OnGetRecordset()->AddNew(); m_bAddMode = TRUE; UpdateData(FALSE); return;
Deleting records is straightforward. You call the recordset's delete member function, which should delete the current record (if there is one). Next you must move to the next record (giving the user something to look at). Moving to the next record presents a problem, because the application must compute which record is next. The user could have deleted the last record (then the application must move back one record). Also, the application must handle the situation that arises when the user deletes all the records in the database:
CDaoRecordset* pRecordset = OnGetRecordset(); try {// It better work... pRecordset->Delete(); } catch (CDaoException* e) {// ...but if it didn't... AfxMessageBox(e->m_pErrorInfo->m_strDescription); e->Delete(); return; } // Move to the next record after the one just deleted pRecordset->MoveNext(); // Ensure that the move is valid: if (pRecordset->IsEOF()) { pRecordset->MoveLast(); } if (pRecordset->IsBOF()) { pRecordset->SetFieldNull(NULL); } UpdateData(FALSE); return;
Finally, when the user selects the refresh option, you must restore the current record's contents. This functionality is required to handle the situation in which the user has modified a record in error and realizes the error prior to updating the database by doing a record move. You must handle two situations with the refresh handler: the user is modifying an existing record, and the user has (in error) added a record that he doesn't want.
When aborting an add record situation, you must cancel the add using the CDaoRecordset::CancelUpdate() function. After you cancel the add, you move to the current record. You also turn off the add mode by setting m_bAddMode to FALSE. To cancel an update to an existing record, you simply retrieve the record's original contents and redisplay this information. You do this using the view class's UpdateData() member function:
if (m_bAddMode) {// Was the user adding a record? If so, exit add mode. try {// It better work... OnGetRecordset()->CancelUpdate(); OnGetRecordset()->Move(0); } catch (CDaoException* e) {// ...but if it didn't... AfxMessageBox(e->m_pErrorInfo->m_strDescription); e->Delete(); return; } m_bAddMode = FALSE; } // Restore the user's original values (as necessary): UpdateData(FALSE); return;
To use the CDaoRecordset object directly, you must add a new class to your application using ClassWizard. Base your new class on CDaoRecordset. ClassWizard will prompt you for the DAO datasource and table to use. As in a typical CDaoRecordView application, all columns in the table(s) selected will be bound into your CDaoRecordset object. To display ClassWizard's Create New Class dialog, shown in Figure 14.6, select the Class Info tab in ClassWizard and then click the Add Class button.
Figure 14.6. Adding a CDaoRecordset object in a project.
To use this new class (in the example, the new CDaoRecordset object is called COurDaoRecordset), simply add the header file to source file. For example, to include a COurDaoRecordset object in your dialog box, add the following to the #include statements in the dialog box's source file:
// DaoRecordsetDemo.h : header file // ///////////////////////////////////////////////////////////////////////////// // CDaoRecordsetDemo dialog #include "OurDaoRecordset.h"
Then add a member variable m_Set of type OurDaoRecordset in your DaoRecordsetDemo class definition.
You must have controls in your dialog box to bind to your database columns. In this example, two controlsIDC_FIRST_NAME and IDC_LAST_NAMEare used to display the first and last name columns from the database's Addresses table. When the CDaoRecordset object is created, it will have member variables for each of the columns in the table.
The following code fragment shows the binding of COurDaoRecordset's variables to the controls in the dialog box. Notice that you must write this code by hand: ClassWizard won't recognize the COurDaoRecordset member variables. Be careful to place these field exchange statements after the ClassWizard block:
void CDaoRecordsetDemo::DoDataExchange(CDataExchange* pDX) { CDialog::DoDataExchange(pDX); //{{AFX_DATA_MAP(CDaoRecordsetDemo) // NOTE: ClassWizard will add DDX and DDV calls here //}}AFX_DATA_MAP // Add *after* the //AFX_DATA_MAP block! DDX_FieldText(pDX, IDC_FIRST_NAME, m_Set.m_FirstName, &m_Set); DDV_MaxChars(pDX, m_Set.m_FirstName, 50); DDX_FieldText(pDX, IDC_LAST_NAME, m_Set.m_LastName, &m_Set); DDV_MaxChars(pDX, m_Set.m_FirstName, 50); }
The DDX_FieldText() macros (and all other DDX_Field...() macros) require a pointer to your CDaoRecordset object as the final parameter.
Next, the COurDaoRecordset object must be initialized. That is, the datasource must be opened. In a dialog box, you can do this in either the constructor or in the WM_INITDIALOG handler:
BOOL CDaoRecordsetDemo::OnInitDialog() { CDialog::OnInitDialog(); // TODO: Add extra initialization here m_Set.Open(); UpdateData(FALSE); return TRUE; // Return TRUE unless you set the focus to a control // EXCEPTION: OCX Property Pages should return FALSE }
When you do the COurDaoRecordset open in the OnInitDialog() handler, you must tell the dialog manager to update the controls with a call to UpdateData(FALSE). If you forget to do this, the controls that reflect records from the datasource will initially be blank.
At this stage, you have an open datasource. The first record in the datasource will be displayed in the dialog box's controls. You don't have any way to select another record, because there are no record navigation controls!
In the sample program, I've added four buttons to the dialog box: First, Next, Previous, and Last. These controls execute a set of simple functions when clicked, as you will see. The First button checks to see if any records in the datasource have been opened and then moves to the first record if there is one. In all the record navigation examples shown here, a call to MessageBeep() signals the user if the requested action can't be accomplished. Many applications might not need this type of notification. Disabling invalid navigation buttons might be a more elegant solution.
void CDaoRecordsetDemo::OnFirst() { // TODO: Add your control notification handler code here if (!m_Set.IsEOF() || !m_Set.IsBOF()) {// Ensure that it's not a null recordset! m_Set.MoveFirst(); UpdateData(FALSE); } else {// For demo purposes... MessageBeep(MB_ICONEXCLAMATION); } }
The Next record button's handler is similar to the First record button's handler, as the following code shows. In this case, it isn't necessary to check whether the recordset is empty. You only need to make sure that you aren't moving past the end of the recordset:
void CDaoRecordsetDemo::OnNext() { // TODO: Add your control notification handler code here if (!m_Set.IsEOF()) {// Ensure that it's not a null recordset! m_Set.MoveNext(); if (m_Set.IsEOF()) {// Off the end of the world? m_Set.MovePrev(); } UpdateData(FALSE); } else {// For demo purposes... MessageBeep(MB_ICONEXCLAMATION); } }
After you've moved to the next record, you then check to make sure you haven't moved past the end of the recordset (this would cause the user to see a blank record). If you have moved past the end of the recordset, you move back one record. When you're done moving, you tell the dialog manager to update the dialog box's controls. As in the other record navigation examples, you sound a beep if the user's requested move can't be performed.
For programs that need to access a datasource and that aren't presenting records to the user (such as a program that rolls up data for reports or graphs), the techniques are virtually identical: Create your CDaoRecordset object, open it, and navigate through each record in turn until you've reached the end of the records. Use the CDaoRecordset member variables to access the column's data.
Sometimes you will need to use DAO to open a database on-the-fly. The application usually won't know which columns or tables are present in the database at design time, so they'll have to query the database for this information.
For example, your application would create a CDaoDatabase object and then open this database. You would have to either query the user for the datasource (typical) or obtain the datasource name from another location that your application knows about. A typical example of this is the inclusion of the database information in either the registry or an .INI file.
The sample program's dynamic DAO dialog box prompts the user for a datasource name and then queries the datasource for the name and other information about each table in the datasource. Each table is in turn queried about columns (fields) that are contained within the table. For each table that has records, the first record is retrieved. If the column type is character, you present the contents of the first record. This process is actually rather simple, as Listing 14.3 shows. In this listing, the major change is the addition of code in the handler for the Open button in the dialog box.
Listing 14.3. DAO using direct calls: DaoDatabaseDlg.cpp.
// DaoDatabaseDlg.cpp : implementation file // #include "stdafx.h" #include "dao direct.h" #include "DaoDatabaseDlg.h" #ifdef _DEBUG #define new DEBUG_NEW #undef THIS_FILE static char THIS_FILE[] = __FILE__; #endif ///////////////////////////////////////////////////////////////////////////// // CDaoDatabaseDlg dialog CDaoDatabaseDlg::CDaoDatabaseDlg(CWnd* pParent /*=NULL*/) : CDialog(CDaoDatabaseDlg::IDD, pParent) { //{{AFX_DATA_INIT(CDaoDatabaseDlg) // NOTE: ClassWizard will add member initialization here //}}AFX_DATA_INIT } void CDaoDatabaseDlg::DoDataExchange(CDataExchange* pDX) { CDialog::DoDataExchange(pDX); //{{AFX_DATA_MAP(CDaoDatabaseDlg) // NOTE: ClassWizard will add DDX and DDV calls here //}}AFX_DATA_MAP } BEGIN_MESSAGE_MAP(CDaoDatabaseDlg, CDialog) //{{AFX_MSG_MAP(CDaoDatabaseDlg) ON_BN_CLICKED(IDC_OPEN_DATABASE, OnOpenDatabase) //}}AFX_MSG_MAP END_MESSAGE_MAP() ///////////////////////////////////////////////////////////////////////////// // CDaoDatabaseDlg message handlers void CDaoDatabaseDlg::OnOpenDatabase() { // TODO: Add your control notification handler code here // Create a CDaoDatabase object first: CDaoDatabase cdb; CDaoTableDef TableDef(&cdb); CDaoRecordset Recordset(&cdb); CDaoTableDefInfo tabledefinfo; CDaoFieldInfo fieldinfo; COleVariant OleVariant; CString output; int nCount = 0; int nFieldCount = 0; CFileDialog dlg(TRUE, NULL, "*.mdb", OFN_OVERWRITEPROMPT, "Access Database Files (*.mdb) | *.mdb | " "All Files (*.*) | *.* ||"); if (dlg.DoModal()) {// User did not cancel the open file dialog: TRACE("File selected is '%s'\n", dlg.GetPathName()); // As applicable, check to see if the database is already open if (cdb.IsOpen()) {// If open (avoid errors and crashes!), then close: cdb.Close(); } // Save the user's database filename if necessary // Open the database cdb.Open(dlg.GetPathName()); nCount = cdb.GetTableDefCount(); for (int i = 0; i < nCount; i++) { cdb.GetTableDefInfo(i, tabledefinfo, AFX_DAO_ALL_INFO); TRACE("Table %d m_strName '%s' m_lRecordCount '%d'\n", i, tabledefinfo.m_strName, tabledefinfo.m_lRecordCount); // Open the current table (for column names) TableDef.Open(tabledefinfo.m_strName); // Get number of columns in table: nFieldCount = TableDef.GetFieldCount(); for (int j = 0; j < nFieldCount; j++) { try { // This should work... TableDef.GetFieldInfo(j, fieldinfo, AFX_DAO_ALL_INFO); } catch (CDaoException* e) {// ...but if it doesn't... AfxMessageBox(e->m_pErrorInfo->m_strDescription); e->Delete(); } if (tabledefinfo.m_lRecordCount > 0) { Recordset.Open(&TableDef, dbOpenTable); OleVariant = Recordset.GetFieldValue(fieldinfo.m_strName); if (fieldinfo.m_nType == 10) //CString type... {// Character data. Convert to CString: output = VARIANT(OleVariant).pbVal; TRACE(" Column %d: m_strName '%s' " "First Record Data '%s'\n", j, fieldinfo.m_strName , output); } else {// A datatype we don't handle in this example! TRACE(" Column %d: m_strName '%s' Type '%d'\n", j, fieldinfo.m_strName, fieldinfo.m_nType); } Recordset.Close(); } else { TRACE(" Column %d: m_strName '%s' Type '%d'\n", j, fieldinfo.m_strName, fieldinfo.m_nType); } } TableDef.Close(); } // When done, close up to be nice to the world: cdb.Close(); } else {// User aborted. Do nothing, or clean up if appropriate. } }
The process is a simple set of steps:
Typically, the table's data will be retrieved row by row. Applications normally wouldn't store or otherwise save a row's data specifically, except to place the data into a control such as a grid control. Rather, the application would retrieve a row's data as needed.
The preceding program might, for the Addresses database, return the information in Visual C++ 4.0's debug window (note the TRACE functions). A sample output is shown in Listing 14.4. When you run this sample program, you might receive slightly different results, but the basic output will be the same as that shown in the listing.
Listing 14.4. The output of a direct DAO database access.
File selected is 'I:\Database Developers Guide with Visual C++ 4 [ic:ccc]\Source CD\CHAPTR14\Address Book.mdb' Table 0 m_strName 'Addresses' m_lRecordCount '5' Column 0: m_strName 'AddressID' Type '4' Column 1: m_strName 'FirstName' First Record Data 'Nancy' Column 2: m_strName 'LastName' First Record Data 'Davolio' Column 3: m_strName 'SpouseName' First Record Data 'Paul' Column 4: m_strName 'Address' First Record Data '507-20th Ave. E. Apt. 2A' Column 5: m_strName 'City' First Record Data 'Seattle' Column 6: m_strName 'StateOrProvince' First Record Data 'WA' Column 7: m_strName 'PostalCode' First Record Data '98122' Column 8: m_strName 'Country' First Record Data 'USA' Column 9: m_strName 'EmailAddress' First Record Data 'nancyd@anywhere.com' Column 10: m_strName 'HomePhone' First Record Data '(504) 555-9857' Column 11: m_strName 'WorkPhone' First Record Data '(504) 555-9922' Column 12: m_strName 'WorkExtension' First Record Data '' Column 13: m_strName 'MobilePhone' First Record Data '(504) 555-8822' Column 14: m_strName 'FaxNumber' First Record Data '(504) 555-7722' Column 15: m_strName 'Birthdate' Type '8' Column 16: m_strName 'SendCard' Type '1' Column 17: m_strName 'Nickname' First Record Data '' Column 18: m_strName 'Notes' Type '12' Column 19: m_strName 'Hobbies' First Record Data [ic:ccc]'Drinking white wine while composing music' Table 1 m_strName 'MSysACEs' m_lRecordCount '81' Table 2 m_strName 'MSysIMEXColumns' m_lRecordCount '0' Column 0: m_strName 'DataType' Type '3' Column 1: m_strName 'FieldName' Type '10' Column 2: m_strName 'IndexType' Type '2' Column 3: m_strName 'SkipColumn' Type '1' Column 4: m_strName 'SpecID' Type '4' Column 5: m_strName 'Start' Type '3' Column 6: m_strName 'Width' Type '3' Table 3 m_strName 'MSysIMEXSpecs' m_lRecordCount '0' Column 0: m_strName 'DateDelim' Type '10' Column 1: m_strName 'DateFourDigitYear' Type '1' Column 2: m_strName 'DateLeadingZeros' Type '1' Column 3: m_strName 'DateOrder' Type '3' Column 4: m_strName 'DecimalPoint' Type '10' Column 5: m_strName 'FieldSeparator' Type '10' Column 6: m_strName 'FileType' Type '3' Column 7: m_strName 'SpecID' Type '4' Column 8: m_strName 'SpecName' Type '10' Column 9: m_strName 'SpecType' Type '2' Column 10: m_strName 'StartRow' Type '4' Column 11: m_strName 'TextDelim' Type '10' Column 12: m_strName 'TimeDelim' Type '10' Table 4 m_strName 'MSysModules' m_lRecordCount '8' Column 0: m_strName 'Flags' Type '4' Column 1: m_strName 'Form' Type '11' Column 2: m_strName 'Module' Type '11' Column 3: m_strName 'Name' First Record Data 'Lock' Column 4: m_strName 'ReplicationVersion' Type '4' Column 5: m_strName 'Type' Type '4' Column 6: m_strName 'TypeInfo' Type '11' Column 7: m_strName 'Version' Type '4' Table 5 m_strName 'MSysObjects' m_lRecordCount '28' Table 6 m_strName 'MSysQueries' m_lRecordCount '0' Table 7 m_strName 'MSysRelationships' m_lRecordCount '0' Table 8 m_strName 'MSysToolbars' m_lRecordCount '0' Column 0: m_strName 'Grptbcd' Type '11' Column 1: m_strName 'TbName' Type '10' Table 9 m_strName 'Switchboard Items' m_lRecordCount '11' Column 0: m_strName 'SwitchboardID' Type '4' Column 1: m_strName 'ItemNumber' Type '3' Column 2: m_strName 'ItemText' First Record Data 'Main Switchboard' Column 3: m_strName 'Command' Type '3' Column 4: m_strName 'Argument' First Record Data 'Default'
In this chapter you developed two programs. The first used DAO to manipulate an Access database, and the second returned information about an Access database, including table and column information. These two programs showed you how to use the DAO MFC class objects, including
CDaoDatabase
CDaoException
CDaoFieldExchange
CDaoQueryDef
CDaoRecordset
CDaoRecordView
CDaoTableDef
CDaoWorkspace
You saw several sample programs and routines, as well as a typical session with AppWizard creating a DAO project. You also saw how to use DAO directly, without using AppWizard- or ClassWizard-created code.