Tài liệu hạn chế xem trước, để xem đầy đủ mời bạn chọn Tải xuống
1
/ 122 trang
THÔNG TIN TÀI LIỆU
Thông tin cơ bản
Định dạng
Số trang
122
Dung lượng
2,1 MB
Nội dung
your 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 origi- nal record. A dynaset recordset uses an index to the database tables involved to generate the contents of each record dynamically. Because you have no other users accessing the Northwind database, you can choose the Snapshot option for your example. After Snapshot has been chosen, you can click the Generated Classes option to display the classes in your 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 CDBSampleView and CProductView classes and the corresponding changes to the names of the .h and .cpp files for the class, you could also change the CDBSampleSet class name to CProductSet, and the associated .h and .cpp file names to be consistent with the class name. After that is done, click Finish and 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, and view objects. This is done in a standard manner in the InitInstance() member of the application object. 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 CProductSet class type. As a consequence, a recordset object is automatically created when the document object is created in the InitInstance() function member of the application object. The significant departures from a non-database program arise in the detail of the CRecordset class, and in the CRecordView class, so take a look at those. 936 Chapter 19 22_571974 ch19.qxp 1/20/06 11:34 PM Page 936 Understanding Recordsets You can look at the definition of the CProductSet class that the Application wizard has generated piece- meal and see how each piece works. I’ll show the bits under discussion as shaded in the code fragments. 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 overrides public: virtual CString GetDefaultConnect(); // Default connection string virtual CString GetDefaultSQL(); // default SQL for Recordset virtual void DoFieldExchange(CFieldExchange* pFX);// RFX support // Plus some more standard stuff }; The class has CRecordset as a base class and provides the functionality for retrieving data from the database. The constructor for the class accepts a pointer to a CDatabase object that is set to NULL as a default. The parameter to the constructor allows a CProductSet object to be created for a CDatabase object 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 connection when you can. If no pointer is passed to the constructor, as is the case for the m_DBSampleSet member of the document class CDBSampleDoc, the framework automatically creates a CDatabase object for you and calls the GetDefaultConnect() function member of CProductSet to define the connection. The Application wizard provides the following implementation of this function: CString CProductSet::GetDefaultConnect() { return _T( “DSN=Northwind; DBQ=D:\\Beg Visual C++ 2005\\Model Access DB\\Northwind.mdb; DriverId=25; FIL=MS Access; MaxBufferSize=2048; PageTimeout=5; UID=admin;”); } 937 Connecting to Data Sources 22_571974 ch19.qxp 1/20/06 11:34 PM Page 937 The GetDefaultConnect() function is a pure virtual function in the base class, CRecordset, and so must 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 of the string more apparent. The implementation provided by Application wizard returns the text string shown to the framework. This identifies the database with its name and path plus values for the other parameters you can see and enables the framework to create a CDatabase object that establishes the database connection automatically. The meaning of the arguments in the connection string is as follows: Argument Description DSN The data source name. 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. FIL The database file type. 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 failures when 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 the Application 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 the program successfully. You can make the framework pop up a dialog box for the user to select the database name from the list of registered 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 CProductSet class includes a data member for each field in the Products table. The Application wizard 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 Data comment in the CProductSet class definition: 938 Chapter 19 22_571974 ch19.qxp 1/20/06 11:34 PM Page 938 class 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 overrides public: virtual CString GetDefaultConnect(); // Default connection string virtual CString GetDefaultSQL(); // default SQL for Recordset virtual void DoFieldExchange(CFieldExchange* pFX); // RFX support // Implementation #ifdef _DEBUG virtual 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 the Products table. 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 ensure that all references to a field are deleted, too. A further caveat is that you must not delete primary keys. If you do, the recordset won’t work, so you need to be sure which fields are primary keys before chopping out what you don’t want. Note that two of the fields have CStringW as the type. You haven’t seen this before, but the CStringW class type just encapsulates a Unicode string rather than an ASCII string. It is more convenient when 939 Connecting to Data Sources 22_571974 ch19.qxp 1/20/06 11:34 PM Page 939 you are accessing the fields to use type CString, so change the type of the m_ProductName and m_QuantityPerUnit members 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 to maintain any CStringW fields 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 the GetDefaultSQL() function. The implementation that the Application wizard has supplied for this is: CString CProductSet::GetDefaultSQL() { return _T(“[Products]”); } The string returned is obviously created based on the table that you selected during the creation of the project. The square brackets have been included to provide for the possibility of the table name contain- ing spaces. If you had selected several tables in the project creation process, they would all be inserted here, separated by commas, with each table name enclosed within square brackets. 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 skeleton SQL statement with the form: SELECT * FROM < String returned by GetDefaultSQL() >; This looks simplistic, and indeed it is, but you can add WHERE and ORDER BY clauses to the operation, as you’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 CProductSet class. 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 Chapter 19 22_571974 ch19.qxp 1/20/06 11:34 PM Page 940 This function is called automatically by the MFC framework to store data in and retrieve data from the database. It works in a similar fashion to the DoDataExchange() function you have seen with dialog controls in that the pFX parameter determines whether the operation is a read or a write. Each time it’s called, 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 fol- low. In this case, the mode is specified as outputColumn, which indicates that data is to be exchanged between the database field and the corresponding argument specified in each of the following RFX_() function calls. There is a whole range of RFX_() functions for various types of database field. The function call for a particular field corresponds with the data type applicable to that field. The first argument to an RFX_() function call is the pFX object that determines the direction of data movement. The second argument is the 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 win- dow, so you need to understand how this works. The bits of the CProductView class definition that are of primary interest are shown shaded: class CProductView : public CRecordView { protected: // create from serialization only CProductView(); DECLARE_DYNCREATE(CProductView) public: enum{ IDD = IDD_DBSAMPLE_FORM }; CProductSet* m_pSet; // Attributes public: CDBSampleDoc* GetDocument(); // Operations public: // Overrides public: virtual CRecordset* OnGetRecordset(); virtual BOOL PreCreateWindow(CREATESTRUCT& cs); protected: virtual void DoDataExchange(CDataExchange* pDX); // DDX/DDV support virtual void OnInitialUpdate(); // called first time after construct virtual BOOL OnPreparePrinting(CPrintInfo* pInfo); virtual void OnBeginPrinting(CDC* pDC, CPrintInfo* pInfo); virtual void OnEndPrinting(CDC* pDC, CPrintInfo* pInfo); // Implementation public: 941 Connecting to Data Sources 22_571974 ch19.qxp 1/20/06 11:34 PM Page 941 virtual ~CProductView(); #ifdef _DEBUG virtual void AssertValid() const; virtual void Dump(CDumpContext& dc) const; #endif protected: // Generated message map functions protected: DECLARE_MESSAGE_MAP() }; The view class for a recordset always needs to be derived because the class has to be customized to dis- play the particular fields from the recordset that you want. The base class, CRecordView, includes all the functionality required to manage communications with the recordset. All you need to do is to tailor the record view class to suit your application. I’ll get to that in a moment. Note that the constructor is protected. This is because objects of this class are expected to be created from serialization, which is a default assumption for record view classes. When you add further record view classes to the application, you’ll need to change the default access for their constructors to public because you’ll be creating the views yourself. In the first public block in the class, the enumeration adds the ID IDD_DBSAMPLE_FORM as a member the 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 Products table that you want displayed. The dialog ID is passed to the base class, CRecordView, in the initialization list of the con- structor for the view class: CProductView::CProductView() : CRecordView(CProductView::IDD) { m_pSet = NULL; // TODO: add construction code here } This action links the view class to the dialog box, which is necessary to enable the mechanism that trans- fers data between the recordset object and the view object to work. There is also a pointer to a CProductSet object, m_pSet, in the class definition, which is initialized to NULL in the constructor. A more useful value for this pointer is set in the OnInitialUpdate() member of the class, which has been implemented as: void CProductView::OnInitialUpdate() { m_pSet = &GetDocument()->m_DBSampleSet; CRecordView::OnInitialUpdate(); } This function is called when the record view object is created and sets the value of m_pSet to be the address of the m_DBSampleSet member of the document, thus tying the view to the product set object. 942 Chapter 19 22_571974 ch19.qxp 1/20/06 11:34 PM Page 942 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 CProductSet object that correspond to fields in the Products table and the controls in the dialog box associated with the CProductView object is man- aged by the DoDataExchange() member of CProductView. The code in this function to do this is not in place yet because you first need to add the controls to the dialog that are going to display the data and then 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 dialog resources, and double-click Idd_Dbsample_Form. You can delete the static text object with the TODO message from the dialog. If you right-click the dialog box, you can choose to view its properties, as shown in Figure 19-15. If you scroll down through the properties you’ll see that the Style property has been set to Child because the dialog box is going to be a child window and will fill the client area. The Border property has been set to None because 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 an edit control to display it. Database Table DoFieldExchange() member of the Recordset object transfers data between the DB and the recordset DoDataExchange() member of the view object transfers data between the recordset and the view. RecordSet Object View Object 943 Connecting to Data Sources 22_571974 ch19.qxp 1/20/06 11:34 PM Page 943 Figure 19-15 You can enlarge the dialog if necessary by dragging its borders. Then, place controls on the dialog as shown in Figure 19-16. Figure 19-16 944 Chapter 19 22_571974 ch19.qxp 1/20/06 11:34 PM Page 944 You can add the text to each static control by just typing it as soon as the control has been placed on the dialog. 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, so right-click each of them in turn to display and modify their properties. Figure 19-17 shows the properties for 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 have modified it. You can change the IDs for the other edit controls similarly. Because you are not intending to update 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 Only property for of each of the edit controls as True. The background to the edit boxes will then have a different color to signal that they 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 our example 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. 945 Connecting to Data Sources 22_571974 ch19.qxp 1/20/06 11:34 PM Page 945 [...]... entered You can change the class name to COrderSet and the file names in a corresponding way, as shown in Figure 19- 25 Clicking on the Finish button completes the process and causes the COrderSet class to generate 95 1 Chapter 19 Figure 19- 23 Figure 19- 24 95 2 Connecting to Data Sources Figure 19- 25 As you saw in the CProductSet class that was created as part of the initial project, the implementation of... the database and the dialog box owned by the CProductView object is illustrated in Figure 19- 19 ct in me Na ck Sto ID ct du du it s P ro Pr o Un Products Table in Sample Data CProductSet m_ProductID m_ProductName m_UnitslnStock DDX calls in DoDataExchange() RFX calls in DoFieldExchange() Figure 19- 19 947 Chapter 19 The recordset class and the record view class cooperate to enable data to be transferred... the result as the document title by calling the document’s SetTitle() member 94 9 Chapter 19 If you rebuild the application and run it again, it works as before but with a new window caption as Figure 19- 21 shows The product IDs are in ascending sequence within each category ID, with the category IDs in sequence, too Figure 19- 21 Using a Second Recordset Object Now that you can view all the products... derived from CRecordView using the Add > Class menu item in the Class View context menu to display the data from the COrderSet object 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 selecting CRecordView as a base class at all, so you must always create... right-click DBSample in Class View and select Add > Class from the pop-up Select MFC from the set of Visual C++ categories and MFC ODBC Consumer as the template When you click the Add button in the Add Class dialog box that is displayed, you’ll see the MFC ODBC Consumer Wizard dialog box shown in Figure 19- 23 95 0 Connecting to Data Sources Product ID Edit Product Name Edit Units In Stock Edit This dialog... Operation To implement the view switching mechanism, go back to Resource View and open the IDD_DBSAMPLE_ FORM dialog You need to add a button control to the dialog box, as shown in Figure 19- 27 96 3 Chapter 19 Figure 19- 27 You can set the ID for the button to IDC_ORDERS, consistent with the naming for the other controls in the dialog box After saving the resource, you can create a handler for the button... border to None in the Properties box for the dialog After deleting the default buttons, add controls to the dialog box to correspond to the field names for the Customers table, as shown in Figure 19- 29: Figure 19- 29 The two buttons enable you to switch to either the Orders dialog, which is how you got to this dialog, or directly back to the Products dialog The size of the window for the application is determined... the beginning of MainFrm.cpp so that the definition of PRODUCT_VIEW is accessible in the source file Switching Views To enable the view switching mechanism, you add a public function member to the CMainFrame class with the name SelectView() with a parameter specifying a view ID This function switches from the current view to whatever view is specified by the ID passed as an argument 96 1 Chapter 19 Right-click...Chapter 19 Figure 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 control is the job of the DoDataExchange() function in the CProductView class The m_pSet... example When you run 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 96 6 Connecting to Data Sources Clicking the Show Products button returns you to the products dialog box, so you can browse further through the products In this dialog, you can use the toolbar . ID Show Products 95 1 Connecting to Data Sources 22_57 197 4 ch 19. qxp 1/20/06 11:34 PM Page 95 1 Figure 19- 23 Figure 19- 24 95 2 Chapter 19 22_57 197 4 ch 19. qxp 1/20/06 11:34 PM Page 95 2 Figure 19- 25 As you. borders. Then, place controls on the dialog as shown in Figure 19- 16. Figure 19- 16 94 4 Chapter 19 22_57 197 4 ch 19. qxp 1/20/06 11:34 PM Page 94 4 You can add the text to each static control by just typing. member. 94 9 Connecting to Data Sources 22_57 197 4 ch 19. qxp 1/20/06 11:34 PM Page 94 9 If you rebuild the application and run it again, it works as before but with a new window caption as Figure 19- 21