Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Beginning Visual C++ 2005 (2006) [eng]-1

.pdf
Скачиваний:
108
Добавлен:
16.08.2013
Размер:
18.66 Mб
Скачать

Connecting to Data Sources

The transfer of data between the database and the recordset occurs when the Open() member of the recordset 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 CProductView class, as follows:

void CProductView::OnInitialUpdate()

 

{

 

m_pSet = &GetDocument()->m_productSet;

 

m_pSet->m_strSort = “[CategoryID],[ProductID]”;

// Set the sort fields

CRecordView::OnInitialUpdate();

 

}

 

You just set m_strSort in the recordset to a string containing the name of the category ID field followed by the name of the product ID field. Square brackets are useful, even when there are no blanks in a name, because they differentiate strings containing these names from other strings, so you can immediately 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 by adding 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 fields CRecordView::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 string

CString strTable = m_pSet->GetTableName();

if(!strTable.IsEmpty())

// Verify we have a table name

strTitle += _T(“: “) + strTable;

// and add to basic title

GetDocument()->SetTitle(strTitle);

// Set the document title

}

}

After checking that the recordset is indeed open, you initialize a local CString object with a basic title string. You then get the name of the table from the recordset object by calling its GetTableName() member. 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 be more than one table involved in the recordset. After appending a colon followed by the table name you have retrieved to the basic title in strTitle, you set the result as the document title by calling the document’s SetTitle() member.

949

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 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 recordset class to handle order information from the database and a complementary view class to display some of the fields from the recordset. You’ll also add a button to the Products dialog to enable you to switch to the Orders dialog when you want to view the orders for the current product. This enables you to operate with the arrangement shown in Figure 19-22.

The Products dialog box is the starting position where you can step backwards and forwards through all the available products. Clicking the Show Orders button switches you to the dialog where you can view all the orders for the current product. You can return to the Products dialog box by clicking the Show Products button.

Adding a Recordset Class

You can start by adding the recordset class for the orders; 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.

950

Connecting to Data Sources

 

 

 

 

 

 

 

 

 

 

 

 

This dialog will allow you

Product ID

Edit

 

 

Category ID

 

Edit

 

 

to step through the

 

 

 

 

 

products available

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Product Name

Edit

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Unit Price

Edit

 

Units On Order

 

Sample ed

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Units In Stock

Edit

 

 

 

 

Show Orders

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Pressing the Show Orders button

 

 

 

 

 

 

 

 

 

 

will open the Orders dialog

 

 

 

 

 

 

 

 

 

 

for the current product

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Pressing the Show Products

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

button will return to

 

 

 

 

 

 

 

 

 

 

 

 

 

the Products dialog

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Order ID

 

Edit

 

Customer ID

 

Edit

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Product ID

 

Edit

 

 

 

 

 

 

 

 

 

 

This dialog will allow you

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

to step through all the

 

Quantity

 

 

 

 

 

 

 

 

 

 

 

 

 

orders for a given product

 

 

Edit

 

 

 

 

 

Show Products

 

 

Figure 19-22

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Select the type of consumer as Snapshot by selecting the radio button and then click the Data Sources button to go to the Select Data Source dialog box, where you’ll be able to identify the data source;

it should be on the Machine Data Source tab. When you have selected Northwind as the data source in the same way as you’ve seen previously, you’ll see the Select Database Object dialog box as shown in Figure 19-24.

You’ll select two tables to associate with the new recordset class that you’re going to create, so hold the Ctrl key down and select the Orders and Order Details table names. You can then click the OK button to complete the selection process. This returns you to the MFC ODBC Consumer dialog box where you’ll see the class name and file names have been 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.

951

Chapter 19

Figure 19-23

Figure 19-24

952

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 GetDefaultConnect() function for the COrderSet class is preceded by an #error directive that prevents compilation from succeeding, so comment that out.

A data member has been created in the COrderSet class for every field in each of the tables. When you select two or more tables for a given recordset, it is always possible, indeed likely, that there are field names duplicated; the OrderID field appears in both tables, for example. To ensure the names corresponding to field data members are always differentiated, the field names are prefixed with the table name 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 and the RFX_() call for it in the DoFieldExchange() member function. You must also change the initial value for the m_nFields member in the COrderSet constructor 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_nFields to 5. Change the members of type CStringW to type CString.

To hook the new recordset to the document, you need to add a data member to the definition of the CDBSampleDoc class, so right-click the class name in Class View and select Add Member Variable from the pop-up. Specify the type as COrderSet and the variable name as m_OrderSet. You can leave it as a public member of the class. Click OK to finish adding the data member to the document. The compiler has to understand that COrderSet is a class before it begins compiling the CBSampleDoc class. If you take a look at the contents of the DBSampleDoc.h header file, you will see that an #include statement has already been added for you to the top of DBSampleDoc.h:

953

Chapter 19

#pragma once

#include “ProductSet.h” #include “orderset.h”

class CDBSampleDoc : public CDocument

{

// Rest of class definition

}

Adding a View Class for the Recordset

At this point you might expect to be adding a class 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 classes that have CRecordView as a base manually.

You need to create another dialog resource before you create the view class so you have the ID for the resource 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 property to IDD_ORDERS_FORM. You also need to change the Style property to Child and the Border property to None.

You’re now ready to populate the dialog box with controls for the fields that you want to display from the Orders and Order Details tables. If you switch to Class View and select the COrderSet class name, you’ll be able to see the names of the variables concerned while you are working on the dialog. Add controls 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 Quantity fields from the tables associated with the COrderSet class, together with static controls to identify them. You can add 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 the control. 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 Only property to True for each control. Alternatively, you can set them all to read-only in one go by selecting each of them with the Ctrl key held down and then setting the Read Only property to True.

954

Connecting to Data Sources

The button control labeled Show Products is 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 dialog resource.

Creating the Record View Class

Create the OrderView.h file that holds the COrderView class definition. To do this just right-click DBSample in Solution Explorer and select Add >| New Item from the list. Choose the template to create a .h file and enter the file name as OrderView. After you have created the file, you can add the code for the class definition as:

#pragma once

class

COrderSet;

//

Declare

the

class

name

class

CDBSampleDoc;

//

Declare

the

class

name

// COrderView form view

class COrderView : public CRecordView

{

DECLARE_DYNCREATE(COrderView)

protected:

virtual ~COrderView(){}

virtual void DoDataExchange(CDataExchange* pDX); // DDX/DDV support virtual void OnInitialUpdate();

public:

enum { IDD = IDD_ORDERS_FORM };

COrderSet* m_pSet;

// Inline function definition CDBSampleDoc* GetDocument() const

{

return reinterpret_cast<CDBSampleDoc*>(m_pDocument);

}

COrderSet* GetRecordset();

virtual CRecordset* OnGetRecordset(); COrderView(); // constructor now public

#ifdef _DEBUG

virtual void AssertValid() const;

virtual void Dump(CDumpContext& dc) const; #endif

};

This code is based on the CProductView that was generated. The DECLARE_DYNCREATE macro enables objects 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_ DYNCREATE macro to the .cpp file a little later. I have omitted the debug version of the GetDocument() because the CProductView class contains a version of the function that validates the document object.

955

Chapter 19

The inline version in the COrderView class definition just assumes the cast to CDBSampleDoc* will be OK. I have included declarations for AssertValid() and Dump() that is compiled only when debug mode is in effect, so definition has to be included in the .cpp file for the class. The enumeration defines the ID for the dialog and you will use this in the definition of the constructor. The m_pSet member will hold the address of the recordset object that supplies the data displayed by this view.

The implementation of the COrderView class goes in the OrderView.cpp file, so create that file within the project using the procedure you followed for the .h file. You can add the initial #include directive for the classes you will need to reference:

#include “stdafx.h” #include “DBSample.h” #include “OrderView.h” #include “OrderSet.h” #include “DBSampleDoc.h”

This is not the complete set — you’ll be adding a couple more as you develop the class implementation.

You can add the macro to allow dynamic creation of COrderView objects next:

IMPLEMENT_DYNCREATE(COrderView, CRecordView)

The constructor just needs to initialize the m_pSet member to NULL:

COrderView::COrderView()

: CRecordView(COrderView::IDD), m_pSet(NULL)

{

}

Here you call the base class constructor with the dialog ID that you defined in an enumeration in the class as the argument. This identifies the dialog that is associated with the view.

Now add definitions for the two functions that may be used when you execute in debug mode:

// COrderView diagnostics

#ifdef _DEBUG

void COrderView::AssertValid() const

{

CRecordView::AssertValid();

}

void COrderView::Dump(CDumpContext& dc) const

{

CRecordView::Dump(dc);

}

#endif //_DEBUG

The DoDataExchange() function links the controls in the dialog to the fields in the recordset. The definition of this function is:

956

Connecting to Data Sources

void COrderView::DoDataExchange(CDataExchange* pDX)

{

CRecordView::DoDataExchange(pDX); DDX_FieldText(pDX, IDC_ORDERDETAILS_ORDERID,

m_pSet->m_OrderDetailsOrderID, m_pSet); DDX_FieldText(pDX, IDC_ORDERS_CUSTOMERID,

m_pSet->m_OrdersCustomerID, m_pSet); DDX_FieldText(pDX, IDC_ORDERDETAILS_PRODUCTID,

m_pSet->m_OrderDetailsProductID, m_pSet); DDX_FieldText(pDX, IDC_ORDERDETAILS_QUANTITY,

m_pSet->m_OrderDetailsQuantity, m_pSet);

}

You use the m_pSet member to access the fields in the COrderSet object that are to be displayed. The second argument to each DDX_FieldText() method call identifies the control for the field identified by the third argument. As you saw when you explored the CProductView class, the first argument determines whether data is being transferred to or from the control. The last argument simply identifies the recordset that is involved in the process.

There are two functions to be defined that are involved in retrieving the recordset. You’ll call the GetRecordset() function to obtain a pointer to the COrderSet object encapsulating the recordset. You can implement this as follows:

COrderSet* COrderView::GetRecordset()

{

ASSERT(m_pSet != NULL); return m_pSet;

}

The m_pSet member contains a pointer to the recordset. The MFC ASSERT macro here aborts the program with a message if the expression between the parentheses evaluates to 0. Thus, this just verifies that the pointer to the COrderSet object is not NULL. The ASSERT macro has the advantage that it operates only in a debug version of the application. In a release version, it does nothing.

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()

{

return m_pSet;

}

This just returns the address in m_pSet in this case. Obviously, in a situation where you needed to recreate the recordset, the code would need to be more complicated.

You are not finished with the view class yet. The next step is to determine more precisely what the recordset for the orders contain.

Customizing the Recordset

As it stands, the SQL SELECT operation for a COrderSet object produces a table that will contain all combinations of records from the two tables involved. This could be a lot of records, so you must add

957

Chapter 19

the equivalent of a WHERE clause to the query to restrict the records selected to those that make sense. But there is another problem, too: when you switch from the Products table 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, which amounts to selecting only those orders that have the same product ID as that contained in the current CProductSet record. This is also effected through a WHERE clause. In the MFC context, the WHERE clause for a SQL SELECT operation 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_strFilter member of the recordset object. This member is inherited from the base class, CRecordSet. As with the ORDER BY clause, which you added by assigning a value to the m_strSort member of the recordset, the place to implement this is in the 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 to those where the OrderID field in the Orders table is equal to the field with the same name in the Order Details table. 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 only those with a ProductID field that is equal to the ProductID field in the current record in the recordset object displaying the Products table. This means that you need to have the ProductID field from the COrderSet object 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 where the ProductID field equals the parameter value. The value that is to replace the question mark is set in the DoFieldExchange() member of the recordset. You’ll implement this in a moment, but first you’ll complete the specification of the filter.

You can define the string for the filter variable that incorporates both the conditions that you need with the 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 COrderView class, but before that, you’ll finish setting the parameter for the filter.

Defining the Filter Parameter

Add a data member to the COrderSet class to store the current value of the ProductID field from the CProductSet object. This member also actS as the parameter to substitute for the ? in the filter for the COrderSet object. So, right-click the COrderSet class name in Class View and select Add > Add

Variable from the pop-up. The variable type needs to be the same as that of the m_ProductID member of the CProductSet class, which is type long, and you can specify the name as m_ProductIDparam.

958