Special Edition Using Visual FoxPro 6


Chapter 17

Advanced Object-Oriented Programming


Data Environment Classes

Back in Chapter 15, "Creating Classes with Visual FoxPro," I mention the concept of manually created data environment classes. You might already be familiar with data environments from working with forms. The following section is a quick review of data environments from that perspective.

Data Environments with Forms

When you create a form with Visual FoxPro's Form Designer, you can specify the tables that the form works with by creating a data environment for the form. As Figure 17.1 shows, a form's data environment is made up of cursors and relations. If you like, you can think of a data environment as a package (that is, a container) that holds the information necessary for setting up the data environment.

Figure 17.1 : The data environment of a form.

Data Environments and Form Classes

One of the limitations of a form class is that data environments cannot be saved with the class. However, as it turns out, data environments, cursors, and relations are classes in and of themselves. That means you can create data environment definitions by combining cursors and relations into one package.

There is one limitation on the data environment, cursor, and relation classes: They cannot be created visually. They can only be created in code.

The Cursor Class

The first class to examine is the Cursor class. A Cursor class defines the contents of a work area. The following sections provide a brief review of the properties, events, and methods of the Cursor class.

Properties  Table 17.1 describes the properties of the Cursor class. The Cursor class also supports the base properties discussed in Chapter 14, "OOP with Visual FoxPro."

Table 17.1  The Properties of the Cursor Class
Property
Description
AliasThis property is the alias to give the work area when opened.
BufferModeOverrideThis property specifies the buffering mode for the cursor. Here are the values:
0     None (no buffering is done)
1     Use whatever is set at the form level
2     Pessimistic row buffering
3     Optimistic row buffering
4     Pessimistic table buffering
5     Optimistic table buffering

If the data environment class is not used on a form, you should use a value other than 1.

CursorSourceThis property is the source for the cursor's data. This could be a table from a database, a view (local or remote) from a database, or a free table. If CursorSource is a free table, you must specify the path to the DBF file.
DatabaseThis property is the database that has the cursor source. If the cursor source comes from a database, this field must be completed with a fully valid path, including drive and directories.
ExclusiveThis property specifies whether the cursor source is open in exclusive mode. It accepts a logical value.
FilterThis property is the Filter expression to place on the cursor source. The string placed here would be the string you would specify in a SET FILTER TO command.
NoDataOnLoadThis property, if set to .T., opens the cursor and creates a structure, but no data is downloaded for the cursor. Requery() will download the data.
OrderThis property specifies the initial order in which to display the data. You must specify an index tag name.
ReadOnlyThis property, if set to .T., opens the cursor as read-only; therefore, the data cannot be modified.

Events and Methods  The Cursor class supports only the Init, Destroy, and Error events.

The Relation Class

The Relation class specifies the information needed to set up a relationship between two cursors in a data environment. The following sections cover the properties, events, and methods.

Properties  To more easily explain the properties of the Relation class, consider the following situation. You have two tables in a cursor, one called Customer and the other named Invoice. Customer has a field called CID that holds the customer ID. Invoice has a field called cCustId that also holds the customer ID. Here is how you would normally set the relation:

SELECT Invoice
SET ORDER to cCustId
SELECT Customer
SET RELATION TO cId INTO Invoice

Table 17.2 presents the properties and their descriptions.

Table 17.2   The Properties of the Relation Class
Property
Description
ChildAliasThis property is the alias of the child table. In this example, it would be Invoice.
ChildOrderThis property specifies the order of the child table. In this example, it would be cCustId.
OneToManyThis property specifies whether the relationship is a one-to-many relationship.
ParentAliasThis property is the alias of the controlling alias. In this example, it would be Customer.
RelationalExprThis property is the expression of the relationship. In this example, it would be CID.

Events and Methods  The relation class supports only the Init, Destroy, and Error events.

The DataEnvironment Class

The DataEnvironment class is a container for cursors and relations, which, when taken together, make up an environment of data.

Properties  Table 17.3 presents the properties of the DataEnvironment class.

Table 17.3  The Properties of the DataEnvironment Class
Property
Description
AutoCloseTablesThis property specifies that tables should be closed automatically when the object is released. AutoCloseTables works in a form environment. In a coded DataEnvironment class, you would have to put code in the Destroy method for the tables to be closed. You would close the tables in the Destroy method by calling the DataEnvironment's OpenTables method.
AutoOpenTablesThis property specifies that tables should be opened automatically when the object is instantiated. AutoOpenTables works in a form environment. In a coded DataEnvironment class, you would have to put code in the Init method for the tables to be opened. You would open the tables in the Init method by calling the DataEnvironment's OpenTables method.
InitialSelectedAliasThis property specifies which alias should be selected initially.

Methods  Unlike the Cursor and Relation classes, the DataEnvironment class has two methods in addition to the base methods, as shown in Table 17.4.

Table 17.4  The Additional Methods of the DataEnvironment Class
Method
Description
CloseTables()This method closes all the tables and cursors in the data environment class.
OpenTables()This method opens all the tables and cursors in the data environment class.

Events  Table 17.5 presents the events supported by the DataEnvironment class.

Table 17.5  The Events Supported by the DataEnvironment Class
Event
Description
BeforeOpenTables()This event runs before tables are opened.
AfterCloseTables()This event runs after tables are closed.

Building a Data Environment Class  Now that you have seen all the elements that go into creating a data environment class, the next step is to build one. The examples I use here are based on the TESTDATA.DBC database located in Visual FoxPro's SAMPLES\DATA directory. The data environment class code, DE1.PRG, is presented in Listing 17.1.


Listing 17.1  17CODE01.PRG-Code for Sample Data Environment Class
*  Program...........: DE1.PRG
*  Author............: Menachem Bazian, CPA
*  Description.......: A sample data environment class

DEFINE CLASS CUSTOMER AS cursor
    alias = "CUSTOMER"
    cursorsource = "CUSTOMER"
    database = HOME()+"SAMPLES\DATA\TESTDATA.DBC"
ENDDEFINE

DEFINE CLASS ORDERS AS cursor
    alias = "ORDERS"
    cursorsource = "ORDERS"
    database = HOME()+"SAMPLES\DATA\TESTDATA.DBC"
ENDDEFINE

DEFINE CLASS ORDITEMS AS cursor
    alias = "ORDITEMS"
    cursorsource = "ORDITEMS"
    database = HOME()+"SAMPLES\DATA\TESTDATA.DBC"
ENDDEFINE

DEFINE CLASS Cust_To_Orders AS relation
    childalias = "ORDERS"
    parentalias = "CUSTOMER"
    childorder = "CUST_ID"
    RelationalExpr = "CUST_ID"
ENDDEFINE

DEFINE CLASS Orders_To_OrdItems AS relation
    childalias = "ORDITEMS"
    parentalias = "ORDERS"
    childorder = "ORDER_ID"
    RelationalExpr = "ORDER_ID"
ENDDEFINE

DEFINE CLASS DE AS DataEnvironment
    ADD OBJECT oCUSTOMER             AS CUSTOMER
    ADD OBJECT oORDERS               AS ORDERS
    ADD OBJECT oORDITEMS             AS ORDITEMS
    ADD OBJECT oCust_To_Orders       AS CUST_TO_ORDERS
    ADD OBJECT oOrders_To_OrdItems   AS ORDERS_TO_ORDITEMS
    
    PROCEDURE Init
        this.OpenTables()
    ENDPROC
   
    PROCEDURE Destroy
        this.CloseTables()
    ENDPROC
ENDDEFINE

Notice how all the first classes (that is, the Cursor and Relation classes) are manifestations based on the contents of the DBC file. The final class, DE, merely combines the cursor and relation classes under one roof. The Init method calls the OpenTables method so that all of the tables are automatically opened when the object is instantiated and the CloseTables() method is called when the object is released.

Notice also that the DE class shown in DE1.PRG uses all the cursor and relation classes in the program. You don't have to do this. Typically, when you work with data environment classes, you have one data environment class that opens all the tables (I call it a "default" data environment). You also have many other data environment classes that have only the cursor and relation objects that a particular function needs. For example, I could see the following data environment class added to DE1:

DEFINE CLASS SMALLDE AS DataEnvironment
    ADD OBJECT oCUSTOMER        AS CUSTOMER
    ADD OBJECT oORDERS          AS ORDERS
    ADD OBJECT oCust_To_Orders  AS CUST_TO_ORDERS
   
    PROCEDURE Init
        this.OpenTables()
    ENDPROC
   
    PROCEDURE Destroy
        this.CloseTables()
    ENDPROC
ENDDEFINE

This DataEnvironment class uses only the Customer and Orders cursors and the relation between them. It might be used, for example, for a list of order numbers belonging to a customer. This is not to say you couldn't use the default data environment class for everything. As a matter of course, however, I prefer to have only those cursors and relations referenced that I need.

One other item of interest in DE1.PRG is the settings for the Database property in all the classes. Using the Home function is perfectly reasonable: As long as the database name evaluates with a full path, the cursor will work fine.

Retrieving Definitions from a DBC File   There is one problem with retrieving definitions from a DBC file: It can be a major pain to type in all the cursor and relation classes you have in your DBC file. Furthermore, you might make changes to your DBC file during development and will then have to update the program with the data environment classes each time. This is not a pretty prospect.

Fortunately, it is relatively easy to get information from the database and generate your own program directly from the DBC file. Using functions such as ADBObjects, which retrieve information from the database, you can build a program automatically from the DBC file itself, thus saving yourself a lot of work. DumpDbc is a class that retrieves the information from a database and creates a program with the Cursor and Relation classes representing the contents of the database.

Class: DUMPDBC

DumpDbc a subclass of the Custom class. The following sections discuss its properties, events, and methods.

Properties  Table 17.6 presents DumpDbc's properties.

Table 17.6  The Properties of the DumpDbc Subclass
Property
Description
PROTECTED aRelationsThis property is a list of relation objects in the DBC file.
PROTECTED aTablesThis property is a list of table objects in the DBC file.
PROTECTED aViewsThis property is a list of view objects in the DBC.
CDBCNameThis property is the name of the DBC file you are exporting.
CPRGNameThis property is the name of the PRG file to be created.
PROTECTED cPathThis property is the directory path to the database.
PROTECTED nRelationsThis property is the number of relation objects in the DBC file.
PROTECTED nTablesThis property is the number of table objects in the DBC file.
PROTECTED nViewsThis property is the number of view objects in the DBC.

NOTE
Notice that all properties preceded by the keyword PROTECTED are protected members.

Events and Methods  Now that you have seen the properties of DumpDbc, the next step is to learn the events and methods.

doit  This method initiates the action of writing the program. It is the only public method in the class, thus it ensures that the entire process is run properly from start to finish.

As part of getting the process going, this method checks to see whether a DBC file to generate and a program to create have been specified in the cDBCName and cPRGName properties, respectively. If either of these has not been specified, a Getfile() or Putfile() dialog box is displayed. Failure to select a file will terminate the operation.

cursorclasses  This PROTECTED method runs through all the tables and views in the DBC file (as listed in aTables and aViews) and generates cursor classes for them by calling the WriteCursorClass method.

readdbc  This PROTECTED method reads the relation, table, and view definitions from DBC using ADBObjects and places them in the aRelations, aTables, and aViews members' arrays.

relationclasses  This PROTECTED method generates all the relation classes for the DBC file and writes them to the output program file.

Unlike cursor classes, which can be given names based on the name of the table or view with which they are associated, relation classes are more difficult to name objectively. This class names them in consecutive order (Relation1, Relation2, and so on).

writeclasses()  This PROTECTED method launches the process of writing all the classes. It is called by the Doit method after the ReadDbc method completes it operation. The Writeclasses method calls the CursorClasses and RelationClasses methods to write the individual classes to the output file.
NOTE
There are a few items to note. When developing applications, you might put the data in one place on the hard disk, but the data might live in a different place when installed at the user site. Because cursor classes require a full path to the database containing the cursor source, this could be a problem.

The solution I have allowed for here is to use a declared constant called DATABASEPATH when generating the program. DATABASEPATH will have the path to the database when the program is generated. If you need to change paths somewhere along the line, you can modify this one defined constant. Otherwise, you could change this method in DumpDbc and have DATABASENAME refer to a table field, public variable, or property that has the data path. The advantage of this approach is that it does not require any code changes when moving the database.

The #DEFINE command is placed at the end of a header this method generates. The header enables you to keep track of how and when this generated program was created.

One of the last things that this method does is call the WriteDefaultClass method. This method writes a default data environment class that has all the cursors and relations in it.

writecursorclass(tcClassName)  This PROTECTED method writes a single cursor class to the program. The name of the class is always the same as the name of the view or cursor (which is passed through as tcClassName).

writedefaultclass  This PROTECTED method writes a data environment class called Default_de, which has all the cursor and relation classes from the DBC file.

The Code for the Dumpdbc Class  Listing 17.2 presents the code for the Dumpdbc class. This code was exported using the Class Browser from the Chap17a.VCX visual class library.


Listing 17.2  17CODE02.PRG-Code for Dumpdbc Class That Retrieves Information from a Database and Creates a Program
*  Class.............: Dumpdbc.prg (D:\seuvfp6\Chap17\Chap17a.vcx)
*  Author............: Menachem Bazian, CPA
*  Notes.............: Exported code from Class Browser.

**************************************************
*-- Class:        dumpdbc
*-- ParentClass:  custom
*-- BaseClass:    custom
*-- Create CURSOR and RELATION classes for a .DBC .
*
DEFINE CLASS dumpdbc AS custom

    *-- Number of relation objects in the .DBC
    PROTECTED nrelations
    nrelations = 0
    *-- Number of table objects in the .DBC
    PROTECTED ntables
    ntables = 0
    *-- Number of view objects in the .DBC
    PROTECTED nviews
    nviews = 0
    *-- Name of the .DBC to dump
    cdbcname = ""
    *-- Name of program file to create.
    cprgname = ""
    Name = "dumpdbc"

    *-- Path to the database
    PROTECTED cpath

    *-- List of relation objects in the .DBC
    PROTECTED arelations[1]

    *-- List of view objects in the .DBC
    PROTECTED aviews[1]

    *-- List of table objects in the .DBC
    PROTECTED atables[1]
    PROTECTED init

    *-- Reads the DBC into the arrays.
    PROTECTED PROCEDURE readdbc
        IF !dbused(this.cDbcName)
            OPEN DATABASE (this.cDbcName)
        ENDIF

        *-- I need FoxTools for some work here

        LOCAL loFtools
        loFtools = CREATEOBJECT("foxtools")
        this.cPath        = loFtools.JustPath(DBC())

        *-- And, just to make sure that there is no
        *-- path in the .DBC name...

        this.cDbcName    = loFtools.JustfName(DBC())

        *-- Now read the .DBC

        this.nTables    = aDBObjects(this.aTables, "Table")
        this.nViews        = aDBObjects(this.aViews, "View")
        this.nRelations    = aDBObjects(this.aRelations, "Relation")
    ENDPROC

    *-- Writes all the classes.
    PROTECTED PROCEDURE writeclasses
        SET TEXTMERGE TO (this.cPRGNAME) NOSHOW

        *-- Write the header first

        SET TEXTMERGE ON

        LOCAL lcOldCentury
        lcOldCentury = SET("century")
        SET CENTURY ON

        \*  Program...........: <<this.cPRGName>>
        \*  DBC...............: <<this.cDBCName>>
        \*  Generated.........: <<MDY(DATE()) + " - " + TIME()>>
        \
        \#DEFINE databasepath "<<this.cPath>>"

        SET TEXTMERGE OFF

        IF this.nTables > 0    OR this.nViews > 0
            this.CursorClasses()
        ENDIF

        IF this.nRelations > 0
            this.RelationClasses()
        ENDIF

        this.WriteDefaultClass()

        SET TEXTMERGE OFF
        SET TEXTMERGE TO
    ENDPROC

    *-- Processes all the cursor classes in the .DBC.
    PROTECTED PROCEDURE cursorclasses
        LOCAL lnCounter
        SET TEXTMERGE ON
        FOR lnCounter = 1 TO this.nTables
            this.WriteCursorClass(this.aTables[lnCounter])
        ENDFOR

        FOR lnCounter = 1 TO this.nViews
            this.WriteCursorClass(this.aViews[lnCounter])
        ENDFOR
        SET TEXTMERGE OFF
    ENDPROC

    *-- Writes a cursor class to the output program file.
    PROTECTED PROCEDURE writecursorclass
        LPARAMETERS tcClassName

        \DEFINE CLASS <<STRTRAN(tcClassName, chr(32), "_")>> AS cursor
        \    alias = "<<tcClassName>>"
        \    cursorsource = "<<tcClassName>>"
        \    database = DATABASEPATH + "<<this.cDbcName>>"
        \ENDDEFINE
        \
    ENDPROC

    *-- Processes and writes all the relation classes.
    PROTECTED PROCEDURE relationclasses
        LOCAL    lnCounter, ;
                lcClassName, ;
                lcChildAlias, ;
                lcParentAlias, ;
                lcChildOrder, ;
                lcRelationalExpr

        SET TEXTMERGE ON

        FOR lnCounter = 1 TO this.nRelations
            lcClassName = "RELATION"-Alltrim(Str(lnCounter))
            lcChildAlias = this.aRelations[lnCounter,1]
            lcParentAlias = this.aRelations[lnCounter,2]
            lcChildOrder = this.aRelations[lnCounter,3]
            lcRelationalExpr = this.aRelations[lnCounter,4]

            \DEFINE CLASS <<lcClassName>> AS relation
            \    childalias = "<<lcChildAlias>>"
            \    parentalias = "<<lcParentAlias>>"
            \    childorder = "<<lcChildOrder>>"
            \    RelationalExpr = "<<lcRelationalExpr>>"
            \ENDDEFINE
            \
        ENDFOR

        SET TEXTMERGE OFF
    ENDPROC

    *-- Writes the default DE class to the program
    PROTECTED PROCEDURE writedefaultclass
        LOCAL laClasses[this.nTables + this.nViews + this.nRelations]

        FOR lnCounter = 1 TO this.nTables
            laClasses[lnCounter] = this.aTables[lnCounter]
        ENDFOR

        FOR lnCounter = 1 TO this.nViews
            laClasses[lnCounter+this.nTables] = this.aViews[lnCounter]
        ENDFOR

        FOR lnCounter = 1 TO this.nRelations
            laClasses[lnCounter+this.nTables+this.nViews] = ;
                "Relation" + ALLTRIM(STR(lnCounter))
        ENDFOR

        SET TEXTMERGE ON

        \DEFINE CLASS default_de AS DataEnvironment

        FOR lnCounter = 1 TO ALEN(laClasses,1)
            lcObjectName = 'o'+laClasses[lnCounter]
            lcClassName = laClasses[lnCounter]
        \    ADD OBJECT <<lcObjectName>> AS <<lcClassName>>
        ENDFOR
        \ENDDEFINE
        \

        SET TEXTMERGE OFF
    ENDPROC

    PROCEDURE doit
        *-- If no dbc name is specified, ask for one.

        IF EMPTY(this.cDBCName)
            this.cDBCName = GETFILE("DBC", "Please select DBC to dump:")
            IF !FILE(this.cDBCName)
                =MESSAGEBOX("No DBC selected! Aborted!",16)
                RETURN .F.
            ENDIF
        ENDIF

        *-- Same deal with a .PRG

        IF EMPTY(this.cPRGName)
            this.cPRGName = PUTFILE("PRG to create:","","PRG")

            IF EMPTY(this.cPRGName)
                =Messagebox("Operation cancelled!", 16)
                RETURN
            ENDIF
        ENDIF

        *-- As for overwrite permission here. I prefer to do this manually
        *-- (rather than let VFP handle it automatically) because it gives
        *-- me control.
        *--
        *-- Note how the SAFETY setting is queries first.

        IF SET("safety") = "ON" AND ;
            FILE(this.cPRGName) AND ;
            MessageBox("Overwrite existing " + ;
                        ALLTRIM(this.cPRGName) + "?", 36) # 6

            =Messagebox("Operation cancelled!", 16)
            RETURN
        ENDIF

        *-- save the SAFETY setting

        LOCAL lcOldSafety
        lcOldSafety = SET("safety")
        SET SAFETY OFF

        this.readdbc()
        this.writeclasses()

        SET SAFETY &lcOldSafety
    ENDPROC

ENDDEFINE
*
*-- EndDefine: dumpdbc
**************************************************

NOTE
DumpDbc uses the Foxtools class created in Chapter 15. You will need to load the Chap17a.VCX class library with the SET CLASSLIB TO CHAP17a command before instantiating an object from the DumpDbc subclass.

TESTDBC is a small test program that illustrates how this class can be used to generate a program for the TESTDATA.DBC database. The code is presented in Listing 17.3.


Listing 17.3  17CODE03.PRG-Program That Illustrates Usage of the Dumpdbc Class
*  Program...........: TESTDBC.PRG (D:\seuvfp6\Chap17\Chap17a.vcx)
*  Author............: Menachem Bazian, CPA              
*  Description.......: Illustrates usage of the DumpDbc class
*  Calling Samples...:
*  Parameter List....:
*  Major change list.:

*-- Note, FoxTool.VCX and Chap17a.VCX must be in the same
*-- Directory for this program to work.

Set ClassLib to Chap17
Set ClassLib to FoxTool ADDITIVE
oDbcGen = CREATEOBJECT("dumpdbc")
oDbcGen.cDBCName = "testdata" oDbcGen.cPRGName = "deleteme.prg"
oDbcGen.DoIt()

DELETEME.PRG is created by TESTDBC and is presented in Listing 17.4.


Listing 17.4  17CODE04.PRG-Code for DELETEME.PRG Program Output by the Dumpdbc Class
*  Program...........: deleteme.prg
*  DBC...............: TESTDATA.DBC
*  Generated.........: August 28, 1998 - 10:16:23

#DEFINE databasepath HOME()+"SAMPLES\DATA\"DEFINE CLASS CUSTOMER AS cursor
    alias = "CUSTOMER"
    cursorsource = "CUSTOMER"
    database = DATABASEPATH + "TESTDATA.DBC"
ENDDEFINE

DEFINE CLASS PRODUCTS AS cursor
    alias = "PRODUCTS"
    cursorsource = "PRODUCTS"
    database = DATABASEPATH + "TESTDATA.DBC"
ENDDEFINE

DEFINE CLASS ORDITEMS AS cursor
    alias = "ORDITEMS"
    cursorsource = "ORDITEMS"
    database = DATABASEPATH + "TESTDATA.DBC"
ENDDEFINE

DEFINE CLASS ORDERS AS cursor
    alias = "ORDERS"
    cursorsource = "ORDERS"
    database = DATABASEPATH + "TESTDATA.DBC"
ENDDEFINE

DEFINE CLASS EMPLOYEE AS cursor
    alias = "EMPLOYEE"
    cursorsource = "EMPLOYEE"
    database = DATABASEPATH + "TESTDATA.DBC"
ENDDEFINE

DEFINE CLASS RELATION1 AS relation
    childalias = "ORDERS"
    parentalias = "CUSTOMER"
    childorder = "CUST_ID"
    RelationalExpr = "CUST_ID"
ENDDEFINE

DEFINE CLASS RELATION2 AS relation
    childalias = "ORDERS"
    parentalias = "EMPLOYEE"
    childorder = "EMP_ID"
    RelationalExpr = "EMP_ID"
ENDDEFINE

DEFINE CLASS RELATION3 AS relation
    childalias = "ORDITEMS"
    parentalias = "ORDERS"
    childorder = "ORDER_ID"
    RelationalExpr = "ORDER_ID"
ENDDEFINE

DEFINE CLASS RELATION4 AS relation
    childalias = "ORDITEMS"
    parentalias = "PRODUCTS"
    childorder = "PRODUCT_ID"
    RelationalExpr = "PRODUCT_ID"
ENDDEFINE

DEFINE CLASS default_de AS DataEnvironment
    ADD OBJECT oCUSTOMER AS CUSTOMER
    ADD OBJECT oPRODUCTS AS PRODUCTS
    ADD OBJECT oORDITEMS AS ORDITEMS
    ADD OBJECT oORDERS AS ORDERS
    ADD OBJECT oEMPLOYEE AS EMPLOYEE
    ADD OBJECT oRelation1 AS Relation1
    ADD OBJECT oRelation2 AS Relation2
    ADD OBJECT oRelation3 AS Relation3
    ADD OBJECT oRelation4 AS Relation4
ENDDEFINE

Notice that just instantiating Default_De in this case is not enough to get the tables open. In order to open the tables, you have to call the OpenTables() method. Furthermore, in order to close the tables, you need to run the CloseTables() method.

If you prefer to have these actions happen by default, you could create a subclass of the data environment class as follows:

DEFINE CLASS MyDeBase AS DataEnvironment
    PROCEDURE Init
        this.OpenTables()
    ENDPROC

    PROCEDURE Destroy
        this.CloseTables()
    ENDPROC
ENDDEFINE

Then, when generating classes, you could use the subclass as the parent of the data environment classes you create.

Increasing the Power of Data Environments with DataSessions

Data environments are powerful, no doubt about that. But wait a minute. Back in the old days of FoxPro 2.x, most developers opened all their tables and relations once at the beginning of their applications and left them open throughout the application session. If this were still the strategy in Visual FoxPro, data environments would seem to be a whole lot to do about nothing.

The truth is, however, that data environments are extraordinarily useful because the basic strategy of opening all tables and relations up front is no longer the way to do things. Why not open all the tables and relations at the beginning of an application? Because you can use multiple data sessions, which gives you a tremendous amount of additional flexibility. With data sessions, you can segregate the data manipulation in one function from the data manipulation of another.

However, there's a catch here: Only forms can create independent data sessions. This would be insurmountable except for one fact: No one ever said a form has to be able to display. In other words, just because a form is a visual class, there is no reason you can't use it as a nonvisual class.

Consider the form class presented in Listing 17.5.


Listing 17.5  17CODE05.PRG-Code for Newdatasession Class
*  Class.............: Newdatasession.prg (D:\seuvfp6\Chap17\Chap17a.vcx)
*  Author............: Menachem Bazian, CPA
*  Notes.............: Exported code from Class Browser.

**************************************************
*-- Class:        newdatasession
*-- ParentClass:  form
*-- BaseClass:    form
*
DEFINE CLASS newdatasession AS form

    DataSession = 2
    Top = 0
    Left = 0
    Height = 35
    Width = 162
    DoCreate = .T.
    Caption = "Form"
    Visible = .F.
    Name = "newdatasession"
    PROTECTED show
    PROTECTED visible

ENDDEFINE
*
*-- EndDefine: newdatasession
**************************************************

Notice that the Visible property has been set to .F. and has been protected, and that the SHOW method has been protected as well. In other words, I have just created a form that cannot be displayed.

This concept would be ludicrous except for the fact that the DataSession property has been set to 2, which indicates that this form has its own data session. When the object is instantiated, a new DataSession is created and can be referenced with the SET DATASESSION command (the ID of the form's data session is stored in its DataSessionId property).

This is a prime example of using a class that is normally thought of as a visual class as the basis for creating a nonvisual class.

CAUTION
In order to use the NewDataSession class, the instance must live as its own object and cannot be added to a container. If you want this object to be a property of another object, create the instance with CREATEOBJECT() and not with ADD OBJECT or ADDOBJECT(). If you use one of the two latter methods, you will not get a private data session because the data session is governed by the container package in a composite class. See Chapter 14 for more information.

Modeling Objects on the Real World

Objects are supposed to model the real world, right? Up until now, the classes I covered have been focused on functionality rather than modeling the real world. Now it's time to look at classes designed to model real-world objects.

The first object I present is a familiar one that provides a good opportunity to look at modeling functionality in objects: a stopwatch.

Defining the Stopwatch

The first step in any attempt to create an object is to define what the object is all about. This usually happens, when developing object-oriented software, in the analysis and design phase of the project. I briefly discussed this phase in Chapter 13, "Introduction to Object-Oriented Programming." In this case, it's a relatively simple exercise.

Consider a stopwatch. If you happen to have one, take it out and look at it. Notice that it has a display (usually showing the time elapsed in HH:MM:SS.SS format). The stopwatch has buttons that enable you to start it, stop it, pause it (lap time), and reset the display. Naturally, a stopwatch has the capability to track time from when it is started until it is stopped. This is a good list of the functions needed for a stopwatch class. When you have the required behavior of the object, you can then work on designing the implementation of the class.

Implementing the Stopwatch

Many factors can affect how a class is implemented, ranging from how the class is intended to be used to the personal preferences of the developer.

In this case, when designing the implementation of the stopwatch class, the functionality is divided into two parts. The first part is the engine (the portion of the stopwatch that has the functionality for calculating the time as well as starting, stopping, and pausing the stopwatch). The second class combines the engine with the display to create a full stopwatch.

Frequently, when working on a single class's implementation, opportunities present themselves for creating additional functionality at little cost. In this case, breaking the functionality of the stopwatch into an engine and a display portion gives you the ability either to subclass the engine into something different or to use the engine on its own without being burdened by the display component.

It's always a good idea to look at the implementation design of a class and ask the following question: Is there a way I can increase the reusability of the classes I create by abstracting (separating) functionality? The more you can make your classes reusable, the easier your life will be down the road.

The SwatchEngine Class

This class can be thought of as the mechanism behind the stopwatch. Based on the Timer class, the SwatchEngine class basically counts time from when it is started to when it is stopped. It does not enable for any display of the data. (A stopwatch with a display is added in class SWATCH). This class is useful for when you want to track the time elapsed between events. The SwatchEngine class code is presented in Listing 17.6.


Listing 17.6  17CODE06.PRG-Code for the SwatchEngine Class, Which Performs the Stopwatch Count-Down Operations
*  Class.............: Swatchengine.prg  (D:\seuvfp6\Chap17\Chap17a.vcx)
*  Author............: Menachem Bazian, CPA
*  Notes.............: Exported code from Class Browser.

**************************************************
*-- Class:        swatchengine
*-- ParentClass:  timer
*-- BaseClass:    timer
*-- Engine behind the SWATCH class.
*
DEFINE CLASS swatchengine AS timer

    Height = 23
    Width = 26
    *-- Number of seconds on the clock
    nsecs = 0
    *-- Time the clock was last updated
    PROTECTED nlast
    nlast = 0
    *-- Time the clock was started
    nstart = 0
    Name = "swatchengine"

    *-- Start the clock
    PROCEDURE start
        this.nstart   = SECONDS()
        this.nLast    = this.nStart
        this.nSecs    = 0
        this.Interval = 200
    ENDPROC

    *-- Stop the clock
    PROCEDURE stop
        this.timer()

        this.Interval = 0
        this.nLast    = 0
    ENDPROC

    *-- Pause the clock.
    PROCEDURE pause
        this.timer()
        this.interval = 0
    ENDPROC

    *-- Resume the clock
    PROCEDURE resume
        If this.nLast = 0 && Clock was stopped
           this.nLast = SECONDS() && Pick up from now
           this.interval = 200
        ELSE
           this.interval = 200
        ENDIF
    ENDPROC

    PROCEDURE Init
        this.nstart = 0
        this.Interval = 0
        this.nSecs = 0
        this.nLast = 0
    ENDPROC

    PROCEDURE Timer
        LOCAL lnSeconds

        lnSeconds     = SECONDS()
        this.nSecs    = this.nSecs + (lnSeconds - this.nLast)
        this.nLast    = lnSeconds
    ENDPROC

ENDDEFINE
*
*-- EndDefine: swatchengine
**************************************************

Properties  Table 17.7 presents the properties for SwatchEngine.

Table 17.7  The Properties for SwatchEngine
Properties
Description
IntervalThe Interval property is not a new property-it is standard to the Timer class. If Interval is set to zero, the clock does not run; if Interval is set to a value greater than zero, the clock runs.

In SwatchEngine, the clock runs at a "standard" interval of 200 milliseconds.

NsecsThis property is the number of seconds counted. It is carried out to three decimal places and is what the SECONDS function returns.
PROTECTED nlastThis property is the last time the Timer event fired. This is a protected property.
nstartThis property is the time the watch was started, measured in seconds since midnight.

NOTE
For the record, the Timer's Interval property governs how often the Timer event fires. If set to 0, the Timer event does not run. A positive Interval determines how often the Timer event runs. The Interval is specified in milliseconds.

Events and Methods  The following methods have a common theme: There is very little action happening. For the most part, all of these methods accomplish their actions by setting properties in the timer. For example, the clock can be started and stopped by setting the Interval property (a value of zero stops the clock, and anything greater than zero starts the clock). Table 17.8 presents the methods for SwatchEngine.

Table 17.8  The Methods for SwatchEngine
Method
Description
InitThis method initializes the nstart, Interval, nSecs, and nLast properties to zero.
PauseThis method calls the Timer() method to update the time counter and then stops the clock by setting the Interval property to 0.
ResumeThis method restarts the clock by setting the Interval property to 200 (1/5 of a second). If nLast is not set to 0, the Resume() method knows that the clock was paused and picks up the count as if the clock were never stopped. Otherwise, all the time since the clock was stopped is ignored and the clock begins from that point.
StartThis method starts the clock and records when the clock was started.
StopThis method stops the clock and sets nLast to 0.
TimerThis method updates the number of seconds (nSecs) property.

The Swatch Class

Now that the engine is done, you can combine the engine with a display component to create a stopwatch. The Swatch class is a container-based class that combines a label object (which is used to display the amount of time on the stopwatch) with a swatch engine object to complete the functional stopwatch.

Design Strategy  The key to this class is SwatchEngine. The parent (that is, the container) has properties and methods to mirror SwatchEngine, such as nStart, Start(), Stop(), Pause(), and Resume(). This enables a form using the class to have a consistent interface to control the stopwatch. In other words, the form does not have to know there are separate objects within the container; all the form has to do is communicate with the container.

Figure 17.2 shows what the class looks like in the Visual Class Designer.

Figure 17.2 : The Swatch class in the Visual Class Designer.

The code for the Swatch class is presented in Listing 17.7.


Listing 17.7  17CODE07.PRG-Code for the Swatch Class That Displays the Stopwatch Time
*  Class.............: Swatch.prg  (D:\seuvfp6\Chap17\Chap17a.vcx)
*  Author............: Menachem Bazian, CPA
*  Notes.............: Exported code from Class Browser.

**************************************************
*-- Class:        swatch
*-- ParentClass:  container
*-- BaseClass:    container
*
DEFINE CLASS swatch AS container

    Width = 141
    Height = 41
    nsecs = 0
    nstart = (seconds())
    Name = "swatch"

    ADD OBJECT lbltime AS label WITH ;
        Caption = "00:00:00.0000", ;
        Height = 25, ;
        Left = 7, ;
        Top = 8, ;
        Width = 97, ;
        Name = "lblTime"

    ADD OBJECT tmrswengine AS swatchengine WITH ;
        Top = 9, ;
        Left = 108, ;
        Height = 24, ;
        Width = 25, ;
        Name = "tmrSWEngine"

    PROCEDURE stop
        this.tmrSWEngine.Stop()
    ENDPROC

    PROCEDURE start
        this.tmrSWEngine.Start()
    ENDPROC

    PROCEDURE resume
        this.tmrSWEngine.Resume()
    ENDPROC

    PROCEDURE pause
        this.tmrSWEngine.Pause()
    ENDPROC

    *-- ,Property Description will appear here.
    PROCEDURE reset
        this.tmrSWEngine.nSecs = 0
        this.Refresh()
    ENDPROC

    PROCEDURE Refresh
        LOCAL lcTime, ;
              lnSecs, ;
              lnHours, ;
              lnMins, ;
              lcSecs, ;
              lnLen

        this.nSecs  = this.tmrSWEngine.nSecs
        this.nStart = this.tmrSWEngine.nStart

        *-- Take the number of seconds on the clock (nSecs property)
        *-- and convert it to a string for display.

        lcTime    = ""
        lnSecs    = this.tmrSWEngine.nSecs

        lnHours   = INT(lnSecs/3600)

        lnSecs    = MOD(lnSecs,3600)
        lnMins    = INT(lnSecs/60)

        lnSecs    = MOD(lnSecs,60)

        lcSecs    = STR(lnSecs,6,3)
        lnLen     = LEN(ALLT(LEFT(lcSecs,AT('.', lcSecs)-1)))
        lcSecs    = REPL('0', 2-lnLen) + LTRIM(lcSecs)

        lnLen     = LEN(ALLT(SUBST(lcSecs,AT('.', lcSecs)+1)))
        lcSecs    = RTRIM(lcSecs) + REPL('0', 3-lnLen)

        lcTime    = PADL(lnHours,2,'0') + ":" + ;
                    PADL(lnMins,2,'0') + ":" + ;
                    lcSecs

        this.lblTime.Caption = lcTime
    ENDPROC

    PROCEDURE tmrswengine.Timer
        Swatchengine:Timer()
        this.Parent.refresh()
    ENDPROC

ENDDEFINE
*
*-- EndDefine: swatch
**************************************************

Member Objects  The Swatch class has two member objects:

Custom Properties  Notice that the properties shown in Table 17.9 are properties of the container itself.

Table 17.9  The Custom Properties
Properties
Description
nStartThis property is the time the watch was started, measured in seconds since midnight.
NsecsThis property is the number of seconds counted. It is carried out to three decimal places and is what the SECONDS() function returns. The SwatchEngine properties in tmrSWEngine remain intact as inherited from the base class.

Events and Methods  Notice Table 17.10 presents the events and methods in the Swatch class.

Table 17.10  The Events and Methods in the Swatch Class
Event/Method
Description
Swatch.StartThis method calls tmrSWEngine.Start.
Swatch.StopThis method calls tmrSWEngine.Stop.
Swatch.PauseThis method calls tmrSWEngine.Pause.
Swatch.ResumeThis method calls tmrSWEngine.Resume.
Swatch.ResetThis method resets the nSecs counter to 0 and then calls the Refresh method. This is designed to enable the display portion of the stopwatch to be reset to 00:00:00.000.
Swatch.Refresh()This method updates the container properties nStart and nSecs from the timer and converts the number of seconds counted to HH:MM:SS.SS format.
TmrSWEngine.Timer()This method calls the SwatchEngine::Timer method, followed by the container's Refresh method.

Putting It Together on a Form

And now for the final step: putting all of this functionality together on a form. The form is shown in Figure 17.3. The code for the Swatchform form is presented in Listing 17.8.

Figure 17.3 : The Stopwatch form.


Listing 17.8  17CODE08.PRG-Code for the Swatchform Form That Contains the Stopwatch
*  Class.............: Swatchform.prg (D:\seuvfp6\Chap17\Chap17a.vcx)
*  Author............: Menachem Bazian, CPA
*  Notes.............: Exported code from Class Browser.

**************************************************
*-- Class:        swatchform
*-- ParentClass:  form
*-- BaseClass:    form
*
DEFINE CLASS swatchform AS form

    ScaleMode = 3
    Top = 0
    Left = 0
    Height = 233
    Width = 285
    DoCreate = .T.
    BackColor = RGB(192,192,192)
    BorderStyle = 2
    Caption = "Stop Watch Example"
    Name = "swatchform"

    ADD OBJECT swatch1 AS swatch WITH ;
        Top = 24, ;
        Left = 76, ;
        Width = 132, ;
        Height = 37, ;
        Name = "Swatch1", ;
        lbltime.Caption = "00:00:00.0000", ;
        lbltime.Left = 24, ;
        lbltime.Top = 12, ;
        lbltime.Name = "lbltime", ;
        tmrswengine.Top = 9, ;
        tmrswengine.Left = 108, ;
        tmrswengine.Name = "tmrswengine"

    ADD OBJECT cmdstart AS commandbutton WITH ;
        Top = 84, ;
        Left = 48, ;
        Height = 40, ;
        Width = 85, ;
        Caption = "\<Start", ;
        Name = "cmdStart"

    ADD OBJECT cmdstop AS commandbutton WITH ;
        Top = 84, ;
        Left = 144, ;
        Height = 40, ;
        Width = 85, ;
        Caption = "S\<top", ;
        Enabled = .F., ;
        Name = "cmdStop"

    ADD OBJECT cmdpause AS commandbutton WITH ;
        Top = 129, ;
        Left = 49, ;
        Height = 40, ;
        Width = 85, ;
        Caption = "\<Pause", ;
        Enabled = .F., ;
        Name = "cmdPause"

    ADD OBJECT cmdresume AS commandbutton WITH ;
        Top = 129, ;
        Left = 145, ;
        Height = 40, ;
        Width = 85, ;
        Caption = "\<Resume", ;
        Name = "cmdResume"

    ADD OBJECT cmdreset AS commandbutton WITH ;
        Top = 192, ;
        Left = 72, ;
        Height = 37, ;
        Width = 121, ;
        Caption = "Reset \<Display", ;
        Name = "cmdReset"

    PROCEDURE cmdstart.Click
        this.enabled = .F.
        thisform.cmdStop.enabled = .T.
        thisform.cmdPause.enabled = .T.
        thisform.cmdResume.enabled = .F.
        thisform.cmdReset.enabled = .F.
        thisform.Swatch1.Start()
    ENDPROC

    PROCEDURE cmdstop.Click
        this.enabled = .F.
        thisform.cmdStart.enabled = .T.
        thisform.cmdPause.enabled = .F.
        thisform.cmdResume.enabled = .T.
        thisform.cmdReset.enabled = .T.
        thisform.swatch1.stop()
    ENDPROC

    PROCEDURE cmdpause.Click
        this.enabled = .F.
        thisform.cmdStop.enabled = .T.
        thisform.cmdStart.enabled = .F.
        thisform.cmdResume.enabled = .T.
        thisform.cmdReset.enabled = .F.
        ThisForm.Swatch1.Pause()
    ENDPROC

    PROCEDURE cmdresume.Click
        this.enabled = .F.
        thisform.cmdStart.enabled = .F.
        thisform.cmdPause.enabled = .T.
        thisform.cmdStop.enabled = .T.
        thisform.cmdResume.enabled = .F.
        thisform.cmdReset.enabled = .F.
        thisform.swatch1.resume()
    ENDPROC

    PROCEDURE cmdreset.Click
        thisform.swatch1.reset()
    ENDPROC

ENDDEFINE
*
*-- EndDefine: swatchform
**************************************************

The form, SwatchForm, is a form-based class with a Swatch object dropped on it. The command buttons on the form call the appropriate Swatch methods to manage the stopwatch (that is, start it, stop it, and so on).

There isn't much to this form, as the preceding code shows. All the real work has already been done in the Swatch class. All the objects on the form do is call methods from the Swatch class.

Member Objects  Table 17.11 presents the member objects of the Swatch class.

Table 17.11  The Member Objects of the Swatch Class
Member Object
Description
Swatch1(Swatch Class) This object is an instance of the Swatch class.
CmdStart(Command Button) This object is the Start button, which starts the clock and appropriately enables or disables the other buttons.
CmdStop(Command Button) This object is the Stop button, which stops the clock and appropriately enables or disables the other buttons.
CmdPause(Command Button) This object is the Pause button, which pauses the clock and appropriately enables or disables the other buttons.
CmdResume(Command Button) This object is the Resume button. It resumes the clock and appropriately enables or disables the other buttons.
CmdReset(Command Button) This object is the Reset button, which resets the clock display.

The Swatch Class: A Final Word

One of the keys to achieving reuse is to l ook for it when you design the functionality of your code. There is no magic to this process, but there are methodologies designed to assist in the process. The intent behind showing the Swatch class is to illustrate how a single class-when it is created-can evolve into more classes than one in order to support greater reusability. When you think of your design, think about reusability.

Working with Frameworks

In Chapter 15 I introduced the concept of a framework. A framework, put simply, is the collection of classes that, when taken together, represent the foundation on which you will base your applications. A framework represents two things. First, it represents the parent classes for your subclasses. Second, it represents a structure for implementing functionality within your applications.

Take the issue of creating business classes. A properly designed framework can make creating business objects easy. In the next section I show you why.

The Nature of Business Objects

At the simplest level, a business object (a customer, for example) has the functionality that would normally be associated with data entry and editing. For example, the responsibilities could include the following:

You can add to this list of common responsibilities. Obviously, you need functions for adding new records, for deleting records, and so on. The five functions presented in this section serve as an example and are modeled in the framework.

In terms of creating the framework, the goal is to create a series of classes that interact with each other to provide the functionality you need. In order to make the framework usable, you want to keep the modifications needed for combining the classes into a business class as minimal as possible.

The framework I present breaks down the functionality as follows:

The nice thing about this framework, as it will work out in the end, is that the only work necessary for implementing a business class will occur in the business class. All the other classes are designed generically and refer to the business class for specific functionality. I go through the classes one at a time in the following sections.

The Base_Navigation Class

The Base_Navigation class is a set of navigation buttons designed to be used with forms that display a business class. Figure 17.4 shows the Base_Navigation class in the Class Designer. The Base_Navigation class is also selected in the Class Browser. The code for the class ispresented in Listing 17.9.

Figure 17.4 : The Base Navigation class.

NOTE
The framework classes shown in this chapter can be found on the in the FW.VCX visual class library. The class listings were generated by the Class Browser View Class code function.


Listing 17.9  17CODE09.PRG-Code for Base_Navigation Class

*  Class.............: Base_navigation (D:\seuvfp6\Chap17\FW.vcx)
*  Author............: Menachem Bazian, CPA
*  Notes.............: Exported code from Class Browser.

**************************************************
*-- Class:        base_navigation
*-- ParentClass:  container
*-- BaseClass:    container
*-- Collection of nav buttons for business class forms.
*
DEFINE CLASS base_navigation AS container

    Width = 328
    Height = 30
    Name = "base_navigation"

    ADD OBJECT cmdtop AS commandbutton WITH ;
        Top = 0, ;
        Left = 0, ;
        Height = 29, ;
        Width = 62, ;
        Caption = "Top", ;
        Name = "cmdTop"

    ADD OBJECT cmdbottom AS commandbutton WITH ;
        Top = 0, ;
        Left = 66, ;
        Height = 29, ;
        Width = 62, ;
        Caption = "Bottom", ;
        Name = "cmdBottom"

    ADD OBJECT cmdnext AS commandbutton WITH ;
        Top = 0, ;
        Left = 132, ;
        Height = 29, ;
        Width = 62, ;
        Caption = "Next", ;
        Name = "cmdNext"

    ADD OBJECT cmdprev AS commandbutton WITH ;
        Top = 0, ;
        Left = 198, ;
        Height = 29, ;
        Width = 62, ;
        Caption = "Previous", ;
        Name = "cmdPrev"

    ADD OBJECT cmdclose AS commandbutton WITH ;
        Top = 0, ;
        Left = 265, ;
        Height = 29, ;
        Width = 62, ;
        Caption = "Close", ;
        Name = "cmdClose"

    PROCEDURE cmdtop.Click
        LOCAL lcClassName

        lcClassName = thisform.cClass
        ThisForm.&lcClassName..Topit()
    ENDPROC

    PROCEDURE cmdbottom.Click
        LOCAL lcClassName

        lcClassName = thisform.cClass
        ThisForm.&lcClassName..Bottomit()
    ENDPROC

    PROCEDURE cmdnext.Click
        LOCAL lcClassName

        lcClassName = thisform.cClass
        ThisForm.&lcClassName..Nextit()
    ENDPROC

    PROCEDURE cmdprev.Click
        LOCAL lcClassName

        lcClassName = thisform.cClass
        ThisForm.&lcClassName..Previt()
    ENDPROC

    PROCEDURE cmdclose.Click
        Release ThisForm
    ENDPROC

ENDDEFINE
*
*-- EndDefine: base_navigation
**************************************************

There is nothing too exciting here. The class is almost a yawner-it doesn't seem to present anything new. However, there is one very important difference between this class and all the other navigation functionality you have seen so far. The Base_Navigation class looks for the custom property cClass on the form. This property has the name of the business class residing on the form. With that name, the navigation buttons can call the appropriate movement methods in the business object. The navigation class, in other words, has no clue how to move to the next record, for example. It delegates that responsibility to the business class.

The Base Form

The next step is to create a form that has the cClass property and has an instance of the Base_Navigation class on it. This form will be subclassed for all business object data entry forms (you'll see this later in the section "Using the Framework").

The class Base_Form is shown in Figure 17.5. The code for the Base_Form class is presented in Listing 17.10.

Figure 17.5 : The base Business form.


Listing 17.10  17CODE10.PRG-Code for the Base_Form Class
*  Class.............: Base_form (D:\seuvfp6\Chap17\FW.vcx)
*  Author............: Menachem Bazian, CPA
*  Notes.............: Exported code from Class Browser.

**************************************************
*-- Class:        base_form
*-- ParentClass:  form
*-- BaseClass:    form
*-- Base form for business classes
*
DEFINE CLASS base_form AS form

    DataSession = 2
    DoCreate = .T.
    BackColor = RGB(128,128,128)
    Caption = "Form"
    Name = "base_form"

    *-- Name of business class on the form.
    cclass = .F.

    ADD OBJECT base_navigation1 AS base_navigation WITH ;
        Top = 187, ;
        Left = 25, ;
        Width = 328, ;
        Height = 30, ;
        Name = "base_navigation1", ;
        cmdTop.Name = "cmdTop", ;
        cmdBottom.Name = "cmdBottom", ;
        cmdNext.Name = "cmdNext", ;
        cmdPrev.Name = "cmdPrev", ;
        cmdClose.Name = "cmdClose"

ENDDEFINE
*
*-- EndDefine: base_form
**************************************************

There is nothing really exciting here, either. This form has a custom property called cClass that is designed to tell the navigation class onto which class to delegate the navigation method responsibilities. It also has an instance of the navigation class on it.

The Base Data Environment Class

Business classes use tables. Using tables typically calls for a data environment. The framework class for this is the Base_De class shown in Listing 17.11.


Listing 17.11  17CODE11.PRG-Code for the Base_De Class Used in Table Navigation
*  Class.............: Base_de (D:\seuvfp6\Chap17\FW.vcx)
*  Author............: Menachem Bazian, CPA
*  Notes.............: Exported code from Class Browser.

**************************************************
*-- Class:        base_de
*-- ParentClass:  custom
*-- BaseClass:    custom
*-- DataEnvironment Loader
*
DEFINE CLASS base_de AS custom

    Height = 36
    Width = 36
    Name = "base_de"

    *-- Name of the DE Class to load
    cdeclassname = .F.

    *-- The name of the program holding the DE class
    cdeprgname = .F.

    *-- Ensures that a dataenvironment class name has been specified
    PROTECTED PROCEDURE chk4de
        IF TYPE("this.cDeClassName") # 'C' OR EMPTY(this.cDeClassName)
            =MessageBox("No Data Environment was specified. " + ;
                              "Cannot instantiate object.", ;
                        16, ;
                        "Instantiation Error")
            RETURN .F.
        ENDIF
    ENDPROC

    *-- Opens the Data Environment
    PROCEDURE opende
        *-- Method OPENDE
        *-- This method will instantiate a DE class and run the
        *-- OpenTables() method.
        *--
        *-- Since the Container is not yet instantiated, I cannot do
        *--an AddObject() to it.
        *--
        *-- I'll do that in the container's Init.

        LOCAL loDe, lcClassName

        IF !EMPTY(this.cDEPrgName)
            SET PROCEDURE TO (this.cDEPrgName) ADDITIVE
        ENDIF

        lcClassName = this.cDeClassName
        loDe = CREATEOBJECT(lcClassName)

        IF TYPE("loDe") # "O"
            IF !EMPTY(this.cDEPrgName)
                RELEASE PROCEDURE (this.cDEPrgName)
            ENDIF
            RETURN .F.
        ENDIF

        *-- If we get this far, we can run the opentables method.

        loDe.OpenTables()

        IF !EMPTY(this.cDEPrgName)
            RELEASE PROCEDURE (this.cDEPrgName)
        ENDIF
    ENDPROC

    PROCEDURE Init
        IF !this.Chk4dE()
            RETURN .f.
        ENDIF

        *-- Add code here to add the DE object at runtime and run the OPENTABLES
        *-- event. I will leave that out for now....

        RETURN this.openDE()
    ENDPROC

ENDDEFINE
*
*-- EndDefine: base_de
**************************************************

Finally, here is something interesting to discuss. This class, based on Custom, is designed to load a data environment for the business class. It has two properties: the name of the data environment class to load and the name of the PRG file where the data environment class resides. Because data environment classes cannot be created visually, this class acts as a wrapper.

Why separate this class out? To be sure, the functionality for this class could have been rolled into the business class; however, when creating this framework, I could see the use of having this type of Loader class on many different kinds of forms and classes. By abstracting the functionality out into a different class, I can now use this class whenever and wherever I please.

Events and Methods  Table 17.12 presents the Base_De class's events and methods.

Table 17.12  The Base_De Class's Events and Methods
Event/Method
Description
InitThe Init method first calls the Chk4De method to make sure that a data environment class name has been specified. If not, the object's instantiation is aborted. It then calls OpenDe to open the tables and relations.
Chk4DeThis method checks to make sure that a data environment was specified.
OpenDeThis method instantiates the data environment class and runs the OpenTables method.

What about closing the tables? Well, the base form is set to run in its own data session. When the form instance is released, the data session is closed along with all of the tables in it. Don't you love it when a plan comes together?

The Base Business Class

The next class is the framework class for creating business classes. Figure 17.6 shows the Base_Business class, and the code is presented in Listing 17.12.

Figure 17.6 : The Base Business class.


Listing 17.12  17CODE12.PRG-Code for the Base_Business Class That Creates Business Classes
*  Class.............: Base_business (D:\seuvfp6\Chap17\FW.vcx)
*  Author............: Menachem Bazian, CPA
*  Notes.............: Exported code from Class Browser.

**************************************************
*-- Class:        base_business
*-- ParentClass:  container
*-- BaseClass:    container
*-- Abstract business class.
*
DEFINE CLASS base_business AS container

    Width = 215
    Height = 58
    BackStyle = 0
    TabIndex = 1
    Name = "base_business"

    *-- Name of the table controlling the class
    PROTECTED ctablename

    ADD OBJECT base_de1 AS base_de WITH ;
        Top = 0, ;
        Left = 0, ;
        Height = 61, ;
        Width = 217, ;
        cdeclassname = "", ;
        Name = "base_de1"

    *-- Add a record
    PROCEDURE addit
        SELECT (this.cTableName)
        APPEND BLANK

        IF TYPE("thisform") = "O"
            Thisform.refresh()
        ENDIF
    ENDPROC

    *-- Go to the next record
    PROCEDURE nextit
        SELECT (this.cTableName)

        SKIP 1
        IF EOF()
            ?? CHR(7)
            WAIT WINDOW NOWAIT "At end of file"
            GO BOTTOM
        ENDIF

        IF TYPE("thisform") = "O"
            Thisform.refresh()
        ENDIF
    ENDPROC

    *-- Move to prior record
    PROCEDURE previt
        SELECT (this.cTableName)

        SKIP -1
        IF BOF()
            ?? CHR(7)
            WAIT WINDOW NOWAIT "At beginning of file"
            GO top
        ENDIF

        IF TYPE("thisform") = "O"
            Thisform.refresh()
        ENDIF
    ENDPROC

    *-- Move to the first record
    PROCEDURE topit
        SELECT (this.cTableName)

        GO TOP
        IF TYPE("thisform") = "O"
            Thisform.refresh()
        ENDIF
    ENDPROC

    *-- Move to the last record
    PROCEDURE bottomit
        SELECT (this.cTableName)

        GO BOTTOM

        IF TYPE("thisform") = "O"
            Thisform.refresh()
        ENDIF
    ENDPROC

    PROCEDURE Init
        SELECT (this.cTableName)

        IF TYPE("thisform.cClass") # 'U'
            thisform.cClass = this.Name
        ENDIF
    ENDPROC


    PROCEDURE editit
    ENDPROC

    PROCEDURE getit
    ENDPROC

ENDDEFINE
*
*-- EndDefine: base_business
**************************************************

This class is based on a container class. In Chapter 15, I discuss the flexibility of the container class, and here is a perfect example. The Base Business class is a container with methods attached to it that handle the functionality of the business class. The container also serves as a receptacle for a base data environment loader class. When subclassing the Base_Business class, you would add the GUI elements that make up the data for the business class. Because the data environment loader class is added as the first object on the container, it instantiates first. Thus, if you have controls in a business class that reference a table as the control source, the business class will work because the data environment loader will open the tables before those other objects get around to instantiating.

The Base Business class has one custom property, cTableName, which holds the alias of the table that controls navigation. For example, for an order business class, cTableName would most likely be the Orders table even though the Order Items table is used as well. The following section discusses the events and methods.

Events and Methods  Table 17.13 presents the events and methods for Base_Business.

Table 17.13  The Events and Methods for Base_Business
Event/Method
Description
InitThe Init method selects the controlling table. Remember, because the container will initialize last, by the time this method runs the data environment should already be set up.

The next bit of code is interesting. The Init method checks to make sure that the cClass property exists on the form (it should-the check is a result of just a bit of paranoia at work) and then sets it to be the name of the business class. In other words, you do not have to play with the form when dropping a business class on it. The framework is designed to enable you to just drop a business class on the form and let it go from there.

AdditThis method adds a record to the controlling table. If the class resides on a form, the form's Refresh method is called.
BottomitThis method moves to the last record of the controlling table. If the class resides on a form, the form's Refresh method is called.
NextitThis method moves to the next record of the controlling table. If the class resides on a form, the form's Refresh method is called.
PrevitThis method moves back one record in the controlling table. If the class resides on a form, the form's Refresh method is called.
TopitThis method moves to the top record of the controlling table. If the class resides on a form, the form's Refresh method is called.

Enhancing the Framework

Now that the framework is set up, the next step is to put it to use. In this case, a customer class is created to handle customer information in the TESTDATA.DBC database.

The first step in working with a corporate framework might be to customize it slightly for the use of a department or an application. In this case, I created a subclass of the Base_Business class for the business class I will show here. The subclass specifies the name of the data environment class to load as default_de (remember, default_de is automatically generated by DumpDbc). You'll probably want to use different data environment classes for different business classes. In this case, to show how you might want to customize a framework (and to keep the business classes simple), I decided that I would always load the whole shebang. This being the case, it made sense to set it up once and subclass from there.

The subclass, called Base_TestData, also sets the name of the program to TD_DE.PRG, which is the name of a program I created with DumpDbc. Again, the point here is that you can enhance the framework for your use. In fact, you probably will to some degree. These modifications make up the department or application framework. In this case, Base_TestData would probably be part of the application framework. The code of this subclass is presented in Listing 17.13.


Listing 17.13  17CODE13.PRG-Code for the Base_TestData Subclass of the Base_Business Class
*  Class.............: Base_testdata(D:\seuvfp6\Chap17\FW.vcx)
*  Author............: Menachem Bazian, CPA
*  Notes.............: Exported code from Class Browser.

**************************************************
*-- Class:        base_testdata
*-- ParentClass:  base_business
*-- BaseClass:    container
*
DEFINE CLASS base_testdata AS base_business

    Width = 215
    Height = 58
    Name = "base_tastrade"
    base_de1.cdeprgname = "td_de.prg"
    base_de1.cdeclassname = "default_de"
    base_de1.Name = "base_de1"

ENDDEFINE
*
*-- EndDefine: base_testdata
************************************************** 

Using the Framework

Now that I have the framework just where I want it, I'll use it to create a business class for the Customer table and build a form for it. Just to illustrate how easy it is to use a framework (or at least how easy it should be), I will review the steps it takes from start to finish.

  1. Subclass the base business class, Base_Business (see Figure 17.7).
    Figure 17.7 : Step 1-The subclass before modifications.
  2. Set the cTableName property to Customer (see Figure 17.8).
    Figure 17.8 : Step 2-Setting the cTableName property.
  3. Add the GUI objects and save the class (see Figure 17.9).
    Figure 17.9 : Step 3-Adding GUI objects.
  4. Drop the business class on a subclass of the base Business form, Base_Form (see Figure 17.10). An easy way to do this is to use the Class Browser. Select the Base_Form class and click on the New Class button. Enter a name for the new subclass class and the Class Designer opens. Click on the Base_Form class and drag the Copy icon to the new form class.
    Figure 17.10: Step 4-Dropping the business class on the form.
  5. Instantiate the form by executing the following code:
     SET CLASSLIB TO FW             && Establish the class library
     oForm = CREATEOBJECT("bizcustform") && Create the form object
     oForm.show()                        && Display the form

The instantiated form appears as shown in Figure 17.11.

Figure 17.11: Step 5-Running the form.

What you have here is two classes: the Biz_Cust class, which has the business class specifics, and the form class, bizcustform. The code for these two classes is presented in Listings 17.14 and 17.15, respectively.


Listing 17.14  17CODE14.PRG-Code for the Biz_Cust Subclass of the Base_TestData Class
*  Class.............: Biz_cust (D:\seuvfp6\Chap17\FW.vcx)
*  Author............: Menachem Bazian, CPA
*  Notes.............: Exported code from Class Browser.

**************************************************
*-- Class:        biz_cust
*-- ParentClass:  base_testdata
*-- BaseClass:    container
*
DEFINE CLASS biz_cust AS base_testdata

    Width = 509
    Height = 97
    ctablename = "customer"
    Name = "biz_cust"
    base_de1.Top = 0
    base_de1.Left = 0
    base_de1.Height = 241
    base_de1.Width = 637
    base_de1.Name = "base_de1"

    ADD OBJECT text1 AS textbox WITH ;
        Value = "", ;
        ControlSource = "Customer.Company", ;
        Format = "", ;
        Height = 24, ;
        InputMask = "", ;
        Left = 60, ;
        Top = 20, ;
        Width = 433, ;
        Name = "Text1"

    ADD OBJECT label1 AS label WITH ;
        BackStyle = 0, ;
        Caption = "Name:", ;
        Height = 25, ;
        Left = 12, ;
        Top = 20, ;
        Width = 49, ;
        Name = "Label1"

    ADD OBJECT text2 AS textbox WITH ;
        Value = "", ;
        ControlSource = "Customer.City", ;
        Format = "", ;
        Height = 24, ;
        InputMask = "", ;
        Left = 60, ;
        Top = 54, ;
        Width = 113, ;
        Name = "Text2"

    ADD OBJECT label2 AS label WITH ;
        BackStyle = 0, ;
        Caption = "City:", ;
        Height = 18, ;
        Left = 12, ;
        Top = 56, ;
        Width = 43, ;
        Name = "Label2"

ENDDEFINE
*
*-- EndDefine: biz_cust
**************************************************


Listing 17.15  17CODE15.PRG-Code for the Bizcustform Subclass of the Base_Form Class
*  Class.............: Bizcustform (D:\seuvfp6\Chap17\FW.vcx)
*  Author............: Menachem Bazian, CPA
*  Notes.............: Exported code from Class Browser.

**************************************************
*-- Class:        bizcustform
*-- ParentClass:  base_form
*-- BaseClass:    form
*
DEFINE CLASS bizcustform AS base_form

    Top = 0
    Left = 0
    Height = 202
    Width = 549
    DoCreate = .T.
    Name = "bizcustform"
    base_navigation1.cmdTop.Name = "cmdTop"
    base_navigation1.cmdBottom.Name = "cmdBottom"
    base_navigation1.cmdNext.Name = "cmdNext"
    base_navigation1.cmdPrev.Name = "cmdPrev"
    base_navigation1.cmdClose.Name = "cmdClose"
    base_navigation1.Top = 156
    base_navigation1.Left = 108
    base_navigation1.Width = 328
    base_navigation1.Height = 30
    base_navigation1.Name = "base_navigation1"

    ADD OBJECT biz_cust2 AS biz_cust WITH ;
        Top = 36, ;
        Left = 24, ;
        Width = 509, ;
        Height = 97, ;
        Name = "Biz_cust2", ;
        base_de1.Name = "base_de1", ;
        Text1.Name = "Text1", ;
        Label1.Name = "Label1", ;
        Text2.Name = "Text2", ;
        Label2.Name = "Label2"

ENDDEFINE
*
*-- EndDefine: bizcustform
**************************************************

Consider the power of this approach. The developer concentrates his or her efforts in one place: the functionality and information in the business class. As for the surrounding functionality (for example, the form, the navigation controls, and so on), that is handled by the framework.

Additional Notes on the Business Class

Creating the business class based on a container has an additional benefit. Whenever a program needs to work with customers, all you have to do is instantiate the business class and call the appropriate methods. Because all the functionality related to the business class is encapsulated within the confines of the container, no other function needs to worry about how to handle the data of the business class. If you're worried about the visual component to the class, you can stop worrying. There is no law that says you have to display the business class when you instantiate it (this also goes for the form I designed for creating new data sessions).

Finally, you can use the business class on forms other than a data entry form. For example, suppose you are working on an Invoice class and you need to provide the ability to edit customer information for an invoice. All you need to do is drop the business class on a modal form.

Frameworks: A Final Word

Creating a framework is not a small undertaking. In fact, it is quite a daunting prospect. Still, in the long run, having a framework that you understand and can work with and that provides the functionality and structure you need will prove absolutely essential.

As Visual FoxPro matures in the marketplace, there will be third-party vendors offering frameworks for development. As I write these words, there are several third-party frameworks in progress. The temptation will be strong to purchase an existing framework to jump-start yourself. It would be foolhardy to state that you can only work with a framework you create. Re-creating the wheel is not usually a good idea. Besides, the popularity of frameworks, such as the FoxPro Codebook, proves my point.

Remember, though, that the framework you choose will color every aspect of your development. You will rely on it heavily. If it is robust, well documented, and standardized, you have a good chance of succeeding. Don't take this choice lightly. Subject the framework to rigorous testing and evaluation. Accept nothing on faith. If the choice you make is wrong, it can hurt you big time.

What should you look for in a framework? The criteria I discussed in Chapter 16, "Managing Classes with Visual FoxPro," relating to the class librarian's review of suggested classes will do nicely. To review, here are the criteria:

By the way, if you're wondering what happened to compatibility, I left it out because the framework is the ground level by which compatibility will be measured.

Development Standards

It is critical to develop or adopt standards for coding when working with Visual FoxPro. There is a high degree of probability that the classes you create will be used by others (unless you work alone and never intend to bring other people into your projects). In order for others to be able to use your code, there has to be a degree of standardization that permeates the code. Without these standards, you will be left with a tower of Babel that will not withstand the test of time.

What kind of standards should you implement? The following sections provide some basic categories in which you should make decisions.

Variable Naming

I like to use a form of Hungarian notation. The first character denotes the scope of the variable and the second denotes the data type.

Scope Characters
L
Local
P
Private
G
Global (public)
T
ParameTer
Type Characters
C
Character
D
Date
L
Logical
N
Numeric, Float, Double, Integer
O
Object
T
DateTime
U
Undefined
Y
Currency

If you're wondering where these characters come from, they are the values returned by the Type function. The rest of the variable name should be descriptive. Remember that you can have really long names if you like; therefore, there is no need to skimp anymore.

Naming Methods and Properties

One of the nicest features of OOP lies in polymorphism, which means that you can have multiple methods and properties of the same name even though they might do different things. To use this feature effectively, it is important that naming standards are established that dictate the names of commonly used methods.

Imagine, for example, what life would be like if one class used the method name Show to display itself, another used the name Display, and another used the name Paint. It would be pretty confusing, wouldn't it?

Where possible, decide on naming conventions that make sense for you. I think it is a good idea to adopt the Microsoft naming conventions (use Show instead of something else, for example) when available. This maintains consistency not only within your classes but also with the FoxPro base classes. By the way, naming conventions apply to properties, too. A common way to name properties is to use as the first character the expected data type in the property (using the same character identifiers shown previously). This helps use the properties too. When standards are not applicable for methods and properties (that is, they are not expected to be commonly used), try to use a descriptive name.

The Effects of a Framework on Standards

The standards you adopt should be based on your framework if possible. If you are looking to purchase a framework and the framework does not have standards with it, I would be very leery of purchasing it. I personally consider well-documented standards the price of entry for a framework.

If you purchase a framework, take a look at the standards and make sure that you can live with them. Although I wouldn't recommend living and dying by standards (rules do have to be broken sometimes in order to get the job done), you will be working by those standards 98 percent of the time (or more). Let's face it, if you didn't work by the standards almost all the time, standards would be meaningless. Make sure that the standards dictated by a framework make sense to you.


© Copyright, Sams Publishing. All rights reserved.