Figure 19-14 shows how data from the database ultimately gets to be displayed by the view.Figure 19-14 The transfer of data between the data members in the CProductSetobject that corresp
Trang 1your recordset object accesses a record If the data in the current record is modified by another user, this
is not apparent in your recordset object unless you move to another record and then return to the nal record A dynaset recordset uses an index to the database tables involved to generate the contents ofeach record dynamically Because you have no other users accessing the Northwinddatabase, you canchoose the Snapshotoption for your example
origi-After Snapshot has been chosen, you can click the Generated Classesoption to display the classes inyour application The dialog box is shown in Figure 19-13
Figure 19-13
Here you can change the class names and the corresponding file names assigned by the wizard to something more suitable, if you want In addition to the changes shown for the CDBSampleViewandCProductViewclasses and the corresponding changes to the names of the hand cppfiles for the class,you could also change the CDBSampleSetclass name to CProductSet, and the associated hand cppfilenames to be consistent with the class name After that is done, click Finishand generate the project
Understanding the Program Structure
The basic structure of the program is as you have seen before, with an Application Class CDBSampleApp,
a Frame Window Class CMainFrame, a Document Class CDBSampleDoc, and a view class CProductView
A document template object is responsible for creating and relating the frame window, document, andview objects This is done in a standard manner in the InitInstance()member of the applicationobject The document class is standard, except that the MFC Application wizard has added a data mem-ber, m_DBSampleSet,which is an object of the CProductSetclass type As a consequence, a recordsetobject is automatically created when the document object is created in the InitInstance()functionmember of the application object The significant departures from a non-database program arise in thedetail of the CRecordsetclass, and in the CRecordViewclass, so take a look at those
936
Trang 2Understanding Recordsets
You can look at the definition of the CProductSetclass that the Application wizard has generated meal and see how each piece works I’ll show the bits under discussion as shaded in the code fragments
piece-Recordset Creation
The first segment of the class definition that is of interest is:
class CProductSet : public CRecordset{
public:
CProductSet(CDatabase* pDatabase = NULL);
DECLARE_DYNAMIC(CProductSet)// Plus more of the class definition
// Overrides// Wizard generated virtual function overridespublic:
virtual CString GetDefaultConnect(); // Default connection stringvirtual CString GetDefaultSQL(); // default SQL for Recordsetvirtual void DoFieldExchange(CFieldExchange* pFX);// RFX support// Plus some more standard stuff
};
The class has CRecordsetas a base class and provides the functionality for retrieving data from thedatabase The constructor for the class accepts a pointer to a CDatabaseobject that is set to NULLas adefault The parameter to the constructor allows a CProductSetobject to be created for a CDatabaseobject that already exists, which allows an existing connection to a database to be reused Opening a connection to a database is a lengthy business, so it’s advantageous to re-use a database connectionwhen you can
If no pointer is passed to the constructor, as is the case for the m_DBSampleSetmember of the documentclass CDBSampleDoc, the framework automatically creates a CDatabaseobject for you and calls theGetDefaultConnect()function member of CProductSetto define the connection The Applicationwizard provides the following implementation of this function:
CString CProductSet::GetDefaultConnect(){
Trang 3The GetDefaultConnect()function is a pure virtual function in the base class, CRecordset, and somust always be implemented in a derived recordset class The value returned from the function is a sin-gle string between double quotes but I have shown it spread over several lines to make the contents ofthe string more apparent The implementation provided by Application wizard returns the text stringshown to the framework This identifies the database with its name and path plus values for the otherparameters you can see and enables the framework to create a CDatabaseobject that establishes thedatabase connection automatically The meaning of the arguments in the connection string is as follows:
Argument Description
DBQ The database qualifier, which in this case is the path to the Access database
file
DriverID The ID of the ODBC driver for the database
MaxBufferSize The maximum size of the buffer to be used for data transfer
PageTimeout The length of time in seconds to wait for a connection to the database It is
important to set this value to an adequate value to avoid connection failureswhen accessing a remote database
UID The user ID for accessing the database
In practice, it’s usually necessary to supply a password as well as a user ID before access to a database
is permitted, and it’s unwise to expose the password in the code in plain text form For this reason theApplication wizard has inserted the following line preceding the definition of the GetDefaultConnect()function:
#error Security Issue: The connection string may contain a password
Compilation fails with this directive in the code, so you must comment it out or delete it to compile theprogram successfully
You can make the framework pop up a dialog box for the user to select the database name from the list ofregistered database sources by writing the return statement in the GetDefaultConnect()function as:return _T(“ODBC;”);
You will also be prompted for a user ID and password when this is required for access to the database
Querying the Database
The CProductSetclass includes a data member for each field in the Productstable The Applicationwizard obtains the field names from the database and uses these to name the corresponding data mem-bers of the class They appear in the block of code following the Field/Param Datacomment in theCProductSetclass definition:
938
Trang 4class CProductSet : public CRecordset{
public:
CProductSet(CDatabase* pDatabase = NULL);
DECLARE_DYNAMIC(CProductSet)// Field/Param Data
// The string types below (if present) reflect the actual data type of the// database field - CStringA for ANSI datatypes and CStringW for Unicode// datatypes This is to prevent the ODBC driver from performing // potentially unnecessary conversions
// If you wish, you may change these members to// CString types and the ODBC driver will perform all necessary // conversions
// (Note: You must use an ODBC driver version that is version 3.5 or // greater to support both Unicode and these conversions)
long m_ProductID; // Number automatically assigned to new product
CStringW m_ProductName;
long m_SupplierID; // Same entry as in Suppliers table
long m_CategoryID; // Same entry as in Categories table
CStringW m_QuantityPerUnit; // (e.g., 24-count case, 1-liter bottle)
double m_UnitPrice;
int m_UnitsInStock;
int m_UnitsOnOrder;
int m_ReorderLevel; // Minimum units to maintain in stock
BOOL m_Discontinued; // Yes means item is no longer available
// Overrides// Wizard generated virtual function overridespublic:
virtual CString GetDefaultConnect(); // Default connection string
virtual CString GetDefaultSQL(); // default SQL for Recordsetvirtual void DoFieldExchange(CFieldExchange* pFX); // RFX support// Implementation
#ifdef _DEBUGvirtual void AssertValid() const;
virtual void Dump(CDumpContext& dc) const;
#endif};
The type of each data member is set to correspond with the field type for the corresponding field in theProductstable You may not want all these fields in practice, but you shouldn’t delete them willy-nilly
in the class definition As you will see shortly, they are referenced in other places, so you must ensurethat all references to a field are deleted, too A further caveat is that you must not delete primary keys Ifyou do, the recordset won’t work, so you need to be sure which fields are primary keys before choppingout what you don’t want
Note that two of the fields have CStringWas the type You haven’t seen this before, but the CStringWclass type just encapsulates a Unicode string rather than an ASCII string It is more convenient when
Trang 5you are accessing the fields to use type CString, so change the type of the m_ProductNameandm_QuantityPerUnitmembers to CString This allows the strings to be handled as ASCII strings in the example Clearly, if you are writing internationalized database applications, you would need tomaintain any CStringWfields as such because they may contain characters that are not within the ASCII character set.
The SQL operation that applies to the recordset to populate these data members is specified in theGetDefaultSQL()function The implementation that the Application wizard has supplied for this is:CString CProductSet::GetDefaultSQL()
The GetDefaultSQL()function is called by the MFC framework when it constructs the SQL statement
to be applied for the recordset The framework slots the string returned by this function into a skeletonSQL statement with the form:
SELECT * FROM < String returned by GetDefaultSQL() >;
This looks simplistic, and indeed it is, but you can add WHEREand ORDER BYclauses to the operation, asyou’ll see later
Data Transfer between the Database and the Recordset
The transfer of data from the database to the recordset, and vice versa, is accomplished by the
DoFieldExchange()member of the CProductSetclass The implementation of this function is:void CProductSet::DoFieldExchange(CFieldExchange* pFX)
{
pFX->SetFieldType(CFieldExchange::outputColumn);
// Macros such as RFX_Text() and RFX_Int() are dependent on the
// type of the member variable, not the type of the field in the database
// ODBC will try to automatically convert the column value to the requested
// type
RFX_Long(pFX, _T(“[ProductID]”), m_ProductID);
RFX_Text(pFX, _T(“[ProductName]”), m_ProductName);
RFX_Long(pFX, _T(“[SupplierID]”), m_SupplierID);
RFX_Long(pFX, _T(“[CategoryID]”), m_CategoryID);
RFX_Text(pFX, _T(“[QuantityPerUnit]”), m_QuantityPerUnit);
RFX_Double(pFX, _T(“[UnitPrice]”), m_UnitPrice);
RFX_Int(pFX, _T(“[UnitsInStock]”), m_UnitsInStock);
RFX_Int(pFX, _T(“[UnitsOnOrder]”), m_UnitsOnOrder);
RFX_Int(pFX, _T(“[ReorderLevel]”), m_ReorderLevel);
RFX_Bool(pFX, _T(“[Discontinued]”), m_Discontinued);
}
940
Trang 6This function is called automatically by the MFC framework to store data in and retrieve data from thedatabase It works in a similar fashion to the DoDataExchange()function you have seen with dialogcontrols in that the pFXparameter determines whether the operation is a read or a write Each time it’scalled, it moves a single record to or from the recordset object.
The first function called is SetFieldType(), which sets a mode for the RFX_()function calls that low In this case, the mode is specified as outputColumn, which indicates that data is to be exchangedbetween the database field and the corresponding argument specified in each of the following RFX_()function calls
fol-There is a whole range of RFX_()functions for various types of database field The function call for aparticular field corresponds with the data type applicable to that field The first argument to an RFX_()function call is the pFXobject that determines the direction of data movement The second argument isthe table field name and the third is the data member that is to store that field for the current record
Understanding the Record View
The purpose of the view class is to display information from the recordset object in the application dow, so you need to understand how this works The bits of the CProductViewclass definition that are
win-of primary interest are shown shaded:
class CProductView : public CRecordView{
protected: // create from serialization onlyCProductView();
DECLARE_DYNCREATE(CProductView)public:
enum{ IDD = IDD_DBSAMPLE_FORM };
CProductSet* m_pSet;
// Attributespublic:
CDBSampleDoc* GetDocument();
// Operationspublic:
// Overridespublic:
virtual CRecordset* OnGetRecordset();
virtual BOOL PreCreateWindow(CREATESTRUCT& cs);
protected:
virtual void DoDataExchange(CDataExchange* pDX); // DDX/DDV supportvirtual void OnInitialUpdate(); // called first time after constructvirtual BOOL OnPreparePrinting(CPrintInfo* pInfo);
virtual void OnBeginPrinting(CDC* pDC, CPrintInfo* pInfo);
virtual void OnEndPrinting(CDC* pDC, CPrintInfo* pInfo);
// Implementationpublic:
Trang 7virtual ~CProductView();
#ifdef _DEBUG
virtual void AssertValid() const;
virtual void Dump(CDumpContext& dc) const;
dis-Note that the constructor is protected This is because objects of this class are expected to be createdfrom serialization, which is a default assumption for record view classes When you add further recordview classes to the application, you’ll need to change the default access for their constructors to publicbecause you’ll be creating the views yourself
In the first publicblock in the class, the enumeration adds the ID IDD_DBSAMPLE_FORMas a memberthe class This is the ID for a blank dialog that the Application Wizard has included in the program.You’ll add controls to this dialog to display the database fields from the Productstable that you wantdisplayed The dialog ID is passed to the base class, CRecordView, in the initialization list of the con-structor for the view class:
Trang 8Figure 19-14 shows how data from the database ultimately gets to be displayed by the view.
Figure 19-14
The transfer of data between the data members in the CProductSetobject that correspond to fields inthe Products table and the controls in the dialog box associated with the CProductViewobject is man-aged by the DoDataExchange()member of CProductView The code in this function to do this is not inplace yet because you first need to add the controls to the dialog that are going to display the data andthen link the controls to the recordset data members You will do that next
Creating the View Dialog
The first step is to place the controls on the dialog box, so go to Resource View, expand the list of dialogresources, and double-click Idd_Dbsample_Form You can delete the static text object with the TODOmessage from the dialog If you right-click the dialog box, you can choose to view its properties, asshown in Figure 19-15
If you scroll down through the properties you’ll see that the Styleproperty has been set to Childbecause the dialog box is going to be a child window and will fill the client area The Borderpropertyhas been set to Nonebecause if the dialog box is to fill the client area, it won’t need a border
You’ll add a static text control to identify each field from the recordset that you want to display, plus anedit control to display it
RecordSet Object
View Object
Trang 10You can add the text to each static control by just typing it as soon as the control has been placed on thedialog As you see, I have entered the text for each static control so that it corresponds to the field name
in the database It’s a good idea to make sure that all the edit controls have sensible and different IDs, soright-click each of them in turn to display and modify their properties Figure 19-17 shows the propertiesfor the control corresponding to Product ID
Figure 19-17
It’s helpful to use the field name as part of the control ID as this indicates what the control display.Figure 19-17 shows the ID for the first edit control in the title bar of the properties window after I havemodified it You can change the IDs for the other edit controls similarly Because you are not intending toupdate the database in this example, you should make sure that the data displayed by each edit box can-not be modified from the keyboard You can do that by setting the Read Onlyproperty for of each of theedit controls as True The background to the edit boxes will then have a different color to signal thatthey cannot be altered, as shown in Figure 19-18
You can add other fields to the dialog box, if you want The one that is most important for the rest of ourexample is the Product ID, so you must include that Save the dialog and then move on to the last step:linking the controls to the variables in the recordset class
Trang 11Figure 19-18
Linking the Controls to the Recordset
As you saw earlier in Figure 19-14, getting the data from the recordset displayed by the appropriate trol is the job of the DoDataExchange()function in the CProductViewclass The m_pSetmember pro-vides a means of accessing the members of the CProductSetobject that contains the fields retrievedfrom the database, so linking the controls to the data members of CProductSetis easy MFC defines arange of DDX_Fieldfunctions at global scope that are specifically for exchanging data between a viewand a recordset In particular, the DDX_FieldText()function has overloaded versions that transfers avariety of types of data between a recordset field and an edit box in a CRecordViewobject The typesthat can be exchanged by the DDX_FieldText()function are:
float double CString COleDateTime COleCurrency
When you call the DDX_FieldText()function you must supply four arguments:
❑ ACDataExchangeobject that determines the direction of data transfer — whether the data is to
be transferred to or from the recordset You just supply the pointer that is passed as the ment to the DoDataExchange()function
argu-❑ The ID of the control that is the source or destination of the data
❑ A reference to a field data member in the CRecordsetobject that is the source or destination ofthe data
❑ A pointer to the CRecordsetobject with which data is to be exchanged
So to implement the transfer of data between the recordset and the control for the Product ID field, insertthe following call of the DDX_FieldText()function in the body of the DoDataExchange()function:DDX_FieldText(pDX, IDC_PRODUCTID, m_pSet->m_ProductID, m_pSet);
The first argument is the pDXargument that is passed to the DoDataExchange()function The secondargument is the ID for the first edit control in the dialog box for the view, the third argument uses them_pSetmember of the CProductViewclass to access the m_ProductIDmember of the recordset object,and the last argument is the pointer to the recordset object
946
Trang 12You can therefore fill out the code for the DoDataExchange()function in the CProductViewclass like this:void CProductView::DoDataExchange(CDataExchange* pDX)
{CRecordView::DoDataExchange(pDX);
DDX_FieldText(pDX, IDC_PRODUCTID, m_pSet->m_ProductID, m_pSet);
DDX_FieldText(pDX, IDC_PRODUCTNAME, m_pSet->m_ProductName, m_pSet);
DDX_FieldText(pDX, IDC_UNITPRICE, m_pSet->m_UnitPrice, m_pSet);
DDX_FieldText(pDX, IDC_UNITSINSTOCK, m_pSet->m_UnitsInStock, m_pSet);
DDX_FieldText(pDX, IDC_CATEGORYID, m_pSet->m_CategoryID, m_pSet);
DDX_FieldText(pDX, IDC_UNITSONORDER, m_pSet->m_UnitsOnOrder, m_pSet);
CProductSet
UnitsinStock
ProductName
ProuctID
m_ProductNamem_ProductID
m_UnitslnStock
Trang 13The recordset class and the record view class cooperate to enable data to be transferred between thedatabase and the controls in the dialog box The CProductSetclass handles transfers between thedatabase and its data members and CProductViewdeals with transfers between the data members ofCProductSetand the controls in the dialog.
Exercising the Example
Believe it or not you can now run the example Just build it in the normal way and then execute it Theapplication should display a window similar to the one shown in Figure 19-20
Sor ting a Recordset
As you saw earlier, the data is retrieved from the database by the recordset, using an SQLSELECTment that is generated by the framework using the GetDefaultSQL()member You can add an ORDER
state-BYclause to the statement generated by setting a value in the m_strSortmember of CProductSet,which is inherited from CRecordSet This causes the output table from the query to be sorted, based onthe string stored in m_strSort You need to set only the m_strSortmember to a string that contains thefield name or names that you want to sort on; the framework provides the ORDER BYkeywords Whereyou have multiple names, you separate them by commas But where should you add the code to do this?
948
Trang 14The transfer of data between the database and the recordset occurs when the Open()member of therecordset object is called In your program, the Open()function member of the recordset object is called
by the OnInitialUpdate()member of the base class to the view class, CRecordView You can, therefore,put the code for setting the sort specification in the OnInitialUpdate()member of the CProductViewclass, as follows:
void CProductView::OnInitialUpdate(){
m_pSet = &GetDocument()->m_productSet;
m_pSet->m_strSort = “[CategoryID],[ProductID]”; // Set the sort fieldsCRecordView::OnInitialUpdate();
}
You just set m_strSortin the recordset to a string containing the name of the category ID field followed
by the name of the product IDfield Square brackets are useful, even when there are no blanks in aname, because they differentiate strings containing these names from other strings, so you can immedi-ately pick out the field names They are, of course, optional if there are no blanks in the field name
Modifying the Window Caption
There is one other thing you could add to this function at this point The caption for the window would
be better if it showed the name of the table being displayed You can arrange for this to happen byadding code to set the title in the document object:
void CProductView::OnInitialUpdate(){
m_pSet = &GetDocument()->m_productSet;
m_pSet->m_strSort = “[CategoryID],[ProductID]”; // Set the sort fieldsCRecordView::OnInitialUpdate();
// Set the document title to the table name
if (m_pSet->IsOpen()) // Verify the recordset is open{
CString strTitle = _T(“Table Name”); // Set basic title stringCString strTable = m_pSet->GetTableName();
if(!strTable.IsEmpty()) // Verify we have a table namestrTitle += _T(“: “) + strTable; // and add to basic titleGetDocument()->SetTitle(strTitle); // Set the document title}
}
After checking that the recordset is indeed open, you initialize a local CStringobject with a basic titlestring You then get the name of the table from the recordset object by calling its GetTableName()mem-ber In general, you should check that you do get a string returned from the GetTableName()function.Various conditions can arise that can prevent a table name from being set — for instance, there may bemore than one table involved in the recordset After appending a colon followed by the table name youhave retrieved to the basic title in strTitle, you set the result as the document title by calling the docu-ment’s SetTitle()member
Trang 15If you rebuild the application and run it again, it works as before but with a new window caption asFigure 19-21 shows The product IDs are in ascending sequence within each category ID, with the cate-gory IDs in sequence, too.
Figure 19-21
Using a Second Recordset Object
Now that you can view all the products in the database, a reasonable extension of the program would be
to add the ability to view all the orders for any particular product To do this, you’ll add another set class to handle order information from the database and a complementary view class to display some
record-of the fields from the recordset You’ll also add a button to the Products dialog to enable you to switch tothe Orders dialog when you want to view the orders for the current product This enables you to operatewith the arrangement shown in Figure 19-22
The Products dialog box is the starting position where you can step backwards and forwards through allthe available products Clicking the Show Orders button switches you to the dialog where you can viewall the orders for the current product You can return to the Products dialog box by clicking the ShowProducts button
Adding a Recordset Class
You can start by adding the recordset class for the orders; right-click DBSamplein Class View and select Add > Classfrom the pop-up Select MFC from the set of Visual C++ categories and MFC ODBCConsumer as the template When you click the Addbutton in the Add Class dialog box that is displayed,you’ll see the MFC ODBC Consumer Wizard dialog box shown in Figure 19-23
950
Trang 16You’ll select two tables to associate with the new recordset class that you’re going to create, so hold theCtrlkey down and select the Orders and Order Details table names You can then click the OKbutton tocomplete the selection process This returns you to the MFC ODBC Consumer dialog boxwhere you’llsee the class name and file names have been entered You can change the class name to COrderSetandthe file names in a corresponding way, as shown in Figure 19-25.
Clicking on the Finishbutton completes the process and causes the COrderSetclass to generate
EditProduct ID
EditUnit Price
EditProduct Name
EditCategory ID
Pressing the Show Orders buttonwill open the Orders dialogfor the current product
Pressing the Show Productsbutton will return tothe Products dialog
Units In Stock
Sample edUnits On Order
This dialog will allow you
to step through theproducts available
This dialog will allow you
to step through all theorders for a given product
EditOrder ID
EditQuantity
EditProduct ID
EditCustomer ID
Show Products
Trang 17Figure 19-23
Figure 19-24
952
Trang 18Figure 19-25
As you saw in the CProductSetclass that was created as part of the initial project, the implementation
of the GetDefaultConnect()function for the COrderSetclass is preceded by an #errordirective thatprevents compilation from succeeding, so comment that out
A data member has been created in the COrderSetclass for every field in each of the tables When youselect two or more tables for a given recordset, it is always possible, indeed likely, that there are fieldnames duplicated; the OrderIDfield appears in both tables, for example To ensure the names corre-sponding to field data members are always differentiated, the field names are prefixed with the tablename in each case If you don’t want to keep all these fields, you can delete or comment out any of them,but as I said earlier, you must take care not to delete any variables that are primary keys When you delete
a data member for a table field, you must also delete the initialization for it in the class constructor andthe RFX_()call for it in the DoFieldExchange()member function You must also change the initialvalue for the m_nFieldsmember in the COrderSetconstructor so that it reflects the number of fields left
in the class The data members that you need to keep for this example are as follows: m_OrdersOrderID,m_OrderDetailsOrderID, m_OrderDetailsProductID, m_OrderDetailsQuantity,and
m_OrdersCustomerID If you keep just these you should change the value assigned to m_nFieldsto 5.Change the members of type CStringWto type CString
To hook the new recordset to the document, you need to add a data member to the definition of theCDBSampleDocclass, so right-click the class name in Class View and select Add Member Variable fromthe pop-up Specify the type as COrderSetand the variable name as m_OrderSet You can leave it as apublicmember of the class Click OK to finish adding the data member to the document The compilerhas to understand that COrderSetis a class before it begins compiling the CBSampleDocclass If youtake a look at the contents of the DBSampleDoc.hheader file, you will see that an #includestatementhas already been added for you to the top of DBSampleDoc.h:
Trang 19Adding a View Class for the Recordset
At this point you might expect to be adding a class derived from CRecordViewusing the Add > Classmenu item in the Class View context menu to display the data from the COrderSetobject This used
to be possible in earlier versions of Visual C++, but unfortunately the Visual C++ 2005 product does not provide for this The dialog box for adding a new class does not allow the possibility of selectingCRecordViewas a base class at all, so you must always create classes that have CRecordViewas a basemanually
You need to create another dialog resource before you create the view class so you have the ID for theresource that you can use in the definition of the view class
Creating the Dialog Resource
Switch to Resource View, right-click the Dialog folder, and select Insert Dialog from the context menu.You can delete both of the default buttons from the dialog Now change the ID and styles for the dialog,
so right-click it and display its properties by selecting Properties from the pop-up Change the ID erty to IDD_ORDERS_FORM You also need to change the Styleproperty to Childand the Borderprop-erty to None
prop-You’re now ready to populate the dialog box with controls for the fields that you want to display fromthe Orders and Order Details tables If you switch to Class View and select the COrderSetclass name,you’ll be able to see the names of the variables concerned while you are working on the dialog Add con-trols to the dialog box as shown in Figure 19-26
Figure 19-26
Here, there are four edit controls for the OrderID, CustomerID, ProductID, and Quantityfields fromthe tables associated with the COrderSetclass, together with static controls to identify them You canadd controls to display a few more fields if you want, as long as you haven’t deleted the class members.Don’t forget to modify the IDs for the edit controls so that they are representative of the purpose of thecontrol You can use the table field names prefixed by the table name to match the data member names.Finally, you need to make the edit controls read-only by setting the Read Onlyproperty to Truefor eachcontrol Alternatively, you can set them all to read-only in one go by selecting each of them with the Ctrlkey held down and then setting the Read Onlyproperty to True
954
Trang 20The button control labeled Show Productsis used to return to the Products table view, so modify the
ID for this button to IDC_PRODUCTS When you have arranged everything to your liking, save the dialogresource
Creating the Record View Class
Create the OrderView.hfile that holds the COrderViewclass definition To do this just right-clickDBSamplein Solution Explorer and select Add >| New Itemfrom the list Choose the template to create a hfile and enter the file name as OrderView After you have created the file, you can add thecode for the class definition as:
#pragma onceclass COrderSet; // Declare the class nameclass CDBSampleDoc; // Declare the class name// COrderView form view
class COrderView : public CRecordView{
DECLARE_DYNCREATE(COrderView)protected:
return reinterpret_cast<CDBSampleDoc*>(m_pDocument);
}COrderSet* GetRecordset();
virtual CRecordset* OnGetRecordset();
COrderView(); // constructor now public
#ifdef _DEBUGvirtual void AssertValid() const;
virtual void Dump(CDumpContext& dc) const;
#endif};
This code is based on the CProductViewthat was generated The DECLARE_DYNCREATEmacro enablesobjects of this class type to be created by the MFC framework at runtime In general MFC document,view, and frame classes should define this macro You will add the complementary IMPLEMENT_
DYNCREATEmacro to the cppfile a little later I have omitted the debug version of the GetDocument()because the CProductViewclass contains a version of the function that validates the document object
Trang 21The inline version in the COrderViewclass definition just assumes the cast to CDBSampleDoc*will be
OK I have included declarations for AssertValid()and Dump()that is compiled only when debugmode is in effect, so definition has to be included in the cppfile for the class The enumeration definesthe ID for the dialog and you will use this in the definition of the constructor The m_pSetmember willhold the address of the recordset object that supplies the data displayed by this view
The implementation of the COrderViewclass goes in the OrderView.cppfile, so create that file withinthe project using the procedure you followed for the hfile You can add the initial #includedirectivefor the classes you will need to reference:
Trang 22void COrderView::DoDataExchange(CDataExchange* pDX){
There are two functions to be defined that are involved in retrieving the recordset You’ll call theGetRecordset()function to obtain a pointer to the COrderSetobject encapsulating the recordset Youcan implement this as follows:
COrderSet* COrderView::GetRecordset(){
pro-The OnGetRecordset()function is a pure virtual function in the base class, so you must define it here.You can implement it as
CRecordset* COrderView::OnGetRecordset(){
Customizing the Recordset
As it stands, the SQLSELECToperation for a COrderSetobject produces a table that will contain allcombinations of records from the two tables involved This could be a lot of records, so you must add
Trang 23the equivalent of a WHEREclause to the query to restrict the records selected to those that make sense Butthere is another problem, too: when you switch from the Productstable display, you don’t want to look
at just any old orders You want to see precisely those orders for the product ID we were looking at, whichamounts to selecting only those orders that have the same product ID as that contained in the currentCProductSetrecord This is also effected through a WHEREclause In the MFC context, the WHEREclausefor a SQLSELECToperation for a recordset is called a filter.
Adding a Filter to the Recordset
You add a filter to the query by assigning a string to the m_strFiltermember of the recordset object.This member is inherited from the base class, CRecordSet As with the ORDER BYclause, which youadded by assigning a value to the m_strSortmember of the recordset, the place to implement this is inthe OnInitialUpdate()member of the record view class, just before the base class function is called.You want to set two conditions in the filter One is to restrict the records generated in the recordset tothose where the OrderIDfield in the Orderstable is equal to the field with the same name in the OrderDetailstable You can write this condition as:
[Orders].[OrderID] = [Order Details].[OrderID]
The other condition you want to apply is that, for the records meeting the first condition, you want onlythose with a ProductIDfield that is equal to the ProductIDfield in the current record in the recordsetobject displaying the Productstable This means that you need to have the ProductIDfield from theCOrderSetobject compared to a variable value The variable in this operation is called a parameter, and
the condition in the filter is written in a special way:
ProductID = ?
The question mark represents a parameter value for the filter, and the selected records are those wherethe ProductIDfield equals the parameter value The value that is to replace the question mark is set inthe DoFieldExchange()member of the recordset You’ll implement this in a moment, but first you’llcomplete the specification of the filter
You can define the string for the filter variable that incorporates both the conditions that you need withthe statement:
// Set the filter as Product ID field with equal Order IDs
m_pSet->m_strFilter =
“[ProductID] = ? AND [Orders].[OrderID] = [Order Details].[OrderID]”;
You’ll incorporate this into the OnInitialUpdate()member of the COrderViewclass, but before that,you’ll finish setting the parameter for the filter
Defining the Filter Parameter
Add a data member to the COrderSetclass to store the current value of the ProductIDfield from the CProductSetobject This member also actS as the parameter to substitute for the ?in the filter forthe COrderSetobject So, right-click the COrderSetclass name in Class View and select Add > AddVariablefrom the pop-up The variable type needs to be the same as that of the m_ProductIDmember
of the CProductSetclass, which is type long, and you can specify the name as m_ProductIDparam
958
Trang 24You can also leave it as a publicmember You need to initialize this data member in the constructor andalso set the parameter count The application framework requires the count of the number of parameters
in your recordset to be set to reflect the number of parameters you are using; otherwise, it won’t workcorrectly Add the shaded code shown below to the COrderSetconstructor definition:
COrderSet::COrderSet(CDatabase* pdb) : CRecordset(pdb){
}
All of the unshaded code was supplied by Class wizard to initialize the data members corresponding tothe fields in the recordset and to specify the type as snapshot You should delete the initialization forthe other fields in the recordset The new code initializes the parameter to zero and sets the count of thenumber of parameters to 1 The m_nParamsvariable is inherited from the base class, CRecordSet.Because there is a parameter count, you can deduce that you can have more than one parameter in thefilter for a recordset
At this point you can also remove or comment out the members from the COrderSet class that storefields from the recordset that you won’t need Remove or comment out the fields that are not requiredfrom the class definition, just leaving the following:
long m_OrderDetailsOrderID; // Same as Order ID in Orders table
long m_OrderDetailsProductID; // Same as Product ID in Products table
int m_OrderDetailsQuantity;
long m_OrdersOrderID; // Unique order number
CString m_OrdersCustomerID; // Same entry as in Customers table
long m_ProductIDparam;
To identify the m_ProductIDparamvariable in the class as a parameter to be substituted in the filter forthe COrderSetobject, you must also add some code to the DoFieldExchange()member of the class:void COrderSet::DoFieldExchange(CFieldExchange* pFX)
{pFX->SetFieldType(CFieldExchange::outputColumn);
RFX_Long(pFX, _T(“[Order Details].[OrderID]”), m_OrderDetailsOrderID);
RFX_Long(pFX, _T(“[Order Details].[ProductID]”),
m_OrderDetailsProductID);
RFX_Int(pFX, _T(“[Order Details].[Quantity]”), m_OrderDetailsQuantity);
RFX_Long(pFX, _T(“[Orders].[OrderID]”), m_OrdersOrderID);
RFX_Text(pFX, _T(“[Orders].[CustomerID]”), m_OrdersCustomerID);
// Set the field type as parameterpFX->SetFieldType(CFieldExchange::param);
RFX_Long(pFX,_T(“ProductIDParam”), m_ProductIDparam);
}
Trang 25The Class wizard provided code to transfer data between the database and the field variables it hasadded to the class There will be one RFX_()function call for each data member of the recordset Youcan delete those that are not required in this application, leaving just those shown in the preceding code.The first new line of code calls the SetFieldType()member of the pFXobject to set the mode for thefollowing RFX_()call to param The effect of this is to cause the third argument in any succeedingRFX_()calls to be interpreted as a parameter that is to replace a ?in the filter for the recordset If youhave more than one parameter, the parameters substitute for the question marks in the m_strFilterstring in sequence from left to right, so it’s important to ensure that the RFX_()calls are in the rightorder when there are several With the mode set to param, the second argument in the RFX_()call isignored, so you could put NULLhere or some other string if you want.
Initializing the Record View
You can now implement the override for the OnInitialUpdate()function in the COrderViewclass.This function is called by the MFC framework before the view is initially displayed, so you can put code
in this function to do any one-time initialization that you need In this case you will specify the filter forthe recordset Here’s the definition of the function to do this:
void COrderView::OnInitialUpdate()
{
BeginWaitCursor();
CDBSampleDoc* pDoc = static_cast<CDBSampleDoc*>(GetDocument());
m_pSet = &pDoc->m_OrderSet; // Get a pointer to the recordset// Use the DB that is open for products recordset
imple-The BeginWaitCursor()call at the start of the OnInitialUpdate()function displays the hourglass sor while this function is executing The reason for this is that this function can take an appreciable time toexecute, especially when multiple tables are involved The processing of the query and the transfer of data
cur-to the recordset all takes place in here The cursor is returned cur-to normal by the EndWaitCursor()call atthe end of the function
960
Trang 26The first thing that the code does is to set the m_pDatabasemember of the COrderSetobject to thesame as that for the CProductSetobject If you don’t do this, the framework re-opens the databasewhen the orders recordset is opened Because the database has already been opened for the productsrecordset, this wastes a lot of time.
Next, you set the value for the m_ProductIDparamparameter variable to the current value stored in the m_ProductIDmember of the products recordset This value replaces the question mark in the filterwhen the orders recordset is opened, so select the records you want; then set the filter for the ordersrecordset to the string you saw earlier
Accessing Multiple Table Views
Because you have implemented the program with the single document interface, the application has onedocument and one view The availability of just one view might appear to be a problem, but it isn’t inpractice You can arrange for the frame window object in the application to create an instance of theCOrderViewclass and switch the current window to that when the orders recordset is to be displayed.You’ll need to keep track of what the current window is, which you can do by assigning a unique ID toeach of the record view windows in the application At the moment there are two views: the productsview and the orders view To define IDs for these, create a new file called OurConstants.hand add thefollowing code to it:
// Definitions for our constants
#pragma once// Arbitrary constants to identify record viewsconst unsigned int PRODUCT_VIEW = 1;
const unsigned int ORDER_VIEW = 2;
You can now use one of these constants to identify each view and to record the ID of the current view inthe frame window object To record the current view ID, add a publicdata member to the CMainFrameclass of type unsigned intand give it the name m_CurrentViewID After you have done that, you caninitialize it in the constructor for CMainFrameby adding code as follows:
CMainFrame::CMainFrame() : m_CurrentViewID(PRODUCT_VIEW){
Trang 27Right-click CMainFrameand select Add > Add Functionfrom the pop-up to add a new public member
to the class You can enter the return type as voidand the function name as SelectView The parametername can be ViewID,and the type is unsigned int You can implement the function as follows:void CMainFrame::SelectView(unsigned int 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 1st time around for the new view,
// the new view won’t exist, so we must create it
if (pNewActiveView == NULL)
{
switch(ViewID){
case ORDER_VIEW: // Create an Order viewpNewActiveView = new COrderView;
context.m_pCurrentDoc = pOldActiveView->GetDocument();
pNewActiveView->Create(NULL, NULL, 0L, CFrameWnd::rectDefault,
this, ViewID, &context);
pNewActiveView->OnInitialUpdate();
}
SetActiveView(pNewActiveView); // Activate the new view
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 operation of the function falls into three distinct parts:
1. Getting pointers to the current view and the new view
2. Creating the new view if it doesn’t exist
3. Swapping to the new view in place of the current view
The address of the current active view is supplied by the GetActiveView()member of the CMainFrameobject To get a pointer to the new view, you call the GetDlgItem()member of the frame window
962
Trang 28object If a view with the ID specified in the argument to the function exists, it returns the address of theview; otherwise, it returns NULL,and you need to create the new view.
After creating a view object, you define a CCreateContextobject, context ACCreateContextobject
is necessary only when you are creating a window for a view that is to be connected to a document ACCreateContextobject contains data members that can tie together a document, a frame window, and
a view, and for MDI applications, a document template as well When you switch between views, youwill create a new window for the new view to be displayed Each time you create a new view window,you use the CCreateContextobject to establish a connection between the view and your documentobject You need to store a pointer to the document object only in the m_pCurrentDocmember on context In general, you may need to store additional data in the CCreateContextobject before youcreate the view; it depends on the circumstances and what kind of window you are creating
In the call to the Create()member of the view object that creates the window for the new view, youpass the contextobject as an argument This establishes a proper relationship with your document andvalidates the document pointer The argument thisin the call to Create()specifies the current frame
as the parent window, and the ViewIDargument specifies the ID of the window This ID enables theaddress of the window to be obtained with a subsequent call to the GetDlgItem()member of the par-ent window
To make the new view the active view, you call the SetActiveView()member of CMainFrame The newview then replaces the current active view To remove the old view window, you call the ShowWindow()member of the view with the argument SW_HIDEusing the pointer to the old view To display the newview window, you call the same function with the argument SW_SHOWusing the pointer to the new view SetActiveView(pNewActiveView); // Activate the new view
pOldActiveView->ShowWindow(SW_HIDE); // Hide the old viewpNewActiveView->ShowWindow(SW_SHOW); // Show the new viewpOldActiveView->SetDlgCtrlID(m_CurrentViewID); // Set the old view IDpNewActiveView->SetDlgCtrlID(AFX_IDW_PANE_FIRST);
m_CurrentViewID = ViewID; // Save the new view ID
You restore the ID of the old active view to the ID value that you have defined for it in them_CurrentViewIDmember of the CMainFrameclass that you added earlier You also set the ID of thenew view to AFX_IDW_PANE_FIRSTto identify it as the first window for the application This is neces-sary because the application has only one view, so the first view is the only view Lastly, you save the
ID for the new window in the m_CurrentViewIDmember, so it’s available the next time the currentview is replaced The call to RecalculateLayout()causes the view to be redrawn when the new view
is selected
You must add a #includedirective for the OrderView.hfile to beginning of the MainFrm.cppfile sothat the COrderViewclass definition is available here After you save MainFrm.cpp, you can move on toadding a button control to the Products dialog to link to the Orders dialog You’ll then be able to addhandlers for this button and its partner on the Orders dialog to call the SelectView()member ofCMainFrame
Enabling the Switching Operation
To implement the view switching mechanism, go back to Resource View and open the IDD_DBSAMPLE_FORMdialog You need to add a button control to the dialog box, as shown in Figure 19-27
Trang 29You must add the following #includedirectives to the beginning of the ProductView.cppfile:
#include “OurConstants.h”
#include “MainFrm.h”
The next task is to add the handler for the button that you previously placed on the IDD_ORDERS_FORMdialog From the Editor window showing this dialog box, use the same process to add the OnProducts()handler to the COrderViewclass and add a single line of code to its implementation:
964
Trang 30Handling View Activation
When you switch to a view that already exists, you need to ensure that the recordset is refreshed and thatthe dialog is re-initialized so that the correct information is displayed When an existing view is activated
or deactivated, the framework calls the OnActivateView()member of the class, so this is a good place totake care of refreshing the recordset and the dialog box You can override this function in each of the viewclasses You can do this by selecting the Overridesbutton in the Propertieswindow for a view classand selecting OnActivateViewfrom the list Make sure you add the override to both view classes
You can add the following code to complete the implementation of the OnActivateView()functionoverride for the COrderViewclass:
void COrderView::OnActivateView(BOOL bActivate,
CView* pActivateView, CView* pDeactiveView){
if(bActivate){
// Get a pointer to the documentCDBSampleDoc* pDoc = GetDocument();
// Get a pointer to the frame windowCMainFrame* pMFrame = static_cast<CMainFrame*>(GetParentFrame());
// If the last view was the product view, we must re-query// the recordset with the product ID from the product recordsetif(pMFrame->m_CurrentViewID==PRODUCT_VIEW)
{if(!m_pSet->IsOpen()) // Make sure the recordset is openreturn;
// Set current product ID as parameterm_pSet->m_ProductIDparam = pDoc->m_DBSampleSet.m_ProductID;
m_pSet->Requery(); // Get data from the DB// If we are past the EOF there are no recordsif(m_pSet->IsEOF())
AfxMessageBox(L”No orders for the current product ID”);
}// Set the window captionCString strTitle = _T(“Table Name: “);
CString strTable = m_pSet->GetTableName();
if(!strTable.IsEmpty())strTitle += strTable;
elsestrTitle += _T(“Orders - Multiple Tables”);
Trang 31argu-present because the previous view is always the Product View, but when you add another view to theapplication, this will not always be true so you might as well put the code in now.
To re-query the database, you set the parameter member of COrderSet, m_ProductIDparam, to the rent value of the m_ProductIDmember of the product recordset This causes the orders for the currentproduct to be selected You don’t need to set the m_strFiltermember of the recordset here becausethat was set in the OnInitialUpdate()function when the CRecordViewobject was first created TheIsEOF()function member of the COrderSetobject is inherited from CRecordSetand returns TRUEifthe recordset is empty when it is re-queried
cur-Ad the code for OnActivateView()function in the CProductViewclass as follows:
void CProductView::OnActivateView(BOOL bActivate,
CView* pActivateView, CView* pDeactiveView){
if(bActivate){
// Update the window captionCString strTitle = _T(“Table Name”);
CString strTable = m_pSet->GetTableName();
strTitle += _T(“: “) + strTable;
GetDocument()->SetTitle(strTitle);
}CRecordView::OnActivateView(bActivate, pActivateView, pDeactiveView);
Viewing Orders for a Product
You are now ready to try to build the executable module for the new version of the example When yourun the example, you should be able to see the orders for any product just by clicking the Orders button
on the products dialog box A typical view of an order is shown in Figure 19-28
Figure 19-28
966
Trang 32Clicking the Show Productsbutton returns you to the products dialog box, so you can browse furtherthrough the products In this dialog, you can use the toolbar buttons to browse all the orders for the cur-rent product The Customer IDis a bit cryptic You could add one more view to display the details ofthe customer’s name and address It won’t be too difficult, because you have built already the mecha-nism to switch between views.
V iewing Customer DetailsThe basic mechanism that you’ll add will work through another button control on the order dialog thatswitches to a new dialog for customer data As well as controls to display customer data, you’ll add two buttons to the customer dialog box: one to return to the Order View, and the other to return to theProduct View You’ll need another view ID corresponding to the customer view, which you can add withthe following line in the OurConstants.hfile:
const unsigned int CUSTOMER_VIEW = 3;
Now you’ll add the recordset for the customer details
Adding the Customer Recordset
The process is exactly the same as you followed for the COrderSetclass Use the Add >Classmenuitem from the context menu in Class View and use the MFC ODBC Consumer template to define theCCustomerSetclass, with CRecordsetspecified as the base class Select the database as Northwind asbefore and select the Customerstable for the recordset You can select snapshot as the type of access tothe table The class should then be created with the data members shown as follows:
You will find an #includedirective for CustomerSet.hthat has already been added to DBSampleDoc.h.After saving all the files you have modified, you can create the customer dialog resource
Trang 33Creating the Customer Dialog Resource
This process is also exactly the same as the one you went through for the orders dialog Change toResource View and create a new dialog resource with the ID IDD_CUSTOMER_FORM, not forgetting to setthe style to Childand the border to Nonein the Propertiesbox for the dialog After deleting the defaultbuttons, add controls to the dialog box to correspond to the field names for the Customerstable, asshown in Figure 19-29:
Creating the Customer View Class
You’ll create the view class for the customer recordset manually, just as you did for the COrderViewclass Add CustomerView.hand CustomerView.cppfiles to the project and insert the following code
to define the class in the CustomerView.hfile:
// CCustomerView record view
Trang 34enum { IDD = IDD_CUSTOMER_FORM };
virtual ~CCustomerView(){}
#ifdef _DEBUGvirtual void AssertValid() const;
virtual void Dump(CDumpContext& dc) const;
#endif};
The class has essentially the same members as the COrderViewclass
You can add the following initial code to the CustomerView.cpp:// CCustomerView implementation
ASSERT(m_pSet != NULL);
return m_pSet;
}CRecordset* CCustomerView::OnGetRecordset(){
return m_pSet;
Trang 35You can process the button controls in the IDD_CUSTOMER_FORMdialog box in the same way as you did previously to add the OnOrders()and OnProducts()functions to the CCustomerViewclass; right-click each button and select the Add Event Handler from the pop-up The code for these is similar
to the corresponding functions in the other views The code you need to add to OnOrders()in theCustomerView.cppfile is:
Trang 36m_pSet = &pDoc->m_CustomerSet; // Initialize the recordset pointer// Set the DB for the customer recordset
CString strTitle = m_pSet->m_pDatabase->GetDatabaseName();
CString strTable = m_pSet->GetTableName();
if(!strTable.IsEmpty())strTitle += _T(“:”) + strTable;
GetDocument()->SetTitle(strTitle);
}EndWaitCursor();
}
After getting a pointer to the document, you store the address of the CCustomerSetobject member ofthe document in the m_pSetmember of the view You know the database is already open, so you can setthe database pointer in the customer recordset to that stored in the CProductSetobject
The parameter for the filter will be defined in the m_CustomerIDparammember of CCustomerSet.You’ll add this member to the class in a moment It’s set to the current value of the m_CustomerIDmem-ber of the COrderSetobject owned by the document You will define the filter in such a way that thecustomer recordset contains only the record with the same customer ID as that in the current order.The OnActivateView()function handles activation of the customer view, and you can implement it inCustomerView.cppas follows:
void CCustomerView::OnActivateView(BOOL bActivate,
CView* pActivateView, CView* pDeactiveView){
if(bActivate){
if(!m_pSet->IsOpen())return;
CDBSampleDoc* pDoc = static_cast<CDBSampleDoc*>(GetDocument());
// Set current customer ID as parameterm_pSet->m_CustomerIDparam = pDoc->m_OrderSet.m_OrdersCustomerID;
m_pSet->Requery(); // Get data from the DBCRecordView::OnInitialUpdate(); // Redraw the dialog// Check for empty recordset
if(m_pSet->IsEOF())AfxMessageBox(L”No customer details for the current customer ID”);
CString strTitle = _T(“Table Name:”);
CString strTable = m_pSet->GetTableName();
Trang 37if(!strTable.IsEmpty())strTitle += strTable;
elsestrTitle += _T(“Multiple Tables”);
The m_CustomerIDparammember for the CCustomerSetrecordset object that is associated with thisview object is set to the customer ID from the orders recordset object that is stored in the document Thiswill be the customer ID for the current order The call to the Requery()function for the CCustomerSetobject retrieves records from the database using the filter you have set up The result are that the detailsfor the customer for the current order are stored in the CCustomerSetobject and then passed to theCCustomerViewobject for display in the dialog
You will need to add the following #includestatements to the beginning of the CustomerView.cppfile:
Implementing the Filter Parameter
Add a publicvariable of type CStringto the CCustomerSetclass to correspond with the type of them_CustomerIDmember of the recordset and give it the name m_CustomerIDparam If you used the Add
> Add Variable mechanism from Class View to do this, the new member will already be initialized in theconstructor; otherwise, add the initialization as in the code that follows You can set the parameter count
in the CCustomerSetconstructor as follows:
Trang 38RFX_Text(pFX, _T(“[CustomerID]”), m_CustomerID);
RFX_Text(pFX, _T(“[CompanyName]”), m_CompanyName);
RFX_Text(pFX, _T(“[ContactName]”), m_ContactName);
RFX_Text(pFX, _T(“[ContactTitle]”), m_ContactTitle);
RFX_Text(pFX, _T(“[Address]”), m_Address);
RFX_Text(pFX, _T(“[City]”), m_City);
RFX_Text(pFX, _T(“[Region]”), m_Region);
RFX_Text(pFX, _T(“[PostalCode]”), m_PostalCode);
RFX_Text(pFX, _T(“[Country]”), m_Country);
RFX_Text(pFX, _T(“[Phone]”), m_Phone);
RFX_Text(pFX, _T(“[Fax]”), m_Fax);
pFX->SetFieldType(CFieldExchange::param); // Set parameter modeRFX_Text(pFX, _T(“CustomerIDParam”), m_CustomerIDparam);
}
I have omitted comment lines from the beginning of this function to save space After setting the parammode by calling the SetFieldType()member of the pFXobject, you call the RFX_Text()function topass the parameter value for substitution in the filter You use RFX_Text()because the parameter vari-able is of type CString There are various RFX_()functions supporting a range of parameter types.After you have completed this modification, you can save the CustomerSet.cppfile
Linking the Order Dialog to the Customer Dialog
To permit a switch to the customer dialog, you require a button control on the IDD_ORDERS_FORMlog, so open it in Resource View and add an extra button, as shown in Figure 19-30
Trang 39dia-Figure 19-30
I have rearranged the original controls a little — you can arrange them to please yourself You can definethe ID for the new button control as IDC_CUSTOMER After you save the dialog, you can add a handlerfor the button by right-clicking it and select Add Event Handlerfrom the pop-up The handler requiresonly one line of code to be added to it, as follows:
void COrderView::OnCustomer()
{
static_cast<CMainFrame*>(GetParentFrame())->SelectView(CUSTOMER_VIEW);
}
This obtains the address of the frame window and uses it to call the SelectView()member of
CMainFrameto switch to a customer view The penultimate step to complete the program is to add thecode to the SelectView()function that deals with the CUSTOMER_VIEWvalue being passed to it Thisrequires just three additional lines of code, as follows:
void CMainFrame::SelectView(UINT 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 1st time around for the new view,
// the new view won’t exist, so we must create it
if (pNewActiveView == NULL)
{
switch(ViewID){
case ORDER_VIEW: // Create an Order viewpNewActiveView = new COrderView;
Trang 40}// Switching the views// Obtain the current view context to apply to the new viewCCreateContext context;
context.m_pCurrentDoc = pOldActiveView->GetDocument();
pNewActiveView->Create(NULL, NULL, 0L, CFrameWnd::rectDefault,
this, ViewID, &context);
pNewActiveView->OnInitialUpdate();
}SetActiveView(pNewActiveView); // Activate the new viewpOldActiveView->ShowWindow(SW_HIDE); // Hide the old viewpNewActiveView->ShowWindow(SW_SHOW); // Show the new viewpOldActiveView->SetDlgCtrlID(m_CurrentViewID); // Set the old view IDpNewActiveView->SetDlgCtrlID(AFX_IDW_PANE_FIRST);
m_CurrentViewID = ViewID; // Save the new view IDRecalcLayout();
}
The only change necessary is the addition of a casestatement in the switchto create a CCustomerViewobject when one doesn’t exist Each view object will be re-used next time around, so they get createdonly once The code to switch between views works with any number of views, so if you want this func-tion to handle more views, you just need to add another casein the switchfor each new view that youwant Although you are creating view objects dynamically here, you don’t need to worry about deletingthem Because they are associated with a document object, they are deleted by the framework when theapplication closes
Because you reference the CCustomerViewclass in the SelectView()function, you must add an
#includestatement for the CustomerView.hfile to the block at the beginning of MainFrm.cpp
To complete the application you can add the implementation of the DoDataExachange()function forthe CCustomerViewclass to CustomerView.cpp:
void CCustomerView::DoDataExchange(CDataExchange* pDX){