
Beginning Visual C++ 2005 (2006) [eng]-1
.pdf
Updating Data Sources
At this point you can also right-click each of the buttons and select Add Event Handler from the pop-up to add handlers for them. You can short the default function names to OnSelectproducts() and OnCancelorder() if you want. You will fill in the code for these handlers and deal with the rest of the controls later.
The IDD_PRODUCT_FORM dialog selects the products to be ordered, after the customer has been selected. The application switches to this dialog box when the Select Products button is clicked on the customer selection dialog box. You need to show sufficient information on the dialog box to allow the product to be chosen, and you must provide for the quantity and discount to be entered. The dialog box with its controls is shown in Figure 20-17.
IDC_SELECTPRODUCT Completes the selection of the current project
IDC_ORDERQUANTITY User entry Default value 1
From the customer selection dialog IDC_NEWORDER_ID
From the customer selection dialog IDC_CUSTOMERNAME
Connects to Products table IDC_PRODCUCTNAME
IDC_ORDERDISCOUNT User entry Default value 0
IDC_DONE Completes the order
Figure 20-17
Note that the edit controls here are read-only except for the two for quantity and discount because these two values are the only ones the user needs to supply. You can connect the edit control for the product name to the record set by adding a statement to the DoDataExchange() function in the CProductView class:
void CProductView::DoDataExchange(CDataExchange* pDX)
{
1009

Chapter 20
CRecordView::DoDataExchange(pDX);
DDX_FieldText(pDX, IDC_PRODUCTNAME, m_pSet->m_ProductName, m_pSet);
}
The DDX_FieldText() function call interchangees data between the m_ProductSet member of the CProductSet object and the control with the ID IDD_PRODUCTNAME.
Add BN_CLICKED event handlers for the Select Product and Done buttons to the CProductView using the same technique you used for the previous dialog resource; you can give them the names OnSelectproduct() and OnDone() to conform with the button labels.
The edit controls that display the order ID and the customer name must be initialized with values that will originate in the previous dialog so you will need class variables to hold the values for these controls. Right-click each of these two controls in the IDD_PRODUCT_FORM dialog and select Add Variable from the pop-up. You should select the Value category for both variables and they can both be public. Select type long for the m_OrderID variable and type CString for the m_CustomerName variable.
The user enters values in the edit controls for quantity and discount, so you need variable for these too in the CProductView class; both of these are Value category, too. Add a variable of type int with the name m_Quantity, to store the quantity and a variable of type float with the name m_Discount for the discount value. You should now find that the variables are initialized in the constructor and the DoDataExchange() function looks like this:
void CProductView::DoDataExchange(CDataExchange* pDX)
{
CRecordView::DoDataExchange(pDX);
DDX_FieldText(pDX, IDC_PRODUCTNAME, m_pSet->m_ProductName, m_pSet); DDX_Text(pDX, IDC_NEWORDER, m_OrderID);
DDX_Text(pDX, IDC_COMPANYNAME, m_CustomerName);
DDX_Text(pDX, IDC_ORDERQUANTITY, m_Quantity);
DDX_Text(pDX, IDC_ORDERDISCOUNT, m_Discount);
}
With the dialogs defined, you can implement the mechanism to switch between them.
Implementing Dialog Switching
You saw the basic logic for switching between dialogs in this program in Figure 20-12. A button click is the mechanism for switching from one dialog to the next, so the button handlers will contain code to cause the switch to happen. You can first define view IDs to identify each of the three dialogs, so add a header file, ViewConstants.h. This time you can try using an enum declaration to identify the views so the file should contain the following code:
// Definition of constants identifying the record views
#pragma once
enum ViewID{ ORDER_DETAILS, NEW_ORDER, SELECT_PRODUCT};
1010

Updating Data Sources
You will need a variable of type ViewID in the CMainFrame class to record the ID of the current view, so add m_CurrentViewID by right-clicking on CMainFrame in Class View and selecting Add Member Variable from the pop-up. You need to initialize this, so modify the CMainFrame constructor to:
CMainFrame::CMainFrame() : m_CurrentViewID(ORDER_DETAILS)
{
// TODO: add member initialization code here
}
This identifies the view the application always starts with. Add an #include directive for ViewConstants.h to the .cpp file so the definitions for the view IDs are available here.
You can now add a member function, SelectView(), to CMainFrame that performs switching between dialogs. The return type is void and the single parameter is of type ViewID as the argument will be one of the view IDs defined in the enum. The implementation of SelectView() is like this:
// Enables switching between views. The argument specifies the new view void CMainFrame::SelectView(ViewID viewID)
{
CView* pOldActiveView = GetActiveView(); |
// Get current view |
//Get pointer to new view if it exists
//if it doesn’t the pointer will be null
CView* pNewActiveView = static_cast<CView*>(GetDlgItem(viewID));
//If this is first time around for the new view, the new view
//won’t exist, so we must create it
//The Order Details view is always created first so we don’t need
//to provide for creating that.
if (pNewActiveView == NULL) |
|
{ |
|
switch(viewID) |
|
{ |
|
case NEW_ORDER: |
// Create view to add new order |
pNewActiveView = new CCustomerView; |
|
break; |
|
case SELECT_PRODUCT: |
// Create view to add product to order |
pNewActiveView = new CProductView; break;
default:
AfxMessageBox(_T(“Invalid View ID”)); return;
}
//Switching the views
//Obtain the current view context to apply to the new view CCreateContext context;
context.m_pCurrentDoc = pOldActiveView->GetDocument(); pNewActiveView->Create(NULL, NULL, 0L, CFrameWnd::rectDefault,
|
this, viewID, &context); |
pNewActiveView->OnInitialUpdate(); |
|
} |
|
SetActiveView(pNewActiveView); |
// Activate the new view |
1011

Chapter 20
pOldActiveView->ShowWindow(SW_HIDE); |
// |
Hide |
the |
old |
view |
pNewActiveView->ShowWindow(SW_SHOW); |
// |
Show |
the |
new |
view |
pOldActiveView->SetDlgCtrlID(m_CurrentViewID); // Set the old view ID pNewActiveView->SetDlgCtrlID(AFX_IDW_PANE_FIRST);
m_CurrentViewID = viewID; |
// Save the new view ID |
RecalcLayout(); |
|
}
The code here refers to the CCustomerView and CProductView classes, so #include directives for CustomerView.h and ProductView.h are necessary in the source file.
Switching from the order details dialog to the dialog starting new order creation is done in the handler for OnNeworder() in the COrderDetailsView class:
void COrderDetailsView::OnNeworder()
{
static_cast<CMainFrame*>(GetParentFrame())->SelectView(NEW_ORDER);
}
This gets a pointer to the parent frame for the view — the CMainFrame object for the application — and then uses that to call SelectView() to select the new order processing dialog. An #include directive for ViewConstants.h is also necessary in this source file because you refer to NEW_ORDER here, and you also must add an #include directive for MainFrm.h to get at the definition of CMainFrame.
The Select Products button handler in the CCustomerView class switches to the dialog box for
CProductsView:
void CCustomerView::OnSelectproducts()
{
static_cast<CMainFrame*>(GetParentFrame())->SelectView(SELECT_PRODUCT);
}
The Cancel button handler in the same class just switches back to the previous view:
void CCustomerView::OnCancelorder()
{
static_cast<CMainFrame*>(GetParentFrame())->SelectView(ORDER_DETAILS);
}
Don’t forget to add #include directives for ViewConstants.h and MainFrm.h in CustomerView.cpp.
The last switching operation to be implemented is in the OnDone() handler in the CProductView class:
void CProductView::OnDone()
{
static_cast<CMainFrame*>(GetParentFrame())->SelectView(ORDER_DETAILS);
}
This switches back to the original application view that allows browsing and editing of order details. Of course, you could alternatively switch back to the CCustomerView dialog to provide a succession of order entries if you wanted to. Don’t forget the #include directives for ViewConstants.h and
MainFrm.h once more.
1012

Updating Data Sources
The switching from the initial dialog box that allows you to browse order details to the dialog box for editing the details now has to control the visibility of the New Order button; otherwise, the Cancel button will is hidden by the New Order button in the editing dialog. Add a control variable, m_NewOrderCtrl, in COrderDetailsView corresponding to the IDC_NEWORDER ID. Then you can amend the
OnEditorder() handler to:
void COrderDetailsView::OnEditorder()
{
if(m_pSet->CanUpdate())
{
try
{
if(m_Mode == UPDATE)
{// When button was clicked we were in update // Disable input to edit controls
m_QuantityCtrl.SetReadOnly(); m_DiscountCtrl.SetReadOnly();
//Change the Update button text to Edit Order m_EditOrderCtrl.SetWindowText(_T(“Edit Order”));
//Make the Cancel button invisible
m_CancelEditCtrl.ShowWindow(SW_HIDE);
// Show the new order button
m_NewOrderCtrl.ShowWindow(SW_SHOW);
// Complete the update |
|
m_pSet->Update(); |
|
m_Mode = READ_ONLY; |
// Change to read-only mode |
}
else
{ // When button was clicked we were in read-only mode
//Enable input to edit controls m_QuantityCtrl.SetReadOnly(FALSE); m_DiscountCtrl.SetReadOnly(FALSE);
//Change the Edit Order button text to Update m_EditOrderCtrl.SetWindowText(_T(“Update”));
//Hide the new order button
m_NewOrderCtrl.ShowWindow(SW_HIDE);
// Make the Cancel button visible m_CancelEditCtrl.ShowWindow(SW_SHOW);
// Start the update m_pSet->Edit();
m_Mode = UPDATE; |
// Switch to update mode |
} |
|
} |
|
catch(CException* pEx) |
|
1013

Chapter 20
{ |
|
pEx->ReportError(); |
// Display the error message |
} |
|
} |
|
else |
|
AfxMessageBox(_T(“Recordset is not updatable.”));
}
Now you hide or show the New Order button, depending on whether the value stored in m_UpdateMode is READ_ONLY or UPDATE. You also must make the button visible in the OnCancel() handler:
void COrderDetailsView::OnCancel() |
|
{ |
|
m_pSet->CancelUpdate(); |
// Cancel the update operation |
m_EditOrderCtrl.SetWindowText(_T(“Edit”)); |
// Switch button text |
m_CancelEditCtrl.ShowWindow(SW_HIDE); |
// Hide the Cancel button |
m_NewOrderCtrl.ShowWindow(SW_SHOW); |
// Show the New Order button |
m_QuantityCtrl.SetReadOnly(TRUE); |
// Set state of quantity edit control |
m_DiscountCtrl.SetReadOnly(TRUE); |
// Set state of discount edit control |
m_UpdateMode = !m_UpdateMode; |
// Switch the mode |
} |
|
What you have done so far here is to implement the basic view switching mechanism. You will still need to come back and add code to deal with updating the database; however, this is a good point to try compiling and executing what you have to shake out any typos or other errors you might have added. After it works, you should find that you can scroll through the customers and the products. Make sure you check out all the switching paths.
Creating an Order ID
To create an ID for a new order, you need a recordset for the Orders table. Right-click DBSimpleUpdate in Class View and select the Add > Class menu item from the pop-up. Select MFC ODBC Consumer as the template and click the Add button; then select Northwind as the database and Orders as the table for the recordset. Choose Dynaset as the type because you reuse this recordset when you want to add a new order. Enter the class name as COrderSet and the corresponding files names as OrderSet.h and OrderSet.cpp. Click the Finish button to create the class. You can change the CStringW members of the new class to type CString and comment out the #error directive in the OrderSet.cpp class.
Storing the New Order ID
In this section I will go into operations with recordsets in a little more depth. You will need to create a unique order ID whenever you start creating a new order in the CCustomerView class, so you need to think about where you can best do this and what the process should be. It really should be a COrderSet object’s responsibility to create the new ID, even though the ID is displayed by one of the edit controls in the view represented by the CCustomerView object because the new ID is essentially dependent on the data in this recordset. A good approach would be to add a variable in the CCustomerView class that sets the value of the ID in the edit control that can be set using a function belonging to a COrderSet object.
Go to the IDD_CUSTOMER_FORM dialog form in Resource View and right-click the edit control for the order ID(the control has the ID IDC_NEWORDERID. Select Add Variable from the pop-up and then enter the name. Choose the variable type and category as shown in Figure 20-18.
1014

Updating Data Sources
Figure 20-18
The type is CString by default, so make sure you set it to long. The DDX_Text() functions that transfer data to and from an edit control come in a number of flavors to accommodate the different data types shown in the drop-down list in the dialog box.
Creating the New Order ID
The COrderSet object belongs in the document object, so add a public data member to the
CDBSimpleUpdateDoc class with the name m_OrderSet to go along with the m_DBSimpleUpdateSet member that was created by Application wizard. You do this as usual by right-clicking the class name in Class View a selecting Add > Add Variable from the pop-up. The COrderSet object is created automatically when the document object is created. With the object for the order set in the document, it is accessible in any of the view classes that need it.
You can add a new member function to the COrderSet class to generate the unique new order ID. Go to Class View and add the function, CreateNewOrderID(), with a long return type and no parameters.
The first thing the CreateNewOrderID() function needs to do is check whether the recordset is open:
long COrderSet::CreateNewOrderID()
{
if(!IsOpen())
Open(CRecordset::dynaset);
// Rest of the function implementation...
}
The IsOpen() function that you call in the if statement returns TRUE if the recordset is open and FALSE otherwise. To open the recordset, you call the Open() member that is inherited from CRecordset. This runs an SQL query against the database with the recordset type specified by the first argument. You
1015

Chapter 20
have the first argument specified as CRecordset::dynaset, which, as you might expect, opens the recordset as a dynaset. As it happens, this is unnecessary because if you omitted the argument, the default that you specified when you created the class — dynaset — would apply. However, this does provide a cue to mention the other options that you have for this argument:
CRecordset::snapshot |
Recordset is opened as snapshot — I discussed snapshot |
|
and dynaset in the previous chapter) |
CRecordset::forwardonly |
Recordset is opened as read-only and it can only be scrolled |
|
forward. (When a recordset is opened, it is positioned at the |
|
first record automatically.) |
CRecordset::dynamic |
Recordset is open with scrolling in both directions, and |
|
changes made by other users are reflected in the recordset |
|
fields. |
AFX_DB_USE_DEFAULT_TYPE |
Recordset is opened with the default recordset type stored |
|
in the inherited member, m_nDefaultType, which is initial- |
|
ized in the constructor. |
|
|
There are two further parameters to Open() for which you have accepted default argument values. The second parameter is a pointer to a string that can be a table name, an SQL SELECT statement, a call of a predefined query procedure, or null, which is the default. If it is null, the string returned by GetDefaultSQL() is used. The third parameter is a bit mask that you can use to specify a myriad of
options for the connection, including making it read-only, which means that you can’t write to it at all, or making it append-only, which prohibits editing or deleting records. You will find more details on this in the documentation for this function.
With the recordset opened, we want to scan through all the records to find the largest value in the OrderID field. You can do that by adding the following code:
long COrderSet::CreateNewOrderID()
{
if(!IsOpen())
Open(CRecordset::dynaset); |
|
|
|
// Check for no records in recordset |
|
long newOrderID = 0; |
|
if(!(IsBOF() && IsEOF())) |
|
{ |
// We have records |
MoveFirst(); |
// so go to the first |
while(!IsEOF()) |
// Compare with all the others |
{ |
|
// Save order ID if its larger |
|
if(newOrderID < m_OrderID) |
|
newOrderID = m_OrderID; |
|
MoveNext(); |
// Go to next record |
} |
|
} |
|
return ++newOrderID; |
|
} |
|
1016

Updating Data Sources
The IsBOF() and IsEOF() members of the recordset class return true if you are beyond the beginning or end of the records in the recordset respectively, in which case no record is currently active so you should be using the fields. When a recordset is empty, both functions return TRUE. As long as there are records, you move to the first record by calling the MoveFirst() member function. There is also a MoveLast() member that goes to the last record in the recordset.
You create a local variable, newOrderID, with an initial value of 0 that eventually stores the maximum order ID in the table. The while loop moves through each of the records in the recordset using the MoveNext() member function, checking for a larger value for the m_OrderID member. Before calling any of the move members of a recordset, you must call either IsEOF() or IsBOF(), depending on which way you are going. If you call a move function when you are beyond the end or beginning of the recordset, the function throws an exception of type CDBException.
In addition to the move functions you have used here, a recordset object provides you with three others:
MoveLast() |
Moves to the last record in the recordset. You must not use |
|
this function (or MoveFirst()) with forward-only record- |
|
set, otherwise an exception of type CDBEception is thrown. |
MovePrev() |
Moves to the record preceding the current record in the |
|
recordset. If there isn’t one, it moves to one position beyond |
|
the first record. After this the recordset fields are not valid |
|
and IsBOF()returns true. |
Move() |
This is used to move one or more records through a record- |
|
set. The first argument, of type long, specifies the number |
|
of rows to move. The second argument of type WORD deter- |
|
mines the nature of the move operation. Four values for the |
|
second argument make the function equivalent to the other |
|
move functions we have seen. You will find more details on |
|
this in the Visual C++ documentation. |
|
|
When the loop ends you have the maximum order ID stored in newOrderID, so you just need to increment it by 1 before returning it.
The last step is to get the value transferred to the control so it appears in the IDD_CUSTOMER_FORM dialog. A call to UpdateData() for the recordset view object with an argument of FALSE does this. This function is inherited in the record view class from CWnd. An argument of FALSE causes the data to be transferred from data member of the View Class to the controls in the dialog. A value of TRUE causes data to be retrieved from the controls and stored in the data members. In both cases this is achieved by causing the DoDataExchange() member of view to be called by the framework.
Initiating ID Creation
The customer view needS a new order ID to be available when it is first displayed. Add a public member function, SetNewOrderID(), to the CCustomerView class and implement it as follows:
void CCustomerView::SetNewOrderID(void)
{
1017

Chapter 20
// Get a new order ID from the COrderSet object in the document m_NewOrderID = static_cast<CDBSimpleUpdateDoc*>
|
(GetDocument())->m_OrderSet.CreateNewOrderID(); |
UpdateData(FALSE); |
// Transfer data to controls |
}
The pointer returned by the inherited GetDocument() function is of type CDocument. You want to use this to access the m_OrderSet member of the derived class so you must cast the pointer to type CDBSimpleUpdateDoc*. You then call the member function for the m_OrderSet member of the document class that returns the new order ID, and store the result in the m_NewOrderID member of the CCustomerView class. Calling the inherited UpdateData() member of the view transfers the data from the data members of the view to the controls. You must now add an #include directive for DBSimpleUpdateDoc.h to the source file because you refer to the CDBSimpleUpdateDoc class name.
Because you only ever create a single CCustomerView object and reuse it as necessary, you will want a new ID to be available each time you switch to that view. The SelectView() member of the CMainFrame object deals with switching between dialogs and this is also where a CCustomerView object gets created first time around. This is a good place to initiate the process for creating the new order ID. All you need to do is to add some code to call the SetNewOrderID() member if the view corresponds to the CCustomerView.
void CMainFrame::SelectView(ViewID viewID) |
|
{ |
|
CView* pOldActiveView = GetActiveView(); |
// Get current view |
//Get pointer to new view if it exists
//if it doesn’t the pointer will be null
CView* pNewActiveView = static_cast<CView*>(GetDlgItem(viewID));
//If this is first time around for the new view, the new view
//won’t exist, so we must create it
//The Order Details view is always created first so we don’t need
//to provide for creating that.
if (pNewActiveView == NULL) |
|
{ |
|
switch(viewID) |
|
{ |
|
case NEW_ORDER: |
// Create view to add new order |
pNewActiveView = new CCustomerView; |
|
break; |
|
case SELECT_PRODUCT: |
// Create view to add product to order |
pNewActiveView = new CProductView; break;
default:
AfxMessageBox(_T(“Invalid View ID”)); return;
}
//Switching the views
//Obtain the current view context to apply to the new view CCreateContext context;
context.m_pCurrentDoc = pOldActiveView->GetDocument(); pNewActiveView->Create(NULL, NULL, 0L, CFrameWnd::rectDefault,
1018