4.3 Create a Simplified Data Interface

This section explains the first step in writing Phone Book: simplify the data interface with a new class. This process will require several steps:

1.Create interface and implementation files for class CPerson.

2.Create an interface file for class CDataBase.

3.Design class CDatabase.

4.Create an implementation file for class CDataBase.

5.Write the member functions of class CDataBase.

·To create data object files:

Copy the PERSON.H and PERSON.CPP files that you made when designing the data object to your working directory for the Phone Book program.

The simplified data interface object that you'll create uses these files to implement the database that it manages.

·To create an interface file for class CDataBase:

1.Create a file called DATABASE.H and add the following lines:

// database.h - Declares the interface for the CDataBase class.

//

#ifndef __DATABASE_H__

#define __DATABASE_H__

#include "person.h"

// String const for untitled database

extern const char szUntitled[];

The #define statements ensure that the code in DATABASE.H is not included more than once. The #include statement includes the PERSON.H file developed in Chapter 2. PERSON.H declares classes CPerson and
CPersonList
. These classes are used by class CDataBase. The string constant declaration refers to a variable used by CDataBase and defined in the VIEW.CPP file. You'll create that file in Chapter 5.

2.At the bottom of the DATABASE.H file add the line:

#endif // __DATABASE_H__

Always keep this line at the bottom of the file, after all other code.

·To design class CDataBase:

Class CDataBase encapsulates two CPersonList objects and manages their use. The interface to CDataBase provides member functions to aid in accessing the data as Phone Book must do.

Add the following class declaration for CDataBase to your DATABASE.H file after the lines added previously:

class CDataBase: public CObject

{

public:

// constructor

CDataBase::CDataBase()

{

m_pDataList = NULL;

m_pFindList = NULL;

m_szFileName = "";

m_szFileTitle = "";

}

// Create/Destroy CPersonLists

BOOL New();

void Terminate();

// File handling

BOOL DoOpen( const char* pszFileName );

BOOL DoSave( const char* pszFileName = NULL );

BOOL DoFind( const char* pszLastName = NULL );

// Person Handling

void AddPerson( CPerson* pNewPerson );

void ReplacePerson( CPerson* pOldPerson,

const CPerson& rNewPerson );

void DeletePerson( int nIndex );

CPerson* GetPerson( int nIndex );

// Database Attributes

int GetCount()

{

ASSERT_VALID( this );

if ( m_pFindList != NULL )

return m_pFindList -> GetCount();

if ( m_pDataList != NULL )

return m_pDataList -> GetCount();

return 0;

}

BOOL IsDirty()

{ ASSERT_VALID( this );

return ( m_pDataList != NULL ) ? m_pDataList ->

GetDirty() : FALSE; }

BOOL IsNamed()

{ ASSERT_VALID( this );

return m_szFileName != szUntitled; }

const char* GetName()

{ ASSERT_VALID( this );

return m_szFileName; }

CString GetTitle()

{ ASSERT_VALID( this );

return "Phone Book - " + m_szFileTitle; }

void SetTitle( const char* pszTitle )

{ ASSERT_VALID( this );

m_szFileTitle = pszTitle; }

BOOL IsPresent()

{ ASSERT_VALID( this );

return m_pDataList != NULL; }

protected:

CPersonList* m_pDataList;

CPersonList* m_pFindList;

CString m_szFileName;

CString m_szFileTitle;

private:

CPersonList* ReadDataBase( CFile* pFile );

BOOL WriteDataBase( CFile* pFile );

#ifdef _DEBUG

public:

void AssertValid() const;

#endif

};

·To create an implementation file for class CDataBase:

Create a file called DATABASE.CPP and add the following lines to it:

// DATABASE.CPP - Definitions for class CDataBase

//

#include "database.h"

#include <string.h>

#ifdef _DEBUG

#undef THIS_FILE

static char BASED_CODE THIS_FILE[] = __FILE__;

#endif

const char szUntitled[] = "Untitled";

The #include directives make the class declaration in DATABASE.H and the declarations in STRING.H available to the code in the new file. The #ifdef _DEBUG lines support diagnostic reporting if you build the program in debug mode. The “Untitled” string is defined as a constant. This is the string referred to earlier from DATABASE.H as an externally-defined variable. It's used for new databases that have not been named yet.

The member functions of CDataBase include a constructor and functions that map fairly directly to some of Phone Book's menu commands as well as utility functions designed to make it easier to use the database. The constructor and functions GetCount, IsDirty, IsNamed, GetName, and IsPresent are all defined inline in the CDataBase class declaration. You will add function definitions to your DATABASE.CPP file.

·To write the member functions of CDataBase:

1.Add the New member function.

// CDataBase::New

// Initializes the database.

//

BOOL CDataBase::New()

{

ASSERT_VALID( this );

// Clean up any old data.

Terminate();

m_pDataList = new CPersonList;

return ( m_pDataList != NULL );

}

New creates a new, empty database, to which the user can add persons. In the Windows program developed in the next two chapters, New supports the New command in the File menu.

Note the use of the ASSERT_VALID macro to test the assumption that there is a valid database object for which a new CPersonList data member can be created. For more information about the ASSERT_VALID macro, see “The AssertValid Member Function” on page 137.

2.Add the Terminate member function:

// CDataBase::Terminate

// Cleans up the database.

//

void CDataBase::Terminate()

{

ASSERT_VALID( this );

if ( m_pDataList != NULL )

m_pDataList -> DeleteAll();

delete m_pDataList;

delete m_pFindList;

m_pDataList = NULL;

m_pFindList = NULL;

m_szFileName = szUntitled;

m_szFileTitle = szUntitled;

}

Terminate cleans up when the user ends the program or opens a new database file while an old one is still open. In the Windows version of Phone Book,
Terminate
is used to support the Exit, New, and Open commands in the File menu.

3.Add the AddPerson member function:

// CDataBase::AddPerson

// Inserts a person in the appropriate position (alphabetically by

// last name) in the database.

//

void CDataBase::AddPerson( CPerson* pNewPerson )

{

ASSERT_VALID( this );

ASSERT_VALID( pNewPerson );

ASSERT( pNewPerson != NULL );

ASSERT( m_pDataList != NULL );

POSITION pos = m_pDataList -> GetHeadPosition();

while ( pos != NULL &&

_stricmp( ((CPerson*)m_pDataList -> GetAt(pos)) ->

GetLastName(),

pNewPerson -> GetLastName() ) <= 0 )

m_pDataList -> GetNext( pos );

if ( pos == NULL )

m_pDataList -> AddTail( pNewPerson );

else

m_pDataList -> InsertBefore( pos, pNewPerson );

m_pDataList -> SetDirty( TRUE );

}

AddPerson adds a given new person object to the database. In the Windows version of Phone Book, Add is used to support the Add command in the Person menu.

Again note the use of the ASSERT_VALID macro to test assumptions about the validity of the current database object and about the person object passed as an argument. Also note the use of the related ASSERT macro. During debugging, this macro asserts that the expression passed to it is TRUE; if not, the program halts with a diagnostic message that tells where the error occurred.

AddPerson calls several member functions inherited by class CPersonList from its base class, CObList, to search the list for the place to add.

4.Add the GetPerson member function:

// CDataBase::GetPerson

// Look up someone by index.

//

CPerson* CDataBase::GetPerson( int nIndex )

{

ASSERT_VALID( this );

ASSERT( m_pDataList != NULL );

if ( m_pFindList != NULL )

return (CPerson*)m_pFindList -> GetAt( m_pFindList ->

FindIndex( nIndex ) );

else

return (CPerson*)m_pDataList -> GetAt( m_pDataList ->

FindIndex( nIndex ) );

}

GetPerson retrieves a person from the database by index. In the Windows program, GetPerson is used to support several menu commands.

GetPerson calls inherited CObList member functions to find the specified index. The code in effect requests the database object's m_pFindList or its m_pDataList to use its GetAt member function to retrieve a CPerson object in a CPersonList object. The specified index into the list is converted to a pointer to the object at that index by a call to the list object's FindIndex member function.

5.Add the DeletePerson member function:

// CDatabase::DeletePerson

// Removes record of person from database.

//

void CDataBase::DeletePerson( int nIndex )

{

ASSERT_VALID( this );

ASSERT( m_pDataList != NULL );

POSITION el = m_pDataList -> FindIndex( nIndex );

delete m_pDataList -> GetAt( el );

m_pDataList -> RemoveAt( el );

m_pDataList -> SetDirty( TRUE ); }

DeletePerson deletes the person object at a specified index. In the Windows program, DeletePerson is used to support the Delete command in the Person menu.

The logic of this member function is similar to that of GetPerson above.

6.Add the ReplacePerson member function:

// CDatabase::ReplacePerson

// Replaces an object in the list with the new object.

//

void CDataBase::ReplacePerson( CPerson* pOldPerson, const CPerson& rNewPerson )

{

ASSERT_VALID( this );

ASSERT( pOldPerson != NULL );

ASSERT( m_pDataList != NULL );

// Using the overloaded operator= for CPerson

*pOldPerson = rNewPerson;

m_pDataList->SetDirty( TRUE );

}

ReplacePerson is a utility member function that replaces an existing person object in the database with a newly edited person object. In the Windows program, ReplacePerson is used to support the Edit command in the Person menu. The code takes advantage of the overloaded assignment operator supplied with the CPerson class.

7.Add the DoFind member function:

// CDataBase::DoFind

// Does a FindPerson call, or clears the find data.

//

BOOL CDataBase::DoFind( const char* pszLastName /* = NULL */ )

{

ASSERT_VALID( this );

ASSERT( m_pDataList != NULL );

if ( pszLastName == NULL )

{

delete m_pFindList;

m_pFindList = NULL;

return FALSE;

}

ASSERT( m_pFindList == NULL );

return ( ( m_pFindList = m_pDataList ->

FindPerson( pszLastName ) ) != NULL );

}

DoFind searches for all person objects in the database whose last name data members match a search string. DoFind calls the CPersonList member function FindPerson, which returns a CPersonList object containing pointers to all of the found person objects. In the Windows program, DoFind is used to support the Find command in the Person menu.

8.Add the DoOpen member function:

// CDataBase::DoOpen

// Reads a database from the given filename.

//

BOOL CDataBase::DoOpen( const char* pszFileName )

{

ASSERT_VALID( this );

ASSERT( pszFileName != NULL );

CFile file( pszFileName, CFile::modeRead );

// read the object data from file

CPersonList* pNewDataBase = ReadDataBase( &file );

file.Close();

// get rid of current data base if new one is OK

if ( pNewDataBase != NULL )

{

Terminate();

m_pDataList = pNewDataBase;

m_pDataList -> SetDirty( FALSE );

m_szFileName = pszFileName;

return TRUE;

}

else

return FALSE;

}

DoOpen takes a filename as its argument, opens the file of that name, and calls ReadDatabase (given later) to read its data into a CPersonList object for use as the current database. In the Windows program, DoOpen is used to support the Open command in the File menu.

Note that the existing database, if any, is only deleted after the new one is successfully opened.

9.Add the DoSave member function:

// CDataBase::DoSave

// Saves the database to the given file.

//

BOOL CDataBase::DoSave( const char* pszFileName /* = NULL */ )

{

ASSERT_VALID( this );

// if we were given a name store it in the object.

if ( pszFileName != NULL )

m_szFileName = pszFileName;

CFileStatus status;

int nAccess = CFile::modeWrite;

// GetStatus will return TRUE if file exists, or FALSE

// if it doesn't.

if ( !CFile::GetStatus( m_szFileName, status ) )

nAccess |= CFile::modeCreate;

CFile file( m_szFileName, nAccess );

// write the data base to a file

// mark it clean if write is successful

if ( WriteDataBase( &file ) )

{

m_pDataList -> SetDirty( FALSE );

file.Close();

return TRUE;

}

else

{

file.Close();

return FALSE;

}

}

DoSave calls WriteDatabase (given below) to serialize the current database to a disk file. In the Windows program, DoSave is used to support the Save command in the File menu.

10.Add the ReadDataBase member function (you can copy it from the DMTEST.CPP file in Chapter 2 if you like):

// CDataBase::ReadDataBase

// Serializes in the database.

//

CPersonList* CDataBase::ReadDataBase( CFile* pFile )

{

ASSERT_VALID( this );

CPersonList* pNewDataBase = NULL;

// Create an archive from pFile for reading.

CArchive archive( pFile, CArchive::load );

// Deserialize the new data base from the archive, or catch the

// exception.

TRY

{

archive >> pNewDataBase;

}

CATCH( CArchiveException, e )

{

#ifdef _DEBUG

e -> Dump( afxDump );

#endif

archive.Close();

// If we got part of the database, then delete it.

if ( pNewDataBase != NULL )

{

pNewDataBase -> DeleteAll();

delete pNewDataBase;

}

// We caught this exception, but we throw it again so our

// caller can also catch it.

THROW_LAST();

}

END_CATCH

// Exit here if no errors or exceptions.

archive.Close();

return pNewDataBase;

}

ReadDatabase serializes the current database to a disk file. In the Windows program, ReadDatabase is used along with DoOpen to support the Open command in the File menu.

11.Add the WriteDataBase member function (you can copy it from the DMTEST.CPP file in Chapter 2 if you like—but delete the second parameter, of type CPersonList):

// CDataBase::WriteDataBase

// Serializes out the data into the given file.

//

BOOL CDataBase::WriteDataBase( CFile* pFile )

{

ASSERT_VALID( this );

ASSERT( m_pDataList != NULL );

// Create an archive from theFile for writing

CArchive archive( pFile, CArchive::store );

// Archive out, or catch the exception.

TRY

{

archive << m_pDataList;

}

CATCH( CArchiveException, e )

{

#ifdef _DEBUG

e -> Dump( afxDump );

#endif

archive.Close();

// Throw this exception again for the benefit of our caller.

THROW_LAST();

}

END_CATCH

// Exit here if no errors or exceptions.

archive.Close();

return TRUE;

}

WriteDatabase deserializes a database from a disk file. In the Windows program, WriteDatabase is used along with DoSave to support the Save and Save As commands in the File menu.

If you copy WriteDatabase from Chapter 2, be sure to delete the second parameter, pDataBase. Now that WriteDatabase has become a member function of a CDataBase object, it has direct access to the CPersonList object stored in the m_pDataList member variable. The parameter is no longer needed.

When you delete the second parameter, change the name pDataBase throughout the member function to m_pDataList (it occurs once). Also add the following two lines as the first two statements in the member function:

ASSERT_VALID( this );

ASSERT( m_pDataList != NULL );

12.Add the AssertValid member function:

#ifdef _DEBUG

void CDataBase::AssertValid() const

{

if( m_pDataList != NULL )

{

ASSERT_VALID( m_pDataList );

if( m_pFindList != NULL )

ASSERT_VALID( m_pFindList );

}

else

ASSERT( m_pFindList == NULL );

}

#endif

This AssertValid member function overrides the AssertValid member function of class CObject, the base class of CDataBase. Note that the definition is bracketed by #ifdef _DEBUG and #endif directives. The member function is only compiled if the _DEBUG flag is defined for the build.

For more information about the purpose in overriding this member function, see “The AssertValid Member Function” on page 137.

Files DATABASE.H and DATABASE.CPP are now complete. You can check them against the files in Listings 1 and 2, given earlier.

To continue the tutorial, see “Add Dialog Boxes” on page 153 in the next chapter. For explanations of the CDataBase code just added, see the discussion below, “Discussion: Class CDataBase.”