In this lab, you will enable the STUpload application to work with persistent data contained in files on your local hard disk. You will modify the application in two sections. First, you will finish implementing the CSTUploadDoc::LoadData() function that you created in Chapter 5. The CSTUploadDoc::OnDataImport() function calls the LoadData() function to load data from a text file into the application.
Second, you will implement standard MFC serialization for the STUpload application so that the application data imported from the text file can be saved as an STUpload document file.
In Lab 5, you created a temporary implementation of the CSTUploadDoc::LoadData() function. Currently, this function simply adds some hard-coded stock price records to the CStockDataList object CSTUploadDoc::m_DocList. You will finish implementing the LoadData() function to load records into m_DocList from the CStdioFile object passed in from the CSTUploadDoc::OnDataImport() function. Your first task is to use the CStdioFile object to open the text file selected by the user.
if(nID == IDOK) |
(This code can be found in CH6_01.cpp, installed from the companion CD.)
CFileException fx; if(!aFile.Open(aFileDialog.GetPathName(), CFile::modeRead | CFile::typeText, &fx)) { TCHAR buf[255]; fx.GetErrorMessage(buf, 255); CString strPrompt(buf); AfxMessageBox(strPrompt); return; } |
The entire function should now look as follows:
void CSTUploadDoc::OnDataImport() { // String to customize File Dialog CString strFilter = Data Files (*.dat)|*.dat|All Files (*.*)|*.*||"; CFileDialog aFileDialog(TRUE, NULL, NULL, OFN_HIDEREADONLY | OFN_OVERWRITEPROMPT, strFilter); int nID = aFileDialog.DoModal(); if(nID == IDOK) { CStdioFile aFile; CFileException fx; if(!aFile.Open(aFileDialog.GetPathName(), CFile::modeRead | CFile::typeText, &fx)) { TCHAR buf[255]; fx.GetErrorMessage(buf, 255); CString strPrompt(buf); AfxMessageBox(strPrompt); return; } LoadData(aFile); } } |
Before you replace the old LoadData() function with a new version, you will make a couple of changes to enable the new function to use the Conflicting Records dialog box. First, you must call the AfxInitRichEdit() function to initialize rich edit controls for your application.
AfxInitRichEdit(); |
Now you are ready to implement the new LoadData() function.
#include "ConflictDialog.h" |
(This code can be found in CH6_02.cpp, installed from the companion CD.)
BOOL CSTUploadDoc::LoadData(CStdioFile &infile) { // Check for NULL ASSERT(infile.m_hFile != NULL); // Hold data in temporary list of CStockData objects, // which we assign to CSTUploadDoc::m_DocList only // when we are sure load has been completed successfully CStockDataList TempList; // Additions are cumulative, so we need to copy in existing data TempList.AddHead(&m_DocList); // Line buffer CString strTemp; // Today's date COleDateTime Today = COleDateTime::GetCurrentTime(); COleDateTime FileDate; CString strFileHeader; int addedCtr = 0; // Count added items int discardedCtr = 0; // Count discarded items BOOL bFirstLine = TRUE; while(infile.ReadString(strTemp)) { BOOL bValidDate = FALSE; CString strFund; CString strDate; // Exclude blank lines if(strTemp.GetLength() == 0) continue; if(bFirstLine) { // Get Header information strFileHeader = strTemp.Left(18); strFileHeader.TrimRight(); strDate = strTemp.Mid(18, 10); } else { strFund = strTemp.Left(8); strFund.TrimRight(); strDate = strTemp.Mid(8, 10); } int nYear = atoi(strDate.Right(4)); int nMonth = atoi(strDate.Left(2)); int nDay = atoi(strDate.Mid(3, 2)); COleDateTime aDate(nYear, nMonth, nDay, 0, 0, 0); if(aDate.GetStatus() != COleDateTime::valid) { if(bFirstLine) { // Cannot read file date - assume invalid AfxMessageBox("Invalid File Format"); return FALSE; } else { // Cannot read record date - discard line discardedCtr++; continue; } } if(bFirstLine) { // Get file date - loop back to top FileDate = aDate; bFirstLine = FALSE; continue; } double dPrice = atof(strTemp.Mid(19)); // Make a CStockData object and add it // to our temporary array CStockData aStData(strFund, aDate, dPrice); CStockDataList::errorstatus err; POSITION CurPos = TempList.AddSorted(aStData, err); switch(err) { // Discard identical entry case CStockDataList::duplicate_entry : discardedCtr ++ ; continue; // Same record, different price value case CStockDataList::conflicting_entry : { // Query if user wants to discard duplicate, // replace, or abort CConflictDialog aDialog; // Construct text to appear in rich edit // control CString strText = "Existing entry:\n\n"; CStockData SDTemp = TempList.GetAt(CurPos); strText += SDTemp.GetAsString(); strText += "\n\nReplacement entry:\n\n"; strText += aStData.GetAsString(); // Assign text to control variable aDialog.m_REditText = strText; switch(aDialog.DoModal()) { case IDABORT : // Abandon return FALSE; case IDCANCEL : // Discard new record discardedCtr++ ; continue; case IDOK : // Replace existing record TempList.SetAt(CurPos, aStData); } } default: // Ok addedCtr++ ; } } // If we got this far then this is a valid record CString strPrompt; strPrompt.Format( "Import of file %s complete:\nRecords loaded: %d \ \nRecords discarded: %d \ \n\nHit OK to load data into document.", strFileHeader, addedCtr, discardedCtr); if(AfxMessageBox(strPrompt, MB_OKCANCEL) == IDOK) { // Update document data m_DocList.RemoveAll(); m_DocList.AddHead(&TempList); // Update fund view CMainFrame * pWnd = dynamic_cast<CMainFrame *> (AfxGetMainWnd()); if(pWnd) { pWnd->UpdateFundList(m_DocList); // Show fund window after loading new funds pWnd->SetFundsVisible(TRUE); } return TRUE; } else return FALSE; } |
STUpload application data consists of a single CStockDataList object that is a collection of CStockData objects. The CStockData object encapsulates a fund name, a date and a price. The document also contains a CString variable that records the currently selected fund name. This variable should also be serialized, so that the fund that was currently selected when the document file was saved will still be selected when the file is restored.
To serialize the STUpload application data, you will need to:
The CStockData class that we have provided is directly derived from the CObject class, and it includes a default constructor. You will need to add the serialization macros and the Serialize() function.
DECLARE_SERIAL(CStockData) |
IMPLEMENT_SERIAL(CStockData, CObject, 1) |
virtual void Serialize(CArchive& ar); |
(This code can be found in CH6_03.cpp, installed from the companion CD.)
void CStockData::Serialize(CArchive& ar) { if (ar.IsStoring()) { ar << m_strFund; ar << m_date; ar << m_dblPrice; } else { ar >> m_strFund; ar >> m_date; ar >> m_dblPrice; } } |
You will now implement the function template SerializeElements() for the CStockData element type. The function will simply iterate across the collection data and call Serialize() for each CStockData object.
template <> void AFXAPI SerializeElements <CStockData> (CArchive& ar, CStockData* pNewSD, int nCount); |
(This code can be found in CH6_04.cpp, installed from the companion CD.)
template <> void AFXAPI SerializeElements <CStockData> (CArchive& ar, CStockData* pNewSD, int nCount) { for (int i = 0; i < nCount; i++, pNewSD++) { // Serialize each CStockData object pNewSD->Serialize(ar); } } |
At this point, you are ready to implement the document serialization code.
(This code can be found in CH6_05.cpp, installed from the companion CD.)
void CSTUploadDoc::Serialize(CArchive& ar) { m_DocList.Serialize(ar); if (ar.IsStoring()) { ar << m_strCurrentFund; } else { ar >> m_strCurrentFund; // Update Select Fund window CMainFrame* pWnd = dynamic_cast<CMainFrame *> (AfxGetMainWnd()); if(pWnd) // Will fail if running from icon or from // command line with file name argument { // Update and show fund window pWnd->UpdateFundList(m_DocList, m_strCurrentFund); pWnd->SetFundsVisible(TRUE); } } } |
All the stock data records are serialized in a single call to CStockDataList:: Serialize(). The CSTUploadDoc::m_strCurrentFund data member is also serialized.
Take note of the code that displays the fund window when a document file is loaded. This is not possible here when the application is launched by double-clicking a document file icon, as the pointer to the main window is not available at this point. However, you can instruct the application to display the fund window after the main window has been created, if it detects that a document has already been loaded.
(This code can be found in CH6_06.cpp, installed from the companion CD.)
CMainFrame * pFrameWnd = dynamic_cast<CMainFrame *> (m_pMainWnd); ASSERT_VALID(pFrameWnd); CSTUploadDoc * pDoc = dynamic_cast<CSTUploadDoc *>(pFrameWnd->GetActiveDocument()); ASSERT_VALID(pDoc); if(pDoc->GetDocList().GetCount() > 0) // Non-empty document at main window creation time means we are // running from icon or from command line with file name argument { pFrameWnd->UpdateFundList(pDoc->GetDocList(), pDoc->GetCurrentFund()); pFrameWnd->SetFundsVisible(TRUE); } |
Because STUpload is an SDI application, any data contained in the document object must be cleared out by the DeleteContents() function.
(This code can be found CH6_07.cpp, installed from the companion CD.)
m_DocList.RemoveAll(); CMainFrame * pWnd = dynamic_cast<CMainFrame *> (AfxGetMainWnd()); if(pWnd) { pWnd->UpdateFundList(m_DocList); // No funds on file, so hide fund window pWnd->SetFundsVisible(FALSE); // And reset current fund value SetCurrentFund(""); } |
Finally you must add calls to CDocument::SetModifiedFlag() whenever the persistent document data is changed. In the STUpload application the data is modified in two places:
LoadData(aFile); |
if(LoadData(aFile)) { SetModifiedFlag(); UpdateAllViews(NULL); } |
Locate the CFundDialog::OnSelchangeFundlist() function. At the end of the function string, before the closing brace, add the line:
pDoc->SetModifiedFlag(); |