|
|
Object | Property | Setting |
Static Text |
ID |
IDC_STATIC |
|
Caption | &Name: |
Edit Box | ID | IDC_ENAME |
Static Text | ID | IDC_STATIC |
|
Caption | &Age |
Edit Box | ID | IDC_EAGE |
Static Text | ID | IDC_STATIC |
|
Caption | Marital Status: |
Radio Button | ID | IDC_RSINGLE |
|
Caption | &Single |
|
Group | Checked |
Radio Button | ID | IDC_RMARRIED |
|
Caption | &Married |
Radio Button | ID | IDC_RDIVORCED |
|
Caption | &Divorced |
Radio Button | ID | IDC_RWIDOW |
|
Caption | &Widowed |
Check Box | ID | IDC_CBEMPLOYED |
|
Caption | &Employed |
Button | ID | IDC_BFIRST |
|
Caption | &First |
Button | ID | IDC_BPREV |
|
Caption | &Previous |
Button | ID | IDC_BNEXT |
|
Caption | Nex&t |
Button | ID | IDC_BLAST |
|
Caption | &Last |
Static Text | ID | IDC_SPOSITION |
|
Caption | Record 0 of 0 |
When you were developing dialog-style applications or windows, you attached variables to the controls on the window in the dialog class. However, with an SDI or MDI application, which class do you create the variables in? Because the UpdateData function is a member of the CWnd class, and the view class is descended from the CWnd class, although the document is not, then the view class is the most logical place to add the variables that you will attach to the controls you placed on the window.
To attach variables to the controls in your sample application, open the Class Wizard and add variables to the controls, specifying that the place to add them is the view class (in this case, CSerializeView). For the sample application, add the variables in Table 13.2 to the controls specified.
Object | Name | Category | Type |
IDC_CBEMPLOYED | m_bEmployed | Value | BOOL |
IDC_EAGE | m_iAge | Value | int |
IDC_ENAME | m_sName | Value | CString |
IDC_RSINGLE | m_iMaritalStatus | Value | int |
IDC_SPOSITION | m_sPosition | Value | CString |
If you examine the source code for the view class, you will notice that there is no OnDraw function. If you are using the CFormView ancestor class for your SDI or MDI application, you don't need to worry about the OnDraw function. Instead, you treat the view class very much as you would the dialog class in a dialog window or dialog-style application. The primary difference is that the data that you need to use to populate the controls on the window are not in the view class, but in the document class. As a result, you need to build the interaction between these two classes to pass the data for the controls back and forth.
When you create a form-based application, it is assumed that your application will hold multiple records in the form and that the user will be able to scroll through the records to make changes. The user will be able to add additional records or even remove records from the record set. The challenge at this point in building this application is how you represent this set of records, supporting all the necessary functionality.
One approach is to create a class that would encapsulate each record, and then hold these records in an array, much as you did with the drawing application that you created and enhanced over the past few days. This class would need to descend from the CObject class and would need to contain variables for all the control variables that you added to the view class, along with methods to read and write all of these variables. Along with adding the methods to set and read all of the variables, you need to make the class serializable by adding the Serialize function to the class, as well as the two macros that complete the serialization of the class.
As you may remember from Day 10, when you want to create a new class, you can select the project in the Class View tab of the workspace pane, right-click the mouse button, and select New Class from the context menu. This opens the New Class dialog.
In the New Class dialog, you specify the type of class, whether it's an MFC class, and generic class, or a form class. To create a class that can contain one record's data, you most likely want to create a generic class. You'll learn more about how to determine which of these types of classes to create on Day 16, "Creating Your Own Classes and Modules." The other things that you need to do are give your class a name and specify the base class from which it will be inherited.
For your sample application, because the form that you created has information about a person, you might want to call your class something like CPerson. To be able to hold your class in the object array, you need to give it CObject as the base class. Just like on Day 10, the New Class dialog will claim that it cannot find the header with the base class in it and that you need to add this. Well, it's already included, so you can ignore this message. (On Day 16, you'll learn when you need to pay attention to this message.)
Once you create your new class, you'll need to add the variables for holding the data elements that will be displayed on the screen for the user. Following good object-oriented design, these variables will all be declared as private variables, where they cannot be directly manipulated by other classes. The variable types should match the variable types of the variables that are attached to the window controls in the view class.
With the sample application you are creating, you need to add the variables in Table 13.3.
Name |
Type |
m_bEmployed | BOOL |
m_iAge | int |
m_sName | CString |
m_iMaritalStatus | int |
NOTE: An inline function is a short C++ function in which, when the application is being compiled, the function body is copied in place of the function call. As a result, when the compiled application is running, the function code is executed without having to make a context jump to the function and then jump back once the function has completed. This reduces the overhead in the running application, increasing the execution speed slightly, but also makes the resulting executable application slightly larger. The more places the inline function is called, the larger the application will eventually get. For more information on inline functions, consult Appendix A, "C++ Review."
Once you create your class, you need to provide a means for reading and writing to the variables in the class. One of the easiest ways to provide this functionality is to add inline functions to the class definition. You create a set of inline functions to set each of the variables and then make another set for retrieving the current value of each variable.
If you want to implement the Get and Set variable functions for your CPerson class in the sample application that you are building, edit the Person.h header file, adding the lines in Listing 13.4.
1: class CPerson : public CObject 2: { 3: public: 4: // Functions for setting the variables 5: void SetEmployed(BOOL bEmployed) { m_bEmployed = bEmployed;} 6: void SetMaritalStat(int iStat) { m_iMaritalStatus = iStat;} 7: void SetAge(int iAge) { m_iAge = iAge;} 8: void SetName(CString sName) { m_sName = sName;} 9: // Functions for getting the current settings of the variables 10: BOOL GetEmployed() { return m_bEmployed;} 11: int GetMaritalStatus() { return m_iMaritalStatus;} 12: int GetAge() {return m_iAge;} 13: CString GetName() {return m_sName;} 14: CPerson(); 15: virtual ~CPerson(); 16: 17: private: 18: BOOL m_bEmployed; 19: int m_iMaritalStatus; 20: int m_iAge; 21: CString m_sName; 22: };
After you have the methods for setting and retrieving the values of the variables in your custom class, you'll probably want to make sure that the variables are initialized when the class is first created. You can do this in the class constructor by setting each of the variables to a default value. For instance, in your sample application, you add the code in Listing 13.5 to the constructor of the CPerson class.
1: CPerson::CPerson() 2: { 3: // Initialize the class variables 4: m_iMaritalStatus = 0; 5: m_iAge = 0; 6: m_bEmployed = FALSE; 7: m_sName = ""; 8: }
After you have your custom class with all variables defined and initialized, you need to make the class serializable. Making your class serializable involves three steps. The first step is adding the Serialize function to the class. This function writes the variable values to, and reads them back from, the CArchive object using C++ streams. The other two steps consist of adding the DECLARE_SERIAL and IMPLEMENT_SERIAL macros. Once you add these elements, your custom class will be serializable and ready for your application.
To add the Serialize function to your custom class, add a member function through the Class View tab in the workspace pane. Specify the function type as void, the function declaration as Serialize(CArchive &ar), and the access as public and check the Virtual check box. This should add the Serialize function and place you in the editor, ready to flesh out the function code.
In the Serialize function, the first thing you want to do is to call the ancestor's Serialize function. When you call the ancestor's function first, any foundation information that has been saved is restored first, providing the necessary support for your class before the variables in your class are restored. Once you call the ancestor function, you need to determine whether you need to read or write the class variables. You can do this by calling CArchive's IsStoring method. This function returns TRUE if the archive is being written to and FALSE if it's being read from. If the IsStoring function returns TRUE, you can use C++ I/O streams to write all your class variables to the archive. If the function returns FALSE, you can use C++ streams to read from the archive. In both cases, you must be certain to order the variables in the same order for both reading and writing. If you need more information about C++ streams, see Appendix A.
An example of a typical Serialize function for your sample custom class is shown in Listing 13.6. Notice that the CPerson variables are in the same order when writing to and reading from the archive.
1: void CPerson::Serialize(CArchive &ar) 2: { 3: // Call the ancestor function 4: CObject::Serialize(ar); 5: 6: // Are we writing? 7: if (ar.IsStoring()) 8: // Write all of the variables, in order 9: ar << m_sName << m_iAge << m_iMaritalStatus << m_bEmployed; 10: else 11: // Read all of the variables, in order 12: ar >> m_sName >> m_iAge >> m_iMaritalStatus >> m_bEmployed; 13: }
Once you have the Serialize function in place, you need to add the macros to your custom class. The first macro, DECLARE_SERIAL, needs to go in the class header and is passed the class name as its only argument.
For example, to add the DECLARE_SERIAL macro to the custom CPerson class in your sample application, you add the macro just below the start of the class declaration, where it will receive the default access for the class. You specify the class name, CPerson, as the only argument to the macro, as in Listing 13.7.
NOTE: The default access permission for functions and variables in C++ classes is public. All functions and variables that are declared before the first access declaration are public by default. You could easily add all of the public class functions and variables in this area of the class declaration, but explicitly declaring the access permission for all functions and variables is better practice--because that way, there is little to no confusion about the visibility of any of the class functions or variables.
NOTE: Most C++ functions need a semicolon at the end of the line of code. The two serialization macros do not, due to the C preprocessor, which replaces each of the macros with all of the code before compiling the application. It doesn't hurt to place the semicolons there; they are simply ignored.
1: class CPerson : public CObject 2: { 3: DECLARE_SERIAL (CPerson) 4: public: 5: // Functions for setting the variables 6: void SetEmployed(BOOL bEmployed) { m_bEmployed = bEmployed;} 7: void SetMaritalStat(int iStat) { m_iMaritalStatus = iStat;} 8: void SetAge(int iAge) { m_iAge = iAge;} 9: void SetName(CString sName) { m_sName = sName;} 10: // Functions for getting the current settings of the variables 11: BOOL GetEmployed() { return m_bEmployed;} 12: int GetMaritalStatus() { return m_iMaritalStatus;} 13: int GetAge() {return m_iAge;} 14: CString GetName() {return m_sName;} 15: CPerson(); 16: virtual ~CPerson(); 17: 18: private: 19: BOOL m_bEmployed; 20: int m_iMaritalStatus; 21: int m_iAge; 22: CString m_sName; 23: };
NOTE: In practice, if you read a file that was written using a previous version of the Serialize function in your class, your application will raise an exception, which you can then catch using standard C++ exception-handling techniques. This allows you to add code to your application to recognize and convert files created with earlier versions of your application. For information on C++ exception handling, see Appendix A.
To complete the serialization of your custom class, you need to add the IMPLEMENT_ SERIAL macro to the class definition. The best place to add this macro is before the constructor definition in the CPP file containing the class source code. This macro takes three arguments: the custom class name, the base class name, and the version number. If you make any changes to the Serialize function, you should increment the version number argument to the IMPLEMENT_SERIAL macro. This version number indicates when a file was written using a previous version of the Serialize function and thus may not be readable by the current version of the application.
To add the IMPLEMENT_SERIAL macro to your sample application, add it into the Person.cpp file just before the CPerson class constructor. Pass CPerson as the first argument (the class name), CObject as the second argument (the base class), and 1 as the version number, as in Listing 13.8.
1: // Person.cpp: implementation of the CPerson class. 2: // 3: ////////////////////////////////////////////////////////////////////// 4: 5: #include "stdafx.h" 6: #include "Serialize.h" 7: #include "Person.h" 8: 9: #ifdef _DEBUG 10: #undef THIS_FILE 11: static char THIS_FILE[]=__FILE__; 12: #define new DEBUG_NEW 13: #endif 14: 15: IMPLEMENT_SERIAL (CPerson, CObject, 1) 16: ////////////////////////////////////////////////////////////////////// 17: // Construction/Destruction 18: ////////////////////////////////////////////////////////////////////// 19: 20: CPerson::CPerson() 21: { 22: // Initialize the class variables 23: m_iMaritalStatus = 0; 24: m_iAge = 0; 25: m_bEmployed = FALSE; 26: m_sName = ""; 27: }
When you build a form-based application, where the form on the window is the primary place for the user to interact with the application, there is an unstated assumption that your application will allow the user to work with a number of records. This means that you need to include support for holding and navigating these records. The support for holding the records can be as simple as adding an object array as a variable to the document class, as you did back on Day 10. This allows you to add additional record objects as needed. The navigation could be a number of functions for retrieving the first, last, next, or previous record objects. Finally, you need informational functionality so that you can determine what record in the set the user is currently editing.
To hold and support this functionality, the document class will probably need two variables, the object array and the current record number in the array. These two variables will provide the necessary support for holding and navigating the record set.
For your example, add the two variables for supporting the record set of CPerson objects as listed in Table 13.4. Specify private access for both variables.
Name |
Type |
m_iCurPosition | int |
m_oaPeople | CObArray |
The other thing that you need to do to the document class to provide support for the record objects is make sure that the document knows about and understands the record object that it will be holding. You do this by including the custom class header file before the header file for the document class is included in the document class source code file. Because the document class needs to trigger actions in the view class, it's a good idea to also include the header file for the view class in the document class.
To include these header files in your sample application, open the source-code file for the document class and add the two #include statements as shown in Listing 13.9.
1: // SerializeDoc.cpp : implementation of the CSerializeDoc class 2: // 3: 4: #include "stdafx.h" 5: #include "Serialize.h" 6: 7: #include "Person.h" 8: #include "SerializeDoc.h" 9: #include "SerializeView.h" 10: 11: #ifdef _DEBUG 12: #define new DEBUG_NEW 13: #undef THIS_FILE 14: static char THIS_FILE[] = __FILE__; 15: #endif 16: 17: ////////////////////////////////////////////////////////////////////// 18: // CSerializeDoc
Before you can navigate the record set, you need to be able to add new records to the object array. If you add a private function for adding new records, you can add new records to the set dynamically as new records are needed. Because new records should be presenting the user with blank or empty data fields, you don't need to set any of the record variables when adding a new record to the object array, so you can use the default constructor.
Following the same logic that you used to add new line records on Day 10, you should add a new person record to the object array in your document class in today's sample application. Once you add a new record, you can return a pointer to the new record so that the view class can directly update the variables in the record object.
Once the new record is added, you will want to set the current record position marker to the new record in the array. This way, the current record number can easily be determined by checking the position counter.
If there are any problems in creating the new person record object, let the user know that the application has run out of available memory and delete the allocated object, just as you did on Day 10.
To add this functionality to your sample application, add a new member function to the document class. Specify the type as a pointer to your custom class. If you named your custom class CPerson, the function type is CPerson*. This function needs no arguments. Give the function a name that reflects what it does, such as AddNewRecord. Specify the access for this function as private because it will only be accessed from other functions within the document class. You can edit the resulting function, adding the code in Listing 13.10.
1: CPerson * CSerializeDoc::AddNewRecord() 2: { 3: // Create a new CPerson object 4: CPerson *pPerson = new CPerson(); 5: try 6: { 7: // Add the new person to the object array 8: m_oaPeople.Add(pPerson); 9: // Mark the document as dirty 10: SetModifiedFlag(); 11: // Set the new position mark 12: m_iCurPosition = (m_oaPeople.GetSize() - 1); 13: } 14: // Did we run into a memory exception? 15: catch (CMemoryException* perr) 16: { 17: // Display a message for the user, giving them the 18: // bad news 19: AfxMessageBox("Out of memory", MB_ICONSTOP | MB_OK); 20: // Did we create a line object? 21: if (pPerson) 22: { 23: // Delete it 24: delete pPerson; 25: pPerson = NULL; 26: } 27: // Delete the exception object 28: perr->Delete(); 29: } 30: return pPerson; 31: }
To aid the user in navigating the record set, it's always helpful to provide a guide about where the user is in the record set. To provide this information, you need to be able to get the current record number and the total number of records from the document to display for the user.
The functions to provide this information are both fairly simple. For the total number of records in the object array, all you need to do is get the size of the array and return that to the caller.
For your sample application, add a new member function to the document class. Specify the function type as int, the function name as GetTotalRecords, and the access as public. Once you add the function, edit it using the code in Listing 13.11.
1: int CSerializeDoc::GetTotalRecords() 2: { 3: // Return the array count 4: return m_oaPeople.GetSize(); 5: }
Getting the current record number is almost just as simple. If you are maintaining a position counter in the document class, this variable contains the record number that the user is currently editing. As a result, all you need to do is return the value of this variable to the calling routine. Because the object array begins with position 0, you probably need to add 1 to the current position before returning to display for the user.
To add this function to your sample application, add another new member function to the document class. Specify the type as int, the function name as GetCurRecordNbr, and the access as public. Edit the function using the code in Listing 13.12.
1: int CSerializeDoc::GetCurRecordNbr() 2: { 3: // Return the current position 4: return (m_iCurPosition + 1); 5: }
To make your application really useful, you will need to provide the user with some way of navigating the record set. A base set of functionality for performing this navigation is a set of functions in the document class to get pointers to specific records in the record set. First is a function to get a pointer to the current record. Next are functions to get pointers to the first and last records in the set. Finally, you need functions to get the previous record in the set and the next record in the set. If the user is already editing the last record in the set and attempts to move to the next record, you can automatically add a new record to the set and provide the user with this new, blank record.
To add all this functionality, start with the function to return the current record. This function needs to check the value in the position marker to make sure that the current record is a valid array position. Once it has made sure that the current position is valid, the function can return a pointer to the current record in the array.
To add this function to your sample application, add a new member function to the document class. Specify the function type as CPerson* (a pointer to the custom class), the function name as GetCurRecord, and the access as public. Edit the function, adding the code in Listing 13.13.
1: CPerson* CSerializeDoc::GetCurRecord() 2: { 3: // Are we editing a valid record number? 4: if (m_iCurPosition >= 0) 5: // Yes, return the current record 6: return (CPerson*)m_oaPeople[m_iCurPosition]; 7: else 8: // No, return NULL 9: return NULL; 10: }
The next function you might want to tackle is the function to return the first record in the array. In this function, you need to first check to make sure that the array has records. If there are records in the array, set the current position marker to 0 and return a pointer to the first record in the array.
To add this function to your sample application, add a new member function to the document class. Specify the function type as CPerson* (a pointer to the custom class), the function name as GetFirstRecord, and the access as public. Edit the function, adding the code in Listing 13.14.
1: CPerson* CSerializeDoc::GetFirstRecord() 2: { 3: // Are there any records in the array? 4: if (m_oaPeople.GetSize() > 0) 5: { 6: // Yes, move to position 0 7: m_iCurPosition = 0; 8: // Return the record in position 0 9: return (CPerson*)m_oaPeople[0]; 10: } 11: else 12: // No records, return NULL 13: return NULL; 14: }
For the function to navigate to the next record in the set, you need to increment the current position marker and then check to see if you are past the end of the array. If you are not past the end of the array, you need to return a pointer to the current record in the array. If you are past the end of the array, you need to add a new record to the end of the array.
To add this function to your sample application, add a new member function to the document class. Specify the function type as CPerson* (a pointer to the custom class), the function name as GetNextRecord, and the access as public. Edit the function, adding the code in Listing 13.15.
1: CPerson * CSerializeDoc::GetNextRecord() 2: { 3: // After incrementing the position marker, are we 4: // past the end of the array? 5: if (++m_iCurPosition < m_oaPeople.GetSize()) 6: // No, return the record at the new current position 7: return (CPerson*)m_oaPeople[m_iCurPosition]; 8: else 9: // Yes, add a new record 10: return AddNewRecord(); 11: }
For the function to navigate to the previous record in the array, you need to make several checks. First, you need to verify that the array has records. If there are records in the array, you need to decrement the current position marker. If the marker is less than zero, you need to set the current position marker to equal zero, pointing at the first record in the array. Once you've made it through all of this, you can return a pointer to the current record in the array.
To add this function to your sample application, add a new member function to the document class. Specify the function type as CPerson* (a pointer to the custom class), the function name as GetPrevRecord, and the access as public. Edit the function, adding the code in Listing 13.16.
1: CPerson * CSerializeDoc::GetPrevRecord() 2: { 3: // Are there any records in the array? 4: if (m_oaPeople.GetSize() > 0) 5: { 6: // Once we decrement the current position, 7: // are we below position 0? 8: if (--m_iCurPosition < 0) 9: // If so, set the record to position 0 10: m_iCurPosition = 0; 11: // Return the record at the new current position 12: return (CPerson*)m_oaPeople[m_iCurPosition]; 13: } 14: else 15: // No records, return NULL 16: return NULL; 17: }
For the function that navigates to the last record in the array, you still need to check to make sure that there are records in the array. If the array does have records, you can get the current size of the array and set the current position marker to one less than the number of records in the array. This is actually the last record in the array because the first record in the array is record 0. Once you set the current position marker, you can return a pointer to the last record in the array.
To add this function to your sample application, add a new member function to the document class. Specify the function type as CPerson* (a pointer to the custom class), the function name as GetLastRecord, and the access as public. Edit the function, adding the code in Listing 13.17.
1: CPerson * CSerializeDoc::GetLastRecord() 2: { 3: // Are there any records in the array? 4: if (m_oaPeople.GetSize() > 0) 5: { 6: // Move to the last position in the array 7: m_iCurPosition = (m_oaPeople.GetSize() - 1); 8: // Return the record in this position 9: return (CPerson*)m_oaPeople[m_iCurPosition]; 10: } 11: else 12: // No records, return NULL 13: return NULL; 14: }
When filling in the Serialize functionality in the document class, there's little to do other than pass the CArchive object to the object array's Serialize function, just as you did on Day 10.
When reading data from the archive, the object array will query the CArchive object to determine what object type it needs to create and how many it needs to create. The object array will then create each object in the array and call its Serialize function, passing the CArchive object to each in turn. This enables the objects in the object array to read their own variable values from the CArchive object in the same order that they were written.
When writing data to the file archive, the object array will call each object's Serialize function in order, passing the CArchive object (just as when reading from the archive). This allows each object in the array to write its own variables into the archive as necessary.
For the sample application, edit the document class's Serialize function to pass the CArchive object to the object array's Serialize function, as in Listing 13.18.
1: void CSerializeDoc::Serialize(CArchive& ar) 2: { 3: // Pass the serialization on to the object array 4: m_oaPeople.Serialize(ar); 5: }
Now you need to add the code to clean up the document once the document is closed or a new document is opened. This consists of looping through all objects in the object array and deleting each and every one. Once all the objects are deleted, the object array can be reset when you call its RemoveAll function.
To implement this functionality in your sample application, add an event-handler function to the document class on the DeleteContents event message using the Class Wizard. When editing the function, add the code in Listing 13.19.
1: void CSerializeDoc::DeleteContents() 2: { 3: // TODO: Add your specialized code here and/or call the base class 4: 5: /////////////////////// 6: // MY CODE STARTS HERE 7: /////////////////////// 8: 9: // Get the number of lines in the object array 10: int liCount = m_oaPeople.GetSize(); 11: int liPos; 12: 13: // Are there any objects in the array? 14: if (liCount) 15: { 16: // Loop through the array, deleting each object 17: for (liPos = 0; liPos < liCount; liPos++) 18: delete m_oaPeople[liPos]; 19: // Reset the array 20: m_oaPeople.RemoveAll(); 21: } 22: 23: /////////////////////// 24: // MY CODE ENDS HERE 25: /////////////////////// 26: 27: CDocument::DeleteContents(); 28: }
NOTE: One thing to keep in mind when writing this code is that you need to cast the pointer to the view as a pointer of the class of your view object. The GetNextView function returns a pointer of type CView, so you will not be able to call any of your additions to the view class until you cast the pointer to your view class. Casting the pointer tells the compiler that the pointer is really a pointer to your view object class and thus does contain all the functions that you have added. If you don't cast the pointer, the compiler will assume that the view object does not contain any of the functions that you have added and will not allow you to compile your application.
When a new document is started, you need to present the user with an empty form, ready for new information. To make that empty record ready to accept new information, you need to add a new record into the object array, which is otherwise empty. This results in only one record in the object array. Once the new record is added to the array, you must modify the view to show that a new record exists; otherwise, the view will continue to display the last record edited from the previous record set (and the user will probably wonder why your application didn't start a new record set).
To implement this functionality, you will need to edit the OnNewDocument function in your document class. This function is already in the document class, so you do not need to add it through the Class Wizard. The first thing that you do in this function is add a new record to the object array. Once the new record is added, you need to get a pointer to the view object. You use the GetFirstViewPosition function to get the position of the view object. Using the position returned for the view object, you can use the GetNextView function to retrieve a pointer to the view object. Once you have a valid pointer, you can use it to call a function that you will create in the view class to tell the view to refresh the current record information being displayed in the form.
Locate the OnNewDocument function in the document class source code, and add the code in Listing 13.20. Before you will be able to compile your application, you will need to add the NewDataSet function to the view class.
1: BOOL CSerializeDoc::OnNewDocument() 2: { 3: if (!CDocument::OnNewDocument()) 4: return FALSE; 5: 6: // TODO: add reinitialization code here 7: // (SDI documents will reuse this document) 8: 9: /////////////////////// 10: // MY CODE STARTS HERE 11: /////////////////////// 12: 13: // If unable to add a new record, return FALSE 14: if (!AddNewRecord()) 15: return FALSE; 16: 17: // Get a pointer to the view 18: POSITION pos = GetFirstViewPosition(); 19: CSerializeView* pView = (CSerializeView*)GetNextView(pos); 20: // Tell the view that it's got a new data set 21: if (pView) 22: pView->NewDataSet(); 23: 24: /////////////////////// 25: // MY CODE ENDS HERE 26: /////////////////////// 27: 28: return TRUE; 29: }
When opening an existing data set, you don't need to add any new records, but you still need to let the view object know that it needs to refresh the record being displayed for the user. As a result, you can add the same code to the OnOpenDocument function as you added to the OnNewDocument, only leaving out the first part where you added a new record to the object array.
Add an event-handler function to the document class for the OnOpenDocument event using the Class Wizard. Once you add the function, edit it adding the code in Listing 13.21.
1: BOOL CSerializeDoc::OnOpenDocument(LPCTSTR lpszPathName) 2: { 3: if (!CDocument::OnOpenDocument(lpszPathName)) 4: return FALSE; 5: 6: // TODO: Add your specialized creation code here 7: 8: /////////////////////// 9: // MY CODE STARTS HERE 10: /////////////////////// 11: 12: // Get a pointer to the view 13: POSITION pos = GetFirstViewPosition(); 14: CSerializeView* pView = (CSerializeView*)GetNextView(pos); 15: // Tell the view that it's got a new data set 16: if (pView) 17: pView->NewDataSet(); 18: 19: /////////////////////// 20: // MY CODE ENDS HERE 21: /////////////////////// 22: 23: return TRUE; 24: }
Now that you've added support for the record set to your document class, you need to add the functionality into the view class to navigate, display, and update the records. When you first designed your view class, you placed a number of controls on the window for viewing and editing the various data elements in each record. You also included controls for navigating the record set. Now you need to attach functionality to those controls to perform the record navigation and to update the record with any data changes the user makes.
Because of the amount of direct interaction that the form will have with the record object--reading variable values from the record and writing new values to the record--it makes sense that you want to add a record pointer to the view class as a private variable. For your example, add a new member variable to the view class, specify the type as CPerson*, give it a name such as m_pCurPerson, and specify the access as private. Next, edit the view source code file and include the header file for the person class, as in Listing 13.22.
1: // SerializeView.cpp : implementation of the CSerializeView class 2: // 3: 4: #include "stdafx.h" 5: #include "Serialize.h" 6: 7: #include "Person.h" 8: #include "SerializeDoc.h" 9: #include "SerializeView.h" 10: 11: #ifdef _DEBUG 12: . 13: . 14: .
The first functionality that you will want to add to the view class is the functionality to display the current record. Because this functionality will be used in several different places within the view class, it makes the most sense to create a separate function to perform this duty. In this function, you get the current values of all the variables in the record object and place those values in the view class variables that are attached to the controls on the window. The other thing that you want to do is get the current record number and the total number of records in the set and display those for the user so that the user knows his or her relative position within the record set.
In your sample application, add a new member function, specify the function type as void, give the function a name that makes sense, such as PopulateView, and specify the access as private. In the function, get a pointer to the document object. Once you have a valid pointer to the document, format the position text display with the current record number and the total number of records in the set, using the GetCurRecordNbr and GetTotalRecords functions that you added to the document class earlier. Next, if you have a valid pointer to a record object, set all the view variables to the values of their respective fields in the record object. Once you set the values of all of the view class variables, update the window with the variable values, as shown in Listing 13.23.
1: void CSerializeView::PopulateView() 2: { 3: // Get a pointer to the current document 4: CSerializeDoc* pDoc = GetDocument(); 5: if (pDoc) 6: { 7: // Display the current record position in the set 8: m_sPosition.Format("Record %d of %d", pDoc->GetCurRecordNbr(), 9: pDoc->GetTotalRecords()); 10: } 11: // Do we have a valid record object? 12: if (m_pCurPerson) 13: { 14: // Yes, get all of the record values 15: m_bEmployed = m_pCurPerson->GetEmployed(); 16: m_iAge = m_pCurPerson->GetAge(); 17: m_sName = m_pCurPerson->GetName(); 18: m_iMaritalStatus = m_pCurPerson->GetMaritalStatus(); 19: } 20: // Update the display 21: UpdateData(FALSE); 22: }
If you added navigation buttons to your window when you were designing the form, then adding navigation functionality is a simple matter of adding event-handler functions for each of these navigation buttons and calling the appropriate navigation function in the document. Once the document navigates to the appropriate record in the set, you need to call the function you just created to display the current record. If the document navigation functions are returning pointers to the new current record object, you should capture that pointer before calling the function to display the current record.
To add this functionality to your sample application, add an event handler to the clicked event for the First button using the Class Wizard. In the function, get a pointer to the document object. Once you have a valid pointer to the document, call the document object's GetFirstRecord function, capturing the returned object pointer in the view CPerson pointer variable. If you receive a valid pointer, call the PopulateView function to display the record data, as in Listing 13.24.
1: void CSerializeView::OnBfirst() 2: { 3: // TODO: Add your control notification handler code here 4: 5: // Get a pointer to the current document 6: CSerializeDoc * pDoc = GetDocument(); 7: if (pDoc) 8: { 9: // Get the first record from the document 10: m_pCurPerson = pDoc->GetFirstRecord(); 11: if (m_pCurPerson) 12: { 13: // Display the current record 14: PopulateView(); 15: } 16: } 17: }
For the Last button, perform the same steps as for the First button, but call the document object's GetLastRecord function, as in Listing 13.25.
1: void CSerializeView::OnBlast() 2: { 3: // TODO: Add your control notification handler code here 4: 5: // Get a pointer to the current document 6: CSerializeDoc * pDoc = GetDocument(); 7: if (pDoc) 8: { 9: // Get the last record from the document 10: m_pCurPerson = pDoc->GetLastRecord(); 11: if (m_pCurPerson) 12: { 13: // Display the current record 14: PopulateView(); 15: } 16: } 17: }
For the Previous and Next buttons, repeat the same steps again, but call the document object's GetPrevRecord and GetNextRecord functions. This final step provides your application with all the navigation functionality necessary to move through the record set. Also, because calling the document's GetNextRecord on the last record in the set automatically adds a new record to the set, you also have the ability to add new records to the set as needed.
When the user enters changes to the data in the controls on the screen, these changes somehow need to make their way into the current record in the document. If you are maintaining a pointer in the view object to the current record object, you can call the record object's various set value functions, passing in the new value, to set the value in the record object.
To implement this in your sample application, add an event handler to the CLICKED event for the Employed check box using the Class Wizard. In the function that you created, first call the UpdateData to copy the values from the form to the view variables. Check to make sure that you have a valid pointer to the current record object, and then call the appropriate Set function on the record object (in this case, the SetEmployed function as in Listing 13.26).
1: void CSerializeView::OnCbemployed() 2: { 3: // TODO: Add your control notification handler code here 4: 5: // Sync the data in the form with the variables 6: UpdateData(TRUE); 7: // If we have a valid person object, pass the data changes to it 8: if (m_pCurPerson) 9: m_pCurPerson->SetEmployed(m_bEmployed); 10: }
Repeat these same steps for the other controls, calling the appropriate record object functions. For the Name and Age edit boxes, you add an event handler on the EN_CHANGE event and call the SetName and SetAge functions. For the marital status radio buttons, add an event handler for the BN_CLICKED event and call the same event-handler function for all four radio buttons. In this function, you call the SetMaritalStat function in the record object.
The last functionality that you need to add is the function to reset the view whenever a new record set is started or opened so that the user doesn't continue to see the old record set. You will call the event handler for the First button, forcing the view to display the first record in the new set of records.
To implement this functionality in your sample application, add a new member function to the view class. Specify the function type as void, give the function the name that you were calling from the document object (NewDataSet), and specify the access as public (so that it can be called from the document class). In the function, call the First button event handler, as in Listing 13.27.
1: void CSerialize1View::NewDataSet() 2: { 3: // Display the first record in the set 4: OnBfirst(); 5: }
Before you can compile and run your application, you need to include the header file for your custom class in the main application source-code file. This file is named the same as your project with the CPP extension. Your custom class header file should be included before the header files for either the document or view classes. For your sample application, you edit the Serialize.cpp file, adding line 8 in Listing 13.28.
FIGURE 13.4. The running serialization application.
1: // Serialize.cpp : Defines the class behaviors for the application. 2: // 3: 4: #include "stdafx.h" 5: #include "Serialize.h" 6: 7: #include "MainFrm.h" 8: #include "Person.h" 9: #include "SerializeDoc.h" 10: #include "SerializeView.h" 11: 12: #ifdef _DEBUG 13: . 14: . 15: .
At this point, you can add, edit, save, and restore sets of records with your application. If you compile and run your application, you can create records of yourself and all your family members, your friends, and anyone else you want to include in this application. If you save the record set you create and then reopen the record set the next time that you run your sample application, you should find that the records are restored back to the state that you originally entered them, as in Figure 13.4.
Today, you learned quite a bit. You learned how serialization works and what it does. You learned how to make a custom class serializable and why and how to use the two macros that are necessary to serialize a class. You also learned how to design and build a form-based SDI application, maintaining a set of records in a flat-file database for use in the application. You learned how to use serialization to create and maintain the flat-file database and how to construct the functionality in the document and view classes to provide navigating and editing capabilities on these record sets.
1: void CSerializeView::OnCbemployed() 2: { 3: // TODO: Add your control notification handler code here 4: 5: // Sync the data in the form with the variables 6: UpdateData(TRUE); 7: // If we have a valid person object, pass the data changes to it 8: if (m_pCurPerson) 9: m_pCurPerson->SetEmployed(m_bEmployed); 10: // Get a pointer to the document 11: CSerializeDoc * pDoc = GetDocument(); 12: if (pDoc) 13: // Set the modified flag in the document 14: pDoc->SetModifiedFlag(); 15: }
FIGURE 13.5. The running serialization application with the person's sex.
The Workshop provides quiz questions to help you solidify your understanding of the material covered and exercises to provide you with experience in using what you've learned. The answers to the quiz questions and exercises are provided in Appendix B, "Answers."
Add a couple of radio buttons to the form to specify the person's sex, as shown in Figure 13.5. Incorporate this change into the CPerson class to make the field persistent.