Special Edition Using Visual FoxPro 6
Special Edition Using Visual FoxPro 6
Advanced Object-Oriented Programming
- Data Environment Classes
- Increasing the Power of Data Environments with DataSessions
- Modeling Objects on the Real World
- Working with Frameworks
- Development Standards
- The Effects of a Framework on Standards
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.
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.
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 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
|Alias||This property is the alias to give the work area when opened.|
|BufferModeOverride||This property specifies the buffering mode for the cursor. Here are the values:
0 None (no buffering is done)
If the data environment class is not used on a form, you should use a value other than 1.
|CursorSource||This 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.|
|Database||This 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.|
|Exclusive||This property specifies whether the cursor source is open in exclusive mode. It accepts a logical value.|
|Filter||This 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.|
|NoDataOnLoad||This 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.|
|Order||This property specifies the initial order in which to display the data. You must specify an index tag name.|
|ReadOnly||This 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 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.
|ChildAlias||This property is the alias of the child table. In this example, it would be Invoice.|
|ChildOrder||This property specifies the order of the child table. In this example, it would be cCustId.|
|OneToMany||This property specifies whether the relationship is a one-to-many relationship.|
|ParentAlias||This property is the alias of the controlling alias. In this example, it would be Customer.|
|RelationalExpr||This 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 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.
|AutoCloseTables||This 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.|
|AutoOpenTables||This 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.|
|InitialSelectedAlias||This 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.
|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.
|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.
DumpDbc a subclass of the Custom class. The following sections discuss its properties, events, and methods.
Properties Table 17.6 presents DumpDbc's
|PROTECTED aRelations||This property is a list of relation objects in the DBC file.|
|PROTECTED aTables||This property is a list of table objects in the DBC file.|
|PROTECTED aViews||This property is a list of view objects in the DBC.|
|CDBCName||This property is the name of the DBC file you are exporting.|
|CPRGName||This property is the name of the PRG file to be created.|
|PROTECTED cPath||This property is the directory path to the database.|
|PROTECTED nRelations||This property is the number of relation objects in the DBC file.|
|PROTECTED nTables||This property is the number of table objects in the DBC file.|
|PROTECTED nViews||This property is the number of view objects in the DBC.|
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.
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 *-- List of view objects in the .DBC PROTECTED aviews *-- List of table objects in the .DBC PROTECTED atables 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 **************************************************
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.
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.
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.
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.
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.
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.
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
|Interval||The 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.
|Nsecs||This property is the number of seconds counted. It is carried out to three decimal places and is what the SECONDS function returns.|
|PROTECTED nlast||This property is the last time the Timer event fired. This is a protected property.|
|nstart||This property is the time the watch was started, measured in seconds since midnight.|
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.
|Init||This method initializes the nstart, Interval, nSecs, and nLast properties to zero.|
|Pause||This method calls the Timer() method to update the time counter and then stops the clock by setting the Interval property to 0.|
|Resume||This 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.|
|Start||This method starts the clock and records when the clock was started.|
|Stop||This method stops the clock and sets nLast to 0.|
|Timer||This method updates the number of seconds (nSecs) property.|
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.
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:
- lblTime (Label)
- tmrSWEngine (Swatch engine)
Custom Properties Notice that the properties
shown in Table 17.9 are properties of the container itself.
|nStart||This property is the time the watch was started, measured in seconds since midnight.|
|Nsecs||This 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.
|Swatch.Start||This method calls tmrSWEngine.Start.|
|Swatch.Stop||This method calls tmrSWEngine.Stop.|
|Swatch.Pause||This method calls tmrSWEngine.Pause.|
|Swatch.Resume||This method calls tmrSWEngine.Resume.|
|Swatch.Reset||This 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.|
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.
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.
|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.|
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.
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.
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:
- Display for editing
- Moving between records (top, bottom, next, and previous)
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:
- Navigation class This is similar to the navigation classes covered in Chapter 15. This class will be a series of command buttons for navigation within a form containing a business class.
- Form class This is a combination of a form and the navigation class and is designed to work with business objects.
- Data environment loader class This is a class that handles loading the data environment for a business class.
- Business class This class has the data and methods for the business class being modeled and has a data environment loader class in it.
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 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.
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
* 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 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.
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.
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.
|Init||The 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.|
|Chk4De||This method checks to make sure that a data environment was specified.|
|OpenDe||This 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 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.
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.
|Init||The 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.
|Addit||This method adds a record to the controlling table. If the class resides on a form, the form's Refresh method is called.|
|Bottomit||This method moves to the last record of the controlling table. If the class resides on a form, the form's Refresh method is called.|
|Nextit||This method moves to the next record of the controlling table. If the class resides on a form, the form's Refresh method is called.|
|Previt||This method moves back one record in the controlling table. If the class resides on a form, the form's Refresh method is called.|
|Topit||This method moves to the top record of the controlling table. If the class resides on a form, the form's Refresh method is called.|
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 **************************************************
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.
- Subclass the base business class, Base_Business (see
Figure 17.7 : Step 1-The subclass before modifications.
- Set the cTableName property to Customer
(see Figure 17.8).
Figure 17.8 : Step 2-Setting the cTableName property.
- Add the GUI objects and save the class (see Figure 17.9).
Figure 17.9 : Step 3-Adding GUI objects.
- 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
Figure 17.10: Step 4-Dropping the business class on the form.
- 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.
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.
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.
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.
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.
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.
|Numeric, Float, Double, Integer|
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.
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 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.