Previous Page TOC Next Page



- 2 -
Understanding MFC's Data Access Classes


The MFC data access classes are Visual C++'s object-oriented method of interacting with datasources. MFC's implementation of ODBC supports three major objects: CDatabase, CRecordView, and CRecordset. Supporting classes include CDBException, CFieldExchange, and CLongBinary. Most commonly, programmers use these objects when working with applications created with Visual C++'s AppWizard program. Any database application created by using AppWizard will incorporate these classes.

Chapter 1 introduced Visual C++ and accessing databases. This chapter describes the structure of the MFC data access classes in detail because the member functions of these classes constitute the foundation on which all of your Visual C++ MFC database applications are built. This chapter features examples that use the member functions to create Visual C++ code. By the time you complete this rather lengthy chapter, it's very likely that you will have learned more than you ever wanted to know about data-related objects and classes!

Programmers who want to "roll their own" and use the database classes will have few (if any) problems incorporating them into their applications. However, for a simple front-end application in which data access and updating are the main functions of the program, using AppWizard is the simplest choice. The sample program shown in Figure 2.1 took only about 10 minutes to write and required no manual source code modification to create. The source code for this program is on the CD that comes with this book (see the directory CHAPTR02\Record View). Take a look at the program to see how simple it is to create a quick ODBC application.

Figure 2.1. A CRecordView-based application.



NOTE

Also found on the CD is a 16-bit MFC 2.5 version of the same program, which is in the directory CHAPTR02\RECVIEW. The RECVIEW program should be built only by using Visual C++ 1.5x.



Defining the Characteristics of Data-Related Objects and Classes


In the object-oriented terminology of OLE, objects are containers for properties, methods, and other objects. Methods are called member functions of an object. They perform an action on the object, such as changing its color, size, or shape. Properties are member function pairs of a programmable object. You can set or return information about the state of a programmable object, such as the value of a data item in the field of a table. One member function sets the data, and another member function returns the data—thus, the term pair. All of the member functions are said to be encapsulated in the object.



NOTE

Technically, you should be able to alter any property of a programmable object by assigning an appropriate value to the "set" member of the function pair. The ability to set property values under specific conditions depends on the type of object and the application in which the object is used. Access 1.x, for example, has many objects whose properties can be set only in design mode.



The MFC Database Classes


The MFC database classes may be implemented by the programmer or by AppWizard when it creates an application; in either case, these classes are easy to use.

There are differences between MFC 2.5 (the 16-bit version, used with Visual C++ 1.5x), MFC 3.0 (a 32-bit version, used with Visual C++ 2.x), and MFC 4, which is supplied with Visual C++ 4.0. Where significant differences exist, I describe both. Most differences are in between the 16-bit MFC 2.5 and the later 32-bit versions. In keeping with the concept of compatibility, generally an application written for MFC 2.5 will run without error when recompiled by using one of the 32-bit versions of Visual C++. However, after you've implemented any of the enhancements from MFC 3.0 (or MFC 4) in a 16-bit MFC 2.5 application (and converted it to 32-bits), you won't be able to build that application using Visual C++ 1.5!

CDatabase


The CDatabase class object is used to encapsulate a connection to a database. The CDatabase object may then be used to operate on the database and is derived from the CObject base class. Figure 2.2 shows the class hierarchy for the CDatabase class.

Figure 2.2. The CDatabase class hierarchy.

The CDatabase class object has a number of member functions. These functions are divided into the following five categories:

The following sections take a closer look at the members of this class. The members of the CObject class (which CDatabase is derived from) aren't covered in this book. Refer to the Visual C++ documentation (either the manuals or the online help system) for full information about the CObject class.

Data Members

The CDatabase object contains only one data member.

The m_hdbc member variable contains the ODBC connection handle to the database that is currently open. If no database is currently open, this member variable doesn't contain useful data.

The m_hdbc member variable is of type HDBC. It can be used wherever an HDBC type variable is needed (for example, in one of the SQL...() functions).

Here's an example of the usage of m_hdbc:


nReturnCode = ::SQLGetInfo(cdb.m_hdbc, SQL_ODBC_SQL_CONFORMANCE,

    &nReturn, sizeof(nReturn), &cbValue);

In this example, a call to a function that isn't a member function of CDatabase is made by using the m_hdbc member data variable.

Construction

Three member functions deal directly with CDatabase construction: CDatabase(), Open(), and Close(). There also is a default destructor, which I don't document here because it's never called by an application. The following paragraphs describe each construction member function and, where applicable, give examples of usage.

The CDatabase() function is used to construct the CDatabase object. This function isn't called directly and has no parameters. The process of calling the constructor is taken care of during the initialization of the CDatabase object when it's created. Here is a typical creation of a CDatabase object (this code is usually in the header file for the document class):


CDatabase m_dbCustomerDB;  // No parameters

When creating a CDatabase object, your application must make sure that the CDatabase object is connected to a database. This is accomplished in a member function in the containing class, often called GetDatabase() (if the containing class is based on a CDocument type object).

If you call CDatabase::Open(), passing a NULL as the lpszDSN parameter, the user will be presented with an open datasource dialog box. The Record View sample program for this chapter shows this dialog box (see Figure 2.1).


CDatabase* CMyDoc::GetDatabase()

{// Returns NULL in the event of a failure!

// m_dbCustomerDB is a member of CMyDoc!

    // Connect the object to a database

    if(!m_dbCustomerDB.IsOpen() && !m_dbCustomerDB.Open(NULL))

    (// The database cannot be opened; we've failed!

        return(NULL);

    }

    else

    {// We already had a database, or opened one:

        return(&m_dbCustomerDB);

    }

}

The Open() member function is used to establish a connection to a database. This connection is established through an ODBC driver. The Open() function takes a number of parameters. Here's the prototype of the MFC 2.5 version of the Open() function:


BOOL Open( LPCSTR lpszDSN,          // The name of the dataset

    BOOL bExclusive = FALSE,        // If the dataset is to be exclusive

    BOOL bReadOnly = FALSE,         // If the dataset is read-only

    LPCSTR lpszConnect = "ODBC;");  // The method of connection

The prototype of the MFC 3.0 (and later) versions of Open() function adds a new final parameter to the function:


BOOL Open( LPCSTR lpszDSN,         // The name of the dataset

    BOOL bExclusive = FALSE,       // If the dataset is to be exclusive

    BOOL bReadOnly = FALSE,        // If the dataset is read-only

    LPCSTR lpszConnect = "ODBC;",  // The method of connection

    BOOL bUseCursorLib = TRUE);    // Use ODBC cursor library

The return value will be nonzero if the function is successful and zero if the user clicks the Cancel button in the Connection Information dialog box (if displayed). All other failures will cause the Open() function to throw an exception of type CDBException or CMemoryException.

The Close() function is used to close the connection that was established with the Open() function. The Close() function takes no parameters and has no return value. If no connection is currently open, this function does nothing. A call to Close() will cancel any pending AddNew() or Edit() statements and will roll back (discard) any pending transactions.

Database Attributes

The database attribute functions are used to provide information to the application about the connection, driver, and datasource. These functions are often used in front-end applications. Other functions in this group set options for the datasource for the application. The following list shows the database attribute functions. The functions in the first column are supported by all datasources, and those in the second column might not be supported by all datasources.

Supported by All Datasources Not Supported by All Datasources
GetConnect() SetLoginTimeout()
IsOpen() SetQueryTimeout()
GetDatabaseName() SetSynchronousMode()
CanUpdate()
CanTransact()
InWaitForDataSource()

The GetConnect() function is used to return the ODBC connect string that was used to connect the CDatabase object to a datasource. There are no parameters to the GetConnect() function, and it returns a CString object reference. The GetConnect() function's prototype is


const CString& GetConnect();

If there is no current connection, the returned CString object will be empty.

The IsOpen() function is used to determine whether a datasource is currently connected to the CDatabase object. This function returns a nonzero value if there is currently a connection and a zero value if no connection is currently open. For an example of IsOpen(), see the earlier discussion of the Open() function.

The GetDatabaseName() function returns the name of the database currently in use. GetDatabaseName() returns a CString object. Its prototype is


CString GetDatabaseName();

The GetDatabaseName() function returns the database name if there is one. Otherwise, it returns an empty CString object.

The CanUpdate() function returns a nonzero value if the database can be updated (by either modifying records or adding new records). If the database can't be modified, the CanUpdate() function returns a zero value. CanUpdate() takes no parameters and has the following prototype:


BOOL CanUpdate();

The ability to update a database is based both on how it was opened (how you set the read-only parameter in Open()) and on the capabilities of the ODBC driver. Not all ODBC drivers support the updating of databases.

The CanTransact() function returns a nonzero value if the datasource supports transactions. (See the section "Database Operations" for more information about transactions with the CDatabase object.) The CanTransact() function takes no parameters and has the following prototype:


BOOL CanTransact();

The ability to support transactions is based on ODBC driver support.

The InWaitForDataSource() function returns a nonzero value if the application is waiting for the database server to complete an operation. If the application isn't waiting for the server, the InWaitForDataSource() function returns a zero. InWaitForDataSource() takes no parameters and has the following prototype:


static BOOL PASCAL InWaitForDataSource();

This function is often called in the framework to disable the user interface while waiting for the server to respond. This is done to prevent the user from stacking unwanted commands or operations while the application waits for the server.

The SetLoginTimeout() function is used to set the amount of time that the system will wait before timing out the connection. This option must be set before a call to Open() is made; it will have no effect if it's called after a database has been opened. This function has no return value. SetLoginTimeout() takes one parameter—the number of seconds after which a datasource connection attempt will time out. SetLoginTimeout() has the following prototype:


void SetLoginTimeout(DWORD dwSeconds);

The default login timeout is 15 seconds, an acceptable value for most applications. For applications that might be running on slow systems (perhaps where there are many other connections), the login timeout value might need to be set to a larger value.

The SetQueryTimeout() function is used to set the amount of time that the system will wait before timing out the query. This option must be set before you open the recordset. It will have no effect if it's called after the recordset has been opened. This function has no return value. SetQueryTimeout() takes one parameter—the number of seconds after which a datasource connection attempt will time out. SetQueryTimeout() has the following prototype:


void SetQueryTimeout(DWORD dwSeconds);

The default query timeout is 15 seconds, an acceptable value for most applications. For applications that might be running on slow systems (perhaps where there are many other connections), the query timeout value might need to be set to a larger value.

WARNING

Setting the query timeout value to zero results in no time-outs and might cause the application to hang if a connection can't be made.

The SetQueryTimeout() function affects all subsequent Open(), AddNew(), Edit(), and Delete() calls.

The SetSynchronousMode() function is used to either enable or disable synchronous processing for all recordsets and SQL statements associated with this CDatabase object. SetSynchronousMode() takes one parameter and has no return value. SetSynchronousMode() has the following prototype:


void SetSynchronousMode(BOOL bSynchronousMode);

The default operation is asynchronous processing.

Database Operations

Database operation functions are used to work with the database. The transaction processing functions (used to update the database) and the function used to issue an SQL command are all database operation functions. The database operation functions are

ExecuteSQL()

With the exception of ExecuteSQL(), these functions might not be implemented by all datasources. The BeginTrans() function is used to start a transaction on a database. Transactions are calls to AddNew(), Edit(), Delete(), or Update(). After the application has completed the transaction calls, either CommitTrans() or Rollback() must be called.

The BeginTrans() function takes no parameters and returns a nonzero value if the call is successful. BeginTrans() has the following prototype:


void BeginTrans(BOOL bSynchronousMode);

BeginTrans() should never be called prior to opening a recordset; otherwise, there might be problems when calling Rollback(). Each BeginTrans() call must be matched to a CommitTrans() or Rollback() prior to a subsequent call to BeginTrans(), or an error will occur. If there are pending transactions when the datasource is closed, they are discarded, much as if there had been a call to Rollback() prior to closing the datasource.

The CommitTrans() function is used to complete a transaction set begun with a call to BeginTrans(). CommitTrans() tells the datasource to accept the changes that were specified. CommitTrans() takes no parameters and returns a nonzero value if the call is successful. CommitTrans() has the following prototype:


BOOL CommitTrans();

You can discard the transaction by calling Rollback().

The Rollback() function is used to end a transaction processing operation, discarding the transaction. Rollback() takes no parameters and returns a nonzero value if the call was successful. Rollback() has the following prototype:


void Rollback();

You can accept the transaction by using the CommitTrans() function.

The Cancel() function is used to terminate an asynchronous operation that is currently pending. This function causes the OnWaitForDataSource() function to be called until it returns a value other than SQL_STILL_EXECUTING. Cancel() takes no parameters and has no return value. Cancel() has the following prototype:


void Cancel();

If no asynchronous operation is pending, this function simply returns.

The ExecuteSQL() function is used to execute an SQL command. The SQL command is contained in a NULL-terminated string. A CString object may also be passed to the ExecuteSQL() function if desired. ExecuteSQL() takes one parameter and has no return value. ExecuteSQL() has the following prototype:


void ExecuteSQL(LPCSTR szSQLCommand);

The ExecuteSQL() function throws a CDBException if there is an error in the SQL statement. ExecuteSQL() won't return any data records to the application. Use the CRecordset object to obtain records instead.

Database Overridables

The overridable functions OnSetOptions() and OnWaitForDataSource() are used to allow the framework to set options and control the operation of the application. Neither of these functions is mandatory. If the programmer elects not to code these functions, a default operation will take place.

The OnSetOptions() function is called when the ExecuteSQL() function is being used to execute an SQL statement. OnSetOptions() takes one parameter and has no return value. OnSetOptions() has the following prototype:


void OnSetOptions(HSTMT hstmt);

The default OnSetOptions() function is shown in the following code fragment. You could use this code in your handler as an example of how to code an OnSetOptions() function. The default implementation sets the query timeout value and the processing mode to either asynchronous or synchronous. Your application can set these options prior to the ExecuteSQL() function call by calling SetQueryTimeout() and SetSynchronousMode(). Microsoft uses the calls to AFX SQL SYNC() in its database code.


void CDatabase::OnSetOptions(HSTMT hstmt)

{

    RETCODE nRetCode;

    ASSERT_VALID(this);

    ASSERT(m_hdbc != SQL_NULL_HDBC);

    if (m_dwQueryTimeout != -1)

    {

        // Attempt to set query timeout.  Ignore failure

        AFX_SQL_SYNC(::SQLSetStmtOption(hstmt, SQL_QUERY_TIMEOUT,

            m_dwQueryTimeout));

        if (!Check(nRetCode))

            // don't attempt it again

            m_dwQueryTimeout = (DWORD)-1;

    }

    // Attempt to set AFX_SQL_ASYNC.  Ignore failure

    if (m_bAsync)

    {

        AFX_SQL_SYNC(::SQLSetStmtOption(hstmt, SQL_ASYNC_ENABLE, m_bAsync));

        if (!Check(nRetCode))

            m_bAsync = FALSE;

    }

}

The OnWaitForDataSource() function is called to allow the application to yield time to other applications while waiting for asynchronous operations. OnWaitForDataSource() takes one parameter and has no return value. OnWaitForDataSource() has the following prototype:


void OnWaitForDataSource(BOOL bStillExecuting);

The bStillExecuting parameter is set to TRUE for the first call to OnWaitForDataSource() when it's called prior to an asynchronous operation.

The following code fragment shows the default OnWaitForDataSource() function. You could use this code in your handler as an example of how to code an OnWaitForDataSource() function if your application requires one.


void CDatabase::OnWaitForDataSource(BOOL bStillExecuting)

{

    ASSERT_VALID(this);

    ASSERT(m_hdbc != SQL_NULL_HDBC);

    _AFX_THREAD_STATE* pThreadState = AfxGetThreadState();

    CWinApp* pApp = AfxGetApp();

    if (!bStillExecuting)

    {

        // If never actually waited...

        if (m_dwWait == 0)

            return;

        if (m_dwWait == m_dwMaxWaitForDataSource)

            pApp->DoWaitCursor(-1);      // EndWaitCursor

        m_dwWait = 0;

        pThreadState->m_bWaitForDataSource--;

#ifdef _DEBUG

        if (afxTraceFlags & traceDatabase)

            TRACE0("DONE WAITING for datasource.\n");

#endif

        return;

    }

    if (m_dwWait == 0)

    {

        pThreadState->m_bWaitForDataSource++;

        // 1st call; wait for min amount of time

        m_dwWait = m_dwMinWaitForDataSource;

#ifdef _DEBUG

        if (afxTraceFlags & traceDatabase)

            TRACE0("WAITING for datasource.\n");

#endif

    }

    else

    {

        if (m_dwWait == m_dwMinWaitForDataSource)

        {

            // 2nd call; wait max time; put up wait cursor

            m_dwWait = m_dwMaxWaitForDataSource;

            pApp->DoWaitCursor(1);      // BeginWaitCursor

        }

    }

    CWinThread* pThread = AfxGetThread();

    DWORD clockFirst = GetTickCount();

    while (GetTickCount() - clockFirst < m_dwWait)

    {

        MSG msg;

        if (::PeekMessage(&msg, NULL, NULL, NULL, PM_NOREMOVE))

        {

            TRY

            {

                pThread->PumpMessage();

            }

            CATCH_ALL

            {

                TRACE0("Error: exception in OnWaitForDataSource - continuing.\n");

                DELETE_EXCEPTION;

            }

            END_CATCH_ALL

        }

        else

            pThread->OnIdle(-1);

    }

}

CRecordset


The CRecordset object is used to manage recordsets. This object is often used with the CDatabase and CRecordView objects. The member functions in the CRecordset object offer a powerful set of database record manipulation tools.

The CRecordset object is derived from the CObject base class. Figure 2.3 shows the class hierarchy for the CRecordset class.

Figure 2.3. The CRecordset class hierarchy.

The CRecordset class object has a number of member functions. These functions are divided into the following seven categories:

The following sections take a closer look at the members of this class. I don't cover the members of the CObject class (which CRecordset is derived from) in this book. Refer to the Visual C++ documentation (either the manuals or the online help system) for full information about the CObject class.

Data Members

There are a number of data members in the CRecordset object:


Construction/Destruction

Three member functions deal directly with CRecordset construction: CRecordset(), Open(), and Close(). There also is the default destructor, which I won't document here because it's never called by an application. The following paragraphs describe each construction member function and, where applicable, give examples of usage.

The CRecordset() function is the constructor for the CRecordset class object. CRecordset() takes one parameter, and, because it's a constructor, it has no specified return value. CRecordset() has the following prototype:


void CRecordset(CDatabase * pDatabase = NULL);

The default operation is to create and initialize the CRecordset object. If pDatabase is specified, this CDatabase object will be used with the CRecordset object. If the pDatabase pointer is NULL, the constructor will create a default CDatabase member class.

If you create a derived class, the derived class must have its own constructor. Your constructor will then call the CRecordset::CRecordset() constructor, passing the appropriate parameter.

The Open() function is used to run a query that will return a recordset to the application. Open() takes three parameters and has no return value. Open() has the following prototype:


virtual BOOL Open(

    UINT nOpenType = snapshot,  // Either dynaset, snapshot, or forwardOnly

    LPCSTR lpszSql = NULL,      // NULL, table name, SELECT, or CALL statement

    DWORD dwOptions = none);    // None, appendOnly, or readOnly

The default operation for Open() is to open a datasource. Open() will throw a CDBException, CMemoryException, or CFileException if there are errors.

The Close() function is used to close the currently open recordset. If no recordset is open, this function simply returns. After calling Close(), it's possible to then re-call Open() to reopen the recordset, thereby reusing the CRecordset object. The Close() function takes no parameters and has no return value. Close() has the following prototype:


void Close();

The default operation for Close() is to close the recordset and the ODBC HSTMT that was associated with the recordset.

Recordset Attributes

Thirteen member functions deal directly with CRecordset attributes. These member functions are listed here:

IsDeleted()

With these member functions, applications can obtain information about the recordset.

The CanAppend() function is used to determine whether or not new records can be appended to the end of the recordset. Records are added by using the AddNew() function. CanAppend() takes no parameters and returns a nonzero value if the recordset can have records appended. CanAppend() has the following prototype:


BOOL CanAppend();

Typically, CanAppend() is called to enable or disable the user interface's record append commands and tools.

The CanRestart() function is used to determine whether the query can be restarted. CanRestart() takes no parameters and returns a nonzero value if the query can be restarted. CanRestart() has the following prototype:


BOOL CanRestart();

The CanRestart() function is usually called prior to calling the Requery() member function.

The CanScroll() function is used to determine whether the recordset allows scrolling. CanScroll() takes no parameters and returns a nonzero value if the recordset allows scrolling. CanScroll() has the following prototype:


BOOL CanScroll();


NOTE

Not all recordsets allow scrolling.


The CanTransact() function is used to determine whether the recordset supports transactions. CanTransact() takes no parameters and returns a nonzero value if transactions are supported. CanTransact() has the following prototype:


BOOL CanTransact();


NOTE

Many, but not all, ODBC drivers support transactions.


The CanUpdate() function is used to determine whether the recordset supports updating. Updating would typically fail if the underlying database were opened in read-only mode. CanUpdate() takes no parameters and returns a nonzero value if the recordset supports updating. CanUpdate() has the following prototype:


BOOL CanUpdate();

The most common reason that a recordset can't be updated when the ODBC driver supports updating is that it has been opened in read-only mode. Read-only mode offers faster access (there is no need to perform record locking) at the expense of being able to update the recordset.

The GetRecordCount() function is used to determine the number of records in the current recordset. GetRecordCount() takes no parameters and returns the number of records in the recordset—a –1 value if the number of records can't be determined and a zero value if there are no records in the recordset. GetRecordCount() has the following prototype:


long GetRecordCount();

The number of records in a recordset can be determined only if the application scrolls through the entire recordset. The count of records is maintained as a counter that is incremented with each forward read. The true total number of records is known only after the application has scrolled past the last record. Using MoveLast() won't affect the record counter.

The GetStatus() function is used to obtain status information about the current recordset. GetStatus() takes one parameter, a reference to the CRecordsetStatus structure, and has no return value. GetStatus() has the following prototype:


void GetStatus(CRecordsetStatus & rsStatus);

The members of the CRecordsetStatus class are shown in the following code fragment:


struct CRecordsetStatus

{

    long   m_lCurrentRecord;     // Zero-based index of current record

                                 // if the current record is known, or

                                 // AFX_CURRENT_RECORD_UNDEFINED if the

                                 // current record is undefined.

    BOOL   m_bRecordCountFinal;  // Nonzero if the total number of records

                                 // in the recordset has been determined.

};

The GetTableName() function is used to fetch the name of the recordset table. GetTableName() takes no parameters and returns a CString reference. GetTableName() has the following prototype:


CString & GetTableName();

The CString returned won't contain a name if the recordset was based on a join or if the recordset was created by a call to a stored procedure.

The GetSQL() function is used to return a CString reference that contains the current SQL statement. The SQL statement is the SELECT statement used to generate the recordset. GetSQL() takes no parameters and returns a CString reference. GetSQL() has the following prototype:


CString & GetSQL();

The returned SQL string usually will have been modified by the system to include any filtering (a WHERE clause) and sorting (an ORDER BY clause).

The IsOpen() function is used to determine whether the CRecordset Open() or Requery() functions have been called and whether the recordset has been closed. IsOpen() takes no parameters and returns a nonzero value if there has been a call to Open() or Requery() without an intervening call to Close(). IsOpen() has the following prototype:


BOOL IsOpen();

Your application should check the IsOpen() function prior to calling Open().

The IsBOF() function is used to check whether the current record is the first record in the dataset. IsBOF() takes no parameters and returns a nonzero value if the recordset is empty or if the application has scrolled to before the first record in the recordset. IsBOF() has the following prototype:


BOOL IsBOF();


CAUTION

The IsBOF() function should be called prior to scrolling backward in a recordset. Scrolling backward when there are no records in the recordset or when the current record pointer is before the first record in the recordset causes an error.


The IsEOF() function is used to determine whether the current record is the last record in the dataset. IsEOF() takes no parameters and returns a nonzero value if the recordset is empty or if the application has scrolled to after the last record in the recordset. IsEOF() has the following prototype:


BOOL IsEOF();


CAUTION

The IsEOF() function should be called prior to scrolling forward in a recordset. Scrolling forward when there are no records in the recordset or when the current record pointer is after the last record in the recordset causes an error.


The IsDeleted() function is used to determine whether the current record in the recordset has been deleted. IsDeleted() takes no parameters and returns a nonzero value if the current record has been marked as deleted. IsDeleted() has the following prototype:


BOOL IsDeleted();


CAUTION

It's considered an error to update or delete a record that has been marked as deleted.



Recordset Update Operations

Four member functions deal directly with CRecordset updating:

Update()

With these member functions, applications can add, delete, and edit records in the recordset.

The AddNew() function is used to prepare a new record to be added to the recordset. This record's contents must then be filled in by the application. After the new record's contents are filled in, Update() should be called to write the record. AddNew() takes no parameters and has no return value. AddNew() has the following prototype:


void AddNew();

The AddNew() function throws a CDBException or a CFileException if an error occurs (such as trying to add records to a dataset that is read-only). AddNew() can be used as part of a transaction if the dataset supports transactions.

The Delete() function is used to delete the current record from the recordset. After calling Delete(), you must explicitly scroll to another record. Delete() takes no parameters and has no return value. Delete() has the following prototype:


void Delete();

The Delete() function will throw a CDBException if an error occurs (such as trying to delete records in a dataset that is read-only). Delete() can be used as part of a transaction if the dataset supports transactions.

The Edit() function is used to prepare the current record for editing. The Edit() function will save the current record's current values. If you call Edit(), make changes, and then call Edit() a second time (without a call to Update()), the changes will be lost, and the record will be restored to the original values. Edit() takes no parameters and has no return value. Edit() has the following prototype:


void Edit();

The Edit() function throws a CDBException if an error occurs. Edit() can be used as part of a transaction if the dataset supports transactions.

The Update() function is used to write the record that has been added to or edited by other recordset update operations. Update() takes no parameters and returns a nonzero value if a record was actually updated or a zero if no records were updated. Update() has the following prototype:


BOOL Update();

The Update() function throws a CDBException if an error occurs. Update() can be used as part of a transaction if the dataset supports transactions.

Recordset Navigation Operations

Five member functions deal directly with CRecordset record navigation:

MovePrev()

You should also refer to the IsBOF() and IsEOF() functions, described in the section "Recordset Attributes." With these member functions, applications can move forward, backward, to a specific record, to the beginning of a recordset, and to the end of a recordset.

The Move() function is used to move to a specific record in the recordset, relative to the current record. This function allows random movement in the recordset. Move() takes one parameter and has no return value. Move() has the following prototype:


void Move(long lRows);

Use a negative parameter value to move backward from the current record. The Move() function throws a CDBException, CFileException, or CMemoryException if it fails.

WARNING

Don't call any move function for a recordset that doesn't have any records (if both IsEOF() and IsBOF() return nonzero, the recordset is empty).

The MoveFirst() function is used to move to the first record in the recordset. MoveFirst() takes no parameters and has no return value. MoveFirst() has the following prototype:


void MoveFirst();

The MoveFirst() function throws a CDBException, CFileException, or CMemoryException if it fails.

The MoveLast() function is used to move to the last record in the recordset. MoveLast() takes no parameters and has no return value. MoveLast() has the following prototype:


void MoveLast();

The MoveLast() function throws a CDBException, CFileException, or CMemoryException if it fails.

The MoveNext() function is used to move to the next record in the recordset. If you're positioned after the last record in the recordset, don't call MoveNext(). MoveNext() takes no parameters and has no return value. MoveNext() has the following prototype:


void MoveNext();

The MoveNext() function throws a CDBException, CFileException, or CMemoryException if it fails.

The MovePrev() function is used to move to the previous record in the recordset. If you're positioned before the first record in the recordset, don't call MovePrev(). MovePrev() takes no parameters and has no return value. MovePrev() has the following prototype:


void MovePrev();

The MovePrev() function throws a CDBException, CFileException, or CMemoryException if it fails.

Other Recordset Operations

Eight member functions deal directly with CRecordset operations:

SetLockingMode()

With these member functions, applications can perform miscellaneous operations on recordsets.

The Cancel() function is used to cancel a pending asynchronous operation. Cancel() takes no parameters and has no return value. Cancel() has the following prototype:


void Cancel();

The default operation, should there be no pending asynchronous operation, is simply to return.

The IsFieldDirty() function is used to determine whether a specified field has been changed. IsFieldDirty() takes one parameter—a pointer to a field data member—and returns a nonzero value if the field has, in fact, been modified. IsFieldDirty() has the following prototype:


BOOL IsFieldDirty(void * pField);

If the pField pointer parameter is NULL, all fields in the record are checked.

The IsFieldNull() function is used to determine whether a specified field is currently null (contains no value). IsFieldNull() takes one parameter—a pointer to a field data member—and returns a nonzero value if the field is, in fact, null. IsFieldNull() has the following prototype:


BOOL IsFieldNull(void * pField);

If the pField pointer parameter is NULL, all fields in the record are checked. Note that the C/C++ NULL is different from the SQL null.

The IsFieldNullable() function is used to determine whether a specified field can be set to null (containing no value). IsFieldNullable() takes one parameter—a pointer to a field data member—and returns a nonzero value if the field can be set to null. IsFieldNullable() has the following prototype:


BOOL IsFieldNullable(void * pField);

If the pField pointer parameter is NULL, all fields in the record are checked. Note that the C/C++ NULL is different from the SQL null.

The Requery() function is used to refresh the recordset. A call to the function CanRestart() should be made prior to calling Requery(). Requery() takes no parameters and returns a nonzero value if the refresh was successful. Requery() has the following prototype:


BOOL Requery();

The Requery() function throws a CDBException, CFileException, or CMemoryException if it fails.

The SetFieldDirty() function is used to modify the dirty flag for a specified field. SetFieldDirty() takes two parameters—a pointer to a field data member and a Boolean value specifying the new value for the dirty flag. SetFieldDirty() has no return value and has the following prototype:


void SetFieldDirty(void * pField, BOOL bDirty = TRUE);

If the pField pointer parameter is NULL, all fields in the record are marked with the value of the bDirty parameter. Note that the C/C++ NULL is different from the SQL null.

The SetFieldNull() function is used to modify the null flag for a specified field. SetFieldNull() takes two parameters—a pointer to a field data member and a Boolean value specifying the new value for the dirty flag. SetFieldNull() has no return value and has the following prototype:


void SetFieldNull(void * pField, BOOL bDirty = TRUE);

If the pField pointer parameter is NULL, all fields in the record are marked with the value of the bDirty parameter. Note that the C/C++ NULL is different from the SQL null.

The SetLockingMode() function is used to change the record locking mode. SetLockingMode() takes one parameter—nMode, which must be either optimistic or pessimistic. SetLockingMode() has no return value and has the following prototype:


void SetLockingMode(UINT nMode);

The pessimistic mode is more cautious than optimistic mode. Both pessimistic and optimistic are defined in CRecordset. pessimistic mode locks the record as soon as Edit() is called, and optimistic mode locks the record only while the update is being performed.

Recordset Overridables

Applications may override five members to allow control over the recordset:

OnWaitForDataSource()

The DoFieldExchange() function is used to transfer data to and from the field variables and records in the recordset. If your application is built with AppWizard, a default DoFieldExchange() function will be created. Also, modifications to the AppWizard-created DoFieldExchange() will be done by ClassWizard. DoFieldExchange() takes one parameter and has no return value. DoFieldExchange() has the following prototype:


void DoFieldExchange(CFieldExchange * pFX);

The CFieldExchange class object definition is shown in the following code fragment. The actual definition can be found in the AFXDB.H header file.


// CFieldExchange - for field exchange

class CFieldExchange

{

// Attributes

public:

    enum RFX_Operation

    {

        BindParam, // Register user's parameters with ODBC SQLBindParameter

        RebindParam, // Migrate param values to proxy array before requery

        BindFieldToColumn, // Register user's fields with ODBC SQLBindCol

        BindFieldForUpdate, // Temporarily bind columns before

                            // update (via SQLSetPos)

        UnbindFieldForUpdate, // Unbind columns after update (via SQLSetPos)

        Fixup, // Set string lengths and clear status bits

        MarkForAddNew,

        MarkForUpdate,  // Prepare fields and flags for update operation

        Name, // Append dirty field name

        NameValue, // Append dirty name=value

        Value, // Append dirty value or parameter marker

        SetFieldDirty, // Set status bit for changed status

        SetFieldNull,   // Set status bit for null value

        IsFieldDirty, // Return TRUE if field is dirty

        IsFieldNull, // Return TRUE if field is marked NULL

        IsFieldNullable, // Return TRUE if field can hold NULL values

        StoreField, // Archive values of current record

        LoadField,  // Reload archived values into current record

        GetFieldInfoValue,  // General info on a field via pv for field

        GetFieldInfoOrdinal,  // General info on a field via field ordinal

#ifdef _DEBUG

        DumpField,  // Dump bound field name and value

#endif

    };

    UINT m_nOperation;  // Type of exchange operation

    CRecordset* m_prs;  // Recordset handle

// Operations

    enum FieldType

    {

        noFieldType,

        outputColumn,

        param,

    };

// Operations (for implementors of RFX procs)

    BOOL IsFieldType(UINT* pnField);

    // Indicate purpose of subsequent RFX calls

    void SetFieldType(UINT nFieldType);

// Implementation

    CFieldExchange(UINT nOperation, CRecordset* prs, void* pvField = NULL);

    void Default(LPCTSTR szName,

        void* pv, LONG* plLength, int nCType, UINT cbValue, UINT cbPrecision);

    int GetColumnType(int nColumn, UINT* pcbLength = NULL,

        int* pnScale = NULL, int* pnNullable = NULL);

    // Long binary helpers

    long GetLongBinarySize(int nField);

    void GetLongBinaryData(int nField, CLongBinary& lb, long* plSize);

    BYTE* ReallocLongBinary(CLongBinary& lb, long lSizeRequired,

        long lReallocSize);

    // Current type of field

    UINT m_nFieldType;

    // For GetFieldInfo

    CFieldInfo* m_pfi;  // GetFieldInfo return struct

    BOOL m_bFieldFound; // GetFieldInfo search successful

    // For returning status info for a field

    BOOL m_bNull;       // Return result of IsFieldNull(able)/Dirty operation

    BOOL m_bDirty;      // Return result of IsFieldNull(able)/Dirty operation

    CString* m_pstr;    // Field name or destination for building various SQL

                        // clauses

    BOOL m_bField;      // Value to set for SetField operation

    void* m_pvField;    // For indicating an operation on a specific field

    CArchive* m_par;    // For storing/loading copy buffer

    LPCTSTR m_lpszSeparator; // Append after field names

    UINT m_nFields;     // Count of fields for various operations

    UINT m_nParams;     // Count of fields for various operations

    UINT m_nParamFields;    // Count of fields for various operations

    HSTMT m_hstmt;      // For SQLBindParameter on update statement

    long m_lDefaultLBFetchSize;     // For fetching CLongBinary data of

                                    // unknown length

    long m_lDefaultLBReallocSize;   // For fetching CLongBinary data of

                                    // unknown length

#ifdef _DEBUG

    CDumpContext* m_pdcDump;

#endif //_DEBUG

};

A typical AppWizard-created DoFieldExchange() function is shown in the following code fragment. This example is from the sample program shown in Figure 2.1.


void CRecordViewSet::DoFieldExchange(CFieldExchange* pFX)

{

    //{{AFX_FIELD_MAP(CRecordViewSet)

    pFX->SetFieldType(CFieldExchange::outputColumn);

    RFX_Text(pFX, _T("[Customer ID]"), m_Customer_ID);

    RFX_Text(pFX, _T("[Company Name]"), m_Company_Name);

    RFX_Text(pFX, _T("[Contact Name]"), m_Contact_Name);

    RFX_Text(pFX, _T("[Contact Title]"), m_Contact_Title);

    RFX_Text(pFX, _T("[Address]"), m_Address);

    RFX_Text(pFX, _T("[City]"), m_City);

    RFX_Text(pFX, _T("[Region]"), m_Region);

    RFX_Text(pFX, _T("[Postal Code]"), m_Postal_Code);

    RFX_Text(pFX, _T("[Country]"), m_Country);

    RFX_Text(pFX, _T("[Phone]"), m_Phone);

    RFX_Text(pFX, _T("[Fax]"), m_Fax);

    //}}AFX_FIELD_MAP

}

The GetDefaultConnect() function is used to return the default SQL connect string. GetDefaultConnect() takes no parameters and returns a CString reference. GetDefaultConnect() has the following prototype:


CString & GetDefaultConnect();

The default GetDefaultConnect() function created by AppWizard is shown in the following code fragment. This example is from the sample program shown later in this chapter (see the section called "An AppWizard-Generated Program"). It causes ODBC to display an open database dialog box.


CString CRecordViewSet::GetDefaultConnect()

{

    return _T("ODBC;DSN=MS Access 7.0 Database");

}

The GetDefaultSQL() function is used to return the default SQL string used to select records from the datasource to be placed in the recordset. GetDefaultSQL() takes no parameters and returns a CString reference. GetDefaultSQL() has the following prototype:


CString & GetDefaultSQL();

The default GetDefaultSQL() function created by AppWizard is shown in the following code fragment. This example is from the sample program shown later in this chapter.


CString CRecordViewSet::GetDefaultSQL()

{

    return _T("[Customers]");

}

The OnSetOptions() function is used to set options for the specified HSTMT. OnSetOptions() takes no parameters and has no return value. OnSetOptions() has the following prototype:


void OnSetOptions(HSTMT hstmt);

An AppWizard-created application doesn't have a default OnSetOptions() function. If you need one, you must write it yourself.

The OnWaitForDataSource() function is used to allow the application to perhaps ask the user (or simply query a control) whether there is a need to cancel the current asynchronous operation. If the user really wants to cancel, your OnWaitForDataSource() function should call the Cancel() function to end the asynchronous operation. OnWaitForDataSource() takes one parameter—a Boolean value that will be nonzero if the datasource is still waiting for an asynchronous operation—and has no return value. OnWaitForDataSource() has the following prototype:


void OnWaitForDataSource(BOOL bStillWaiting);

An AppWizard-created application doesn't have a default OnWaitForDataSource() function. If you need one, you must write it yourself. ClassWizard will add the shell of the OnWaitForDataSource() handler for you, which you then can fill in as needed.

CRecordView


The CRecordView object is used to manage recordsets. This object is usually used with the CDatabase and CRecordset objects. The member functions in the CRecordView object offer a powerful set of database record manipulation tools.

The CRecordView object is derived from the CFormView base class. Figure 2.4 shows the class hierarchy for the CRecordView class.

Figure 2.4. The CRecordView class hierarchy.

The CRecordView class object has a number of member functions. These functions are divided into three categories:

The following sections take a closer look at the members of this class. This book doesn't cover the members of the other classes on which CRecordView is based. Refer to the Visual C++ documentation (either the manuals or the online help system) for full information about these classes.

Construction

There is one construction member function: CRecordView(). There also is the default destructor, which I won't document here because it's never called by an application. The following list describes the construction member function and gives an example of its use.

The CRecordView() function is used to initialize the CRecordView object. CRecordView() takes one parameter: an identifier for the dialog box template. Because CRecordView() is a constructor, it has no defined return value. CRecordView() has the following prototype(s):


CRecordView(LPCSTR lpTemplateName);

or


CRecordView(UINT nTemplateID);

The following code fragment shows the default override constructor provided by AppWizard when a database application is created. This example is from the sample program shown later in this chapter.


CRecordViewView::CRecordViewView()

    : CRecordView(CRecordViewView::IDD)

{

    //{{AFX_DATA_INIT(CRecordViewView)

    m_pSet = NULL;

    //}}AFX_DATA_INIT

    // TODO: add construction code here

}

Attributes

Three member functions deal directly with CRecordView attributes: OnGetRecordset(), IsOnFirstRecord(), and IsOnLastRecord(). With these member functions, applications can obtain information about the record view.

The OnGetRecordset() function is used to get the pointer to the default CRecordset object that is attached to this CRecordView. OnGetRecordset() takes no parameters and returns a CRecordset pointer. OnGetRecordset() has the following prototype:


CRecordset * OnGetRecordset();

The following code fragment shows the default OnGetRecordset() provided by AppWizard when a database application is created. In this example, m_pSet was initialized in the constructor. This example is from the sample program shown later in this chapter.


CRecordset* CRecordViewView::OnGetRecordset()

{

        return m_pSet;

}

The IsOnFirstRecord() function is used to tell the view that the current record is the first record. This is necessary to allow the user interface to enable/disable the interface for moving to previous records. IsOnFirstRecord() takes no parameters and returns a nonzero value when the current record is the first record. IsOnFirstRecord() has the following prototype:


BOOL IsOnFirstRecord();

An AppWizard-created application doesn't have a default IsOnFirstRecord() function. If you want to provide special processing in your IsOnFirstRecord() handler, you must write it yourself. ClassWizard won't create a shell IsOnFirstRecord() handler for you. The default IsOnFirstRecord() function is shown in the following code fragment:


BOOL CRecordView::IsOnFirstRecord()

{

        ASSERT_VALID(this);

        CRecordsetStatus status;

        OnGetRecordset()->GetStatus(status);

        return status.m_lCurrentRecord == 0;

}

The IsOnLastRecord() function is used to tell the view that the current record is the last record. This is necessary to allow the user interface to enable/disable the interface for moving to later records. IsOnLastRecord() takes no parameters and returns a nonzero value when the current record is the last record. If IsOnLastRecord() is unable to determine whether the current record is the last record, it returns zero. IsOnLastRecord() has the following prototype:


BOOL IsOnLastRecord();

An AppWizard-created application doesn't have a default IsOnLastRecord() function. If you want to provide special processing in your IsOnLastRecord() handler, you must write it yourself. ClassWizard won't create a shell IsOnLastRecord() handler for you. The default IsOnLastRecord() function is shown in the following code fragment:


BOOL CRecordView::IsOnLastRecord()

{

        ASSERT_VALID(this);

        CRecordset* pRecordset = OnGetRecordset();

        CRecordsetStatus status;

        pRecordset->GetStatus(status);

        if (!status.m_bRecordCountFinal)

                return FALSE;

        return ((status.m_lCurrentRecord+1 == pRecordset->GetRecordCount()));

}

Operations

One member function deals directly with CRecordView operations. With this member function, applications can control which actions take place when the current record pointer is changed.

The OnMove() function is used to let the programmer change the current record pointer. Despite the name of this function, it's not normally overridden by an application. OnMove() takes one parameter and returns a nonzero value if the record pointer was successfully moved and a zero value if the call failed. OnMove() has the following prototype:


BOOL OnMove(UINT nMoveCommand);

The nMoveCommand parameter must be one of the manifest values shown in Table 2.1.

Table 2.1. OnMove() nMoveCommand values.

Value Description
ID_RECORD_FIRST Moves to the first record in the recordset.
ID_RECORD_NEXT Moves to the next record in the recordset, provided that the current record isn't the last record in the recordset.
ID_RECORD_LAST Moves to the last record in the recordset.
ID_RECORD_PREV Moves the previous record in the recordset, provided that the current record isn't the first record in the recordset.

WARNING

Be careful not to call OnMove() on a recordset that has no records.

CFieldExchange


The CFieldExchange class is used to support the record field exchange used by the other database classes. The DoFieldExchange() function has a CFieldExchange pointer passed to it. The CFieldExchange class object is used to encapsulate the exchange of data between records in a recordset and variables in the application that hold the column data. The CFieldExchange object is a base class that isn't derived from any other MFC class. Figure 2.5 shows the class hierarchy for the CFieldExchange class.

Figure 2.5. The CFieldExchange class hierarchy.

The CFieldExchange class object has two public member functions: IsFieldType() and SetFieldType(). These functions aren't divided into categories.

The IsFieldType() function is used to determine if the current operation (transfer) can be performed on the current field. IsFieldType() takes one parameter—pnField, a pointer to an index to the field—and returns a nonzero value if the operation can be performed. IsFieldType() has the following prototype:


BOOL IsFieldType(UINT * pnField);

The IsFieldType() function is useful when you write your own RFX functions. An example of an RFX function is shown in the following code fragment. This code is from the DBRFX.CPP file. The call to IsFieldType() appears in bold.


void AFXAPI RFX_Int(CFieldExchange* pFX, LPCTSTR szName, int& value)

{

    ASSERT(AfxIsValidAddress(pFX, sizeof(CFieldExchange)));

    ASSERT(AfxIsValidString(szName));

    UINT nField;

    if (!pFX->IsFieldType(&nField))

        return;

    LONG* plLength = pFX->m_prs->GetFieldLength(pFX);

    switch (pFX->m_nOperation)

    {

    case CFieldExchange::BindFieldToColumn:

        {

#ifdef _DEBUG

            int nSqlType = pFX->GetColumnType(nField);

            if (nSqlType != SQL_C_SHORT)

            {

                // Warn of possible field schema mismatch

                if (afxTraceFlags & traceDatabase)

                    TRACE1("Warning: int converted from SQL type %ld.\n",

                        nSqlType);

            }

#endif

        }

        // fall through

    default:

LDefault:

        pFX->Default(szName, &value, plLength, SQL_C_LONG,

            sizeof(value), 5);

        return;

    case CFieldExchange::Fixup:

        if (*plLength == SQL_NULL_DATA)

        {

            pFX->m_prs->SetFieldFlags(nField,

                AFX_SQL_FIELD_FLAG_NULL, pFX->m_nFieldType);

            value = AFX_RFX_INT_PSEUDO_NULL;

        }

        return;

    case CFieldExchange::SetFieldNull:

        if ((pFX->m_pvField == NULL &&

            pFX->m_nFieldType == CFieldExchange::outputColumn) ||

            pFX->m_pvField == &value)

        {

            if (pFX->m_bField)

            {

                // Mark fields null

                pFX->m_prs->SetFieldFlags(nField,

                    AFX_SQL_FIELD_FLAG_NULL, pFX->m_nFieldType);

                value = AFX_RFX_INT_PSEUDO_NULL;

                *plLength = SQL_NULL_DATA;

            }

            else

            {

                pFX->m_prs->ClearFieldFlags(nField,

                    AFX_SQL_FIELD_FLAG_NULL, pFX->m_nFieldType);

                *plLength = sizeof(value);

            }

#ifdef _DEBUG

            pFX->m_bFieldFound = TRUE;

#endif

        }

        return;

    case CFieldExchange::MarkForAddNew:

        // Can force writing of psuedo-null value (as a non-null) by

        // setting field dirty

        if (!pFX->m_prs->IsFieldFlagDirty(nField, pFX->m_nFieldType))

        {

            if (value != AFX_RFX_INT_PSEUDO_NULL)

            {

                pFX->m_prs->SetFieldFlags(nField,

                    AFX_SQL_FIELD_FLAG_DIRTY, pFX->m_nFieldType);

                pFX->m_prs->ClearFieldFlags(nField,

                    AFX_SQL_FIELD_FLAG_NULL, pFX->m_nFieldType);

            }

        }

        return;

    case CFieldExchange::MarkForUpdate:

        if (value != AFX_RFX_INT_PSEUDO_NULL)

            pFX->m_prs->ClearFieldFlags(nField,

                AFX_SQL_FIELD_FLAG_NULL, pFX->m_nFieldType);

        goto LDefault;

    case CFieldExchange::GetFieldInfoValue:

        if (pFX->m_pfi->pv == &value)

        {

            pFX->m_pfi->nField = nField-1;

            goto LFieldFound;

        }

        return;

    case CFieldExchange::GetFieldInfoOrdinal:

        if (nField-1 == pFX->m_pfi->nField)

        {

LFieldFound:

            pFX->m_pfi->nDataType = AFX_RFX_INT;

            pFX->m_pfi->strName = szName;

            pFX->m_pfi->pv = &value;

            pFX->m_pfi->dwSize = sizeof(value);

            // Make sure field found only once

            ASSERT(pFX->m_bFieldFound == FALSE);

            pFX->m_bFieldFound = TRUE;

        }

        return;

#ifdef _DEBUG

    case CFieldExchange::DumpField:

        *pFX->m_pdcDump << "\n" << szName << " = " << value;

        return;

#endif //_DEBUG

    }

}

The SetFieldType() function is used to set the field types prior to calls to the RFX functions. SetFieldType() takes one parameter—nFieldType, an enum that is declared in CFieldExchange (see Table 2.2). SetFieldType() has no return value and has the following prototype:


void SetFieldType(UINT nFieldType);

The valid nFieldTypes are listed in Table 2.2.

Table 2.2. Valid nFieldType enum values.

enum Description
CFieldExchange::outputColumn The field is an output column.
CFieldExchange::param The field is a parameter.

For example, the default DoFieldExchange() function that AppWizard includes in a database application calls SetFieldType() as its first function call, with a parameter of CFieldExchange::outputColumn:


void CRecordViewSet::DoFieldExchange(CFieldExchange* pFX)

{

    //{{AFX_FIELD_MAP(CRecordViewSet)

    pFX->SetFieldType(CFieldExchange::outputColumn);

    RFX_Text(pFX, _T("[Customer ID]"), m_Customer_ID);

    RFX_Text(pFX, _T("[Company Name]"), m_Company_Name);

    RFX_Text(pFX, _T("[Contact Name]"), m_Contact_Name);

    RFX_Text(pFX, _T("[Contact Title]"), m_Contact_Title);

    RFX_Text(pFX, _T("[Address]"), m_Address);

    RFX_Text(pFX, _T("[City]"), m_City);

    RFX_Text(pFX, _T("[Region]"), m_Region);

    RFX_Text(pFX, _T("[Postal Code]"), m_Postal_Code);

    RFX_Text(pFX, _T("[Country]"), m_Country);

    RFX_Text(pFX, _T("[Phone]"), m_Phone);

    RFX_Text(pFX, _T("[Fax]"), m_Fax);

    //}}AFX_FIELD_MAP

}

CDBException


The CDBException class is used to handle error conditions that occur when a number of the database class's member functions encounter problems. The CDBException class object is used to encapsulate the error condition. The CDBException object is derived from the CException class, which in turn is derived from the CObject class. Figure 2.6 shows the class hierarchy for the CDBException class.

Figure 2.6. The CDBException class hierarchy.

The CDBException class object has three public member functions, which are all in the data members category.

The following sections take a closer look at the members of this class. This book doesn't cover the members of the other classes on which CDBException is based. You should refer to the Visual C++ documentation (either the manuals or the online help system) for full information about these classes.

Data Members

There are three data members in the CDBException object class: m_nRetCode, m_strError, and m_strStateNativeOrigin.

The m_nRetCode member variable is used to hold the return code that has the error code. Valid return codes are shown in Table 2.3. The file \MSDEV\MFC\include\AFXDB.RC documents these values using string resource definitions.

Table 2.3. m_nRetCode values.

Identifier Description
All Versions of MFC
AFX_SQL_ERROR_API_CONFORMANCE A CDatabase::Open() call was made, and the driver doesn't conform to the required ODBC API conformance level.
AFX_SQL_ERROR_CONNECT_FAIL The datasource connection failed. A NULL CDatabase pointer was passed to the CRecordset constructor, and a subsequent attempt to create a connection based on a call to GetDefaultConnect() failed.
AFX_SQL_ERROR_DATA_TRUNCATED More data was requested than would fit in the storage you provided. See the nMaxLength argument for the RFX_Text() and RFX_Binary() functions for information on expanding the space available.
AFX_SQL_ERROR_DYNASET_NOT_SUPPORTED The call to CRecordset::Open() that requested a dynaset failed. This was due to the fact that dynasets weren't supported by this ODBC driver.
AFX_SQL_ERROR_EMPTY_COLUMN_LIST An attempt was made to open a table. However, no columns were identified in record field exchange (RFX) function calls in your DoFieldExchange() function.
AFX_SQL_ERROR_FIELD_SCHEMA_MISMATCH Your call to an RFX function in your DoFieldExchange() function wasn't compatible with the column data type in the recordset.
AFX_SQL_ERROR_ILLEGAL_MODE A call was made to CRecordset::Update() without having previously called CRecordset::AddNew() or CRecordset::Edit().
AFX_SQL_ERROR_LOCK_MODE_NOT_SUPPORTED A request to lock records for update couldn't be fulfilled because the ODBC driver being used doesn't support locking.
AFX_SQL_ERROR_MULTIPLE_ROWS_AFFECTED A call was made to CRecordset::Update() or CRecordset::Delete() for a table with no unique key, and multiple records were changed.
AFX_SQL_ERROR_NO_CURRENT_RECORD Your application has attempted to edit or delete a previously deleted record. The application must scroll to a different (nondeleted) record after deleting the current record.
AFX_SQL_ERROR_NO_POSITIONED_UPDATES The application's request for a dynaset couldn't be fulfilled because the ODBC driver doesn't support positioned updates.
AFX_SQL_ERROR_NO_ROWS_AFFECTED A call was made to CRecordset::Update() or CRecordset::Delete(), but when the operation began, the record couldn't be found anymore.
AFX_SQL_ERROR_ODBC_LOAD_FAILED The attempt to load ODBC.DLL failed. Windows couldn't find or couldn't load the ODBC.DLL. This error is fatal, and your program must end.
AFX_SQL_ERROR_ODBC_V2_REQUIRED The application's request for a dynaset couldn't be fulfilled because a Level 2-compliant ODBC driver is required, and the current ODBC driver isn't Level 2-compliant.
AFX_SQL_ERROR_RECORDSET_FORWARD_ONLY The attempt to scroll was unsuccessful because the datasource doesn't support backward scrolling.
AFX_SQL_ERROR_SNAPSHOT_NOT_SUPPORTED The application made a call to CRecordset::Open() requesting a snapshot, but the call failed. Snapshots aren't supported by the driver. This will occur only when the ODBC cursor library, ODBCCURS.DLL, can't be found.
AFX_SQL_ERROR_SQL_CONFORMANCE A call to CDatabase::Open() was made, and the driver doesn't conform to the required minimum ODBC SQL conformance level.
AFX_SQL_ERROR_SQL_NO_TOTAL It wasn't possible to specify the total size of a CLongBinary data value. This most likely happened because a global memory block couldn't be preallocated.
AFX_SQL_ERROR_RECORDSET_READONLY An attempt was made to update a recordset that was opened in read-only mode, or the datasource is read-only.
SQL_ERROR The function failed. The error message returned by ::SQLError() is stored in the m_strError data member.
SQL_INVALID_HANDLE A handle (either environment, connection, or statement) was invalid. This was caused by a programmer error. This error isn't reported in MFC version 4.
MFC Version 4 Only
AFX_SQL_ERROR_INCORRECT_ODBC An incorrect version of ODBC was reported.
AFX_SQL_ERROR_DYNAMIC_CURSOR_NOT_SUPPORT This ODBC driver doesn't support dynamic cursors.
AFX_SQL_ERROR_NO_DATA_FOUND The application attempted to move before the first record, after the last record.
AFX_SQL_ERROR_ROW_FETCH There was an attempt to fetch a row from the server during an Open or Requery operation.
AFX_SQL_ERROR_ROW_UPDATE_NOT_SUPPORTED The ODBC driver doesn't support dynasets.
AFX_SQL_ERROR_UPDATE_DELETE_FAILED A call to SQLSetPos() returned SQL_SUCCESS_WITH_INFO explaining why the function call to CRecordset::ExecuteSetPosUpdate() failed.

When you're writing an application, it's important that all error trapping be implemented. It's unacceptable for an application to fail because of an unhandled exception condition.

The m_strError member variable is used to hold a string that contains the text of the error message. The string is in the format State %s, Native %ld, Origin %s. The State value is a five-character string containing the SQL error code. The Native error code is specific to the datasource. The Origin string is error message text returned by the ODBC component generating the error condition.

The m_strStateNativeOrigin member variable contains the error condition formatted as State %s, Native %ld, Origin %s. The State value is a five-character string containing the SQL error code. The Native error code is specific to the datasource. The Origin string is error message text returned by the ODBC component generating the error condition.

CLongBinary


The CLongBinary class is used to hold large binary objects contained in databases. These objects are often referred to as BLOBs (binary large objects). Typical BLOBs are bitmap images, audio or video tracks, and specialized binary data. The CLongBinary class object is used to encapsulate the error condition. The CLongBinary object is derived from the CObject class. Figure 2.7 shows the class hierarchy for the CLongBinary class.

Figure 2.7. The CLongBinary class hierarchy.

The CLongBinary class object has three public member functions that can be divided into two categories:

The following sections take a closer look at the members of this class. This book doesn't cover the members of the other classes on which CLongBinary is based. Refer to the Visual C++ documentation (either the manuals or the online help system) for full information about these classes.

Data Members

There are two data members in the CLongBinary object class: m_dwDataLength and m_hData.


Construction Member

The CLongBinary object class has a single constructor.

The CLongBinary() function is used to construct the CLongBinary object. CLongBinary() takes no parameters. Because it's a constructor, no return value is specified. CLongBinary() has the following prototype:


CLongBinary();

The CLongBinary class is used with the RFX_LongBinary() field exchange function.

RFX Functions


RFX functions transfer data to and from the application's variables to a record's column. These functions are placed in a DoFieldExchange() function. The following code fragment shows an example of a DoFieldExchange() function.


void CRecordViewSet::DoFieldExchange(CFieldExchange* pFX)

{

    //{{AFX_FIELD_MAP(CRecordViewSet)

    pFX->SetFieldType(CFieldExchange::outputColumn);

    RFX_Text(pFX, _T("[Customer ID]"), m_Customer_ID);

    RFX_Text(pFX, _T("[Company Name]"), m_Company_Name);

    RFX_Text(pFX, _T("[Contact Name]"), m_Contact_Name);

    RFX_Text(pFX, _T("[Contact Title]"), m_Contact_Title);

    RFX_Text(pFX, _T("[Address]"), m_Address);

    RFX_Text(pFX, _T("[City]"), m_City);

    RFX_Text(pFX, _T("[Region]"), m_Region);

    RFX_Text(pFX, _T("[Postal Code]"), m_Postal_Code);

    RFX_Text(pFX, _T("[Country]"), m_Country);

    RFX_Text(pFX, _T("[Phone]"), m_Phone);

    RFX_Text(pFX, _T("[Fax]"), m_Fax);

    //}}AFX_FIELD_MAP

}

Each of the RFX_...() functions allows transfer of a different type of data to and from the record's columns. Table 2.4 lists each RFX_...() function and describes its data types.

Table 2.4. The RFX_...() functions.

Function Data Type Description
RFX_Bool() BOOL Transfers a Boolean (TRUE/FALSE) value.
RFX_Byte() BYTE Transfers a byte (unsigned character) value.
RFX_Binary() CByteArray Transfers an array of byte values to the specified CByteArray object.
RFX_Double() double Transfers a floating-point (double) value.
RFX_Single() float Transfers a floating-point (float) value.
RFX_Int() int Transfers an integer (unsigned) value.
RFX_Long() long Transfers a long integer (unsigned long) value.
RFX_LongBinary() CLongBinary Transfers an array of byte values to the specified CLongBinary object.
RFX_Text() CString Transfers a character string (CString) value.
RFX_Date() CTime Transfers a time value to a CTime object.

If there is no RFX...() function to transfer the type of data you need, you can create your own RFX...() functions. You can use the existing RFX...() functions in the DBRFX.CPP file as starting points.

An AppWizard-Generated Program


This chapter's example is an AppWizard (Visual C++ 4) database program that uses ODBC. The final functionality (the dialog box controls for the main window, as well as connections between the controls in the dialog box and the program's variables) was done with the Visual C++ 4 IDE and ClassWizard. The actual time it took to develop this application was only a few minutes.



NOTE

Windows 95, Windows NT, and Visual C++ 4 all now support long filenames. This lets the names of the class implementation files have meaningful names. You should give your projects meaningful names because you're no longer limited to project names that had to be only a few characters to fit into DOS's 8.3 filename structure.


This program uses the CDatabase class, the CRecordset class, and the CRecordView class. This final part of the chapter takes a look at the files that support the CRecordView class (the Record ViewView.cpp file) and the CRecordset class (the Record ViewSet.cpp file).

CRecordView Support

The CRecordView class is supported in the RecordView View.cpp file. The minimal support is sufficient to create a working application that can be easily turned into a working record browser with editing capabilities. The default implementation of the AppWizard-produced program doesn't support adding records to the recordset, but you can add this functionality easily.

First, the RecordView View.cpp file, shown in Listing 2.1, contains the constructor and destructor for our CRecordView object, which is called CRecordViewView.

The next function is DoDataExchange(), which transfers the fields in the recordset to the application's data variables. This is the first time I've mentioned the DoDataExchange() function. Its purpose is to transfer data to and from the application's variables to the main window's dialog box controls.



NOTE

Don't confuse the DoDataExchange() function, which transfers data between an application's variables and dialog box controls, with DoFieldExchange(), which transfers data from the same variables and the current record in the recordset.


The Record ViewView.cpp file also contains functions to assist the programmer in implementing printer support (including print preview) and diagnostic support.

Listing 2.1. The CRecordView handler: Record ViewView.cpp.


// Record ViewView.cpp : implementation of the CRecordViewView class

//

#include "stdafx.h"

#include "Record View.h"

#include "Record ViewSet.h"

#include "Record ViewDoc.h"

#include "Record ViewView.h"

#ifdef _DEBUG

#define new DEBUG_NEW

#undef THIS_FILE

static char THIS_FILE[] = __FILE__;

#endif

/////////////////////////////////////////////////////////////////////////////

// CRecordViewView

IMPLEMENT_DYNCREATE(CRecordViewView, CRecordView)

BEGIN_MESSAGE_MAP(CRecordViewView, CRecordView)

    //{{AFX_MSG_MAP(CRecordViewView)

        // NOTE - the ClassWizard will add and remove mapping macros here.

        //    DO NOT EDIT what you see in these blocks of generated code!

    //}}AFX_MSG_MAP

    // Standard printing commands

    ON_COMMAND(ID_FILE_PRINT, CRecordView::OnFilePrint)

    ON_COMMAND(ID_FILE_PRINT_DIRECT, CRecordView::OnFilePrint)

    ON_COMMAND(ID_FILE_PRINT_PREVIEW, CRecordView::OnFilePrintPreview)

END_MESSAGE_MAP()

/////////////////////////////////////////////////////////////////////////////

// CRecordViewView construction/destruction

CRecordViewView::CRecordViewView()

    : CRecordView(CRecordViewView::IDD)

{

    //{{AFX_DATA_INIT(CRecordViewView)

    m_pSet = NULL;

    //}}AFX_DATA_INIT

    // TODO: add construction code here

}

CRecordViewView::~CRecordViewView()

{

}

void CRecordViewView::DoDataExchange(CDataExchange* pDX)

{

    CRecordView::DoDataExchange(pDX);

    //{{AFX_DATA_MAP(CRecordViewView)

    DDX_FieldText(pDX, IDC_ADDRESS, m_pSet->m_Address, m_pSet);

    DDX_FieldText(pDX, IDC_CITY, m_pSet->m_City, m_pSet);

    DDX_FieldText(pDX, IDC_COMPANY_NAME, m_pSet->m_Company_Name, m_pSet);

    DDX_FieldText(pDX, IDC_CUSTOMER_ID, m_pSet->m_Customer_ID, m_pSet);

    DDX_FieldText(pDX, IDC_FAX, m_pSet->m_Fax, m_pSet);

    DDX_FieldText(pDX, IDC_PHONE, m_pSet->m_Phone, m_pSet);

    DDX_FieldText(pDX, IDC_POSTAL_CODE, m_pSet->m_Postal_Code, m_pSet);

    DDX_FieldText(pDX, IDC_REGION, m_pSet->m_Region, m_pSet);

    //}}AFX_DATA_MAP

}

BOOL CRecordViewView::PreCreateWindow(CREATESTRUCT& cs)

{

    // TODO: Modify the Window class or styles here by modifying

    // the CREATESTRUCT cs

    return CRecordView::PreCreateWindow(cs);

}

void CRecordViewView::OnInitialUpdate()

{

    m_pSet = &GetDocument()->m_recordViewSet;

    CRecordView::OnInitialUpdate();

}

/////////////////////////////////////////////////////////////////////////////

// CRecordViewView printing

BOOL CRecordViewView::OnPreparePrinting(CPrintInfo* pInfo)

{

    // Default preparation

    return DoPreparePrinting(pInfo);

}

void CRecordViewView::OnBeginPrinting(CDC* /*pDC*/, CPrintInfo* /*pInfo*/)

{

    // TODO: add extra initialization before printing

}

void CRecordViewView::OnEndPrinting(CDC* /*pDC*/, CPrintInfo* /*pInfo*/)

{

    // TODO: add cleanup after printing

}

/////////////////////////////////////////////////////////////////////////////

// CRecordViewView diagnostics

#ifdef _DEBUG

void CRecordViewView::AssertValid() const

{

    CRecordView::AssertValid();

}

void CRecordViewView::Dump(CDumpContext& dc) const

{

    CRecordView::Dump(dc);

}

CRecordViewDoc* CRecordViewView::GetDocument() // Non-debug version is inline

{

    ASSERT(m_pDocument->IsKindOf(RUNTIME_CLASS(CRecordViewDoc)));

    return (CRecordViewDoc*)m_pDocument;

}

#endif //_DEBUG

/////////////////////////////////////////////////////////////////////////////

// CRecordViewView database support

CRecordset* CRecordViewView::OnGetRecordset()

{

    return m_pSet;

}

/////////////////////////////////////////////////////////////////////////////

// CRecordViewView message handlers

When you use ClassWizard to add message handlers, these functions will be added to the end of the RecordView View.cpp file.

CRecordset Support

The CRecordset class is supported in the Record ViewSet.cpp file. The minimal support is sufficient to create a working application that easily can be turned into a working record browser with edit capabilities.

First in the Record ViewSet.cpp file, shown in Listing 2.2, is the constructor for the CRecordset object. There is no default destructor, but you could provide one if it were needed.

After the constructor are the GetDefaultConnect() and GetDefaultSQL() functions.

The final function in the Record ViewSet.cpp file is the DoFieldExchange() function. This function manages the transfer of data to and from the application's variables and the recordset's current record.

Listing 2.2. The CRecordset support file Record ViewSet.cpp.


// Record ViewSet.cpp : implementation of the CRecordViewSet class

//

#include "stdafx.h"

#include "Record View.h"

#include "Record ViewSet.h"

#ifdef _DEBUG

#define new DEBUG_NEW

#undef THIS_FILE

static char THIS_FILE[] = __FILE__;

#endif

/////////////////////////////////////////////////////////////////////////////

// CRecordViewSet implementation

IMPLEMENT_DYNAMIC(CRecordViewSet, CRecordset)

CRecordViewSet::CRecordViewSet(CDatabase* pdb)

    : CRecordset(pdb)

{

    //{{AFX_FIELD_INIT(CRecordViewSet)

    m_Customer_ID = _T("");

    m_Company_Name = _T("");

    m_Contact_Name = _T("");

    m_Contact_Title = _T("");

    m_Address = _T("");

    m_City = _T("");

    m_Region = _T("");

    m_Postal_Code = _T("");

    m_Country = _T("");

    m_Phone = _T("");

    m_Fax = _T("");

    m_nFields = 11;

    //}}AFX_FIELD_INIT

    m_nDefaultType = snapshot;

}

CString CRecordViewSet::GetDefaultConnect()

{

    return _T("ODBC;DSN=MS Access 7.0 Database");

}

CString CRecordViewSet::GetDefaultSQL()

{

    return _T("[Customers]");

}

void CRecordViewSet::DoFieldExchange(CFieldExchange* pFX)

{

    //{{AFX_FIELD_MAP(CRecordViewSet)

    pFX->SetFieldType(CFieldExchange::outputColumn);

    RFX_Text(pFX, _T("[Customer ID]"), m_Customer_ID);

    RFX_Text(pFX, _T("[Company Name]"), m_Company_Name);

    RFX_Text(pFX, _T("[Contact Name]"), m_Contact_Name);

    RFX_Text(pFX, _T("[Contact Title]"), m_Contact_Title);

    RFX_Text(pFX, _T("[Address]"), m_Address);

    RFX_Text(pFX, _T("[City]"), m_City);

    RFX_Text(pFX, _T("[Region]"), m_Region);

    RFX_Text(pFX, _T("[Postal Code]"), m_Postal_Code);

    RFX_Text(pFX, _T("[Country]"), m_Country);

    RFX_Text(pFX, _T("[Phone]"), m_Phone);

    RFX_Text(pFX, _T("[Fax]"), m_Fax);

    //}}AFX_FIELD_MAP

}

/////////////////////////////////////////////////////////////////////////////

// CRecordViewSet diagnostics

#ifdef _DEBUG

void CRecordViewSet::AssertValid() const

{

    CRecordset::AssertValid();

}

void CRecordViewSet::Dump(CDumpContext& dc) const

{

    CRecordset::Dump(dc);

}

#endif //_DEBUG

}

Summary


You need a thorough understanding of the member objects that constitute Visual C++'s data access object class to develop commercial-quality Visual C++ database applications. This chapter began by showing each of the data access object classes, with a detailed explanation of the member functions of each data-related member object. The Record View sample application introduced you to the code that AppWizard creates when you use AppWizard to create a basic database application. This chapter also can serve as a reference for the functions and member variables of the data access classes.

Chapter 3, "Using Visual C++ Data Access Functions," completes Part I of this book by showing you how to use Visual C++'s native C database SQL...() functions. The SQL...() functions can be used both with the MFC data access classes or alone, without any of the data access classes. These functions are useful when your application must have greater control over the process of database access.

Previous Page Page Top TOC Next Page