CATEGORII DOCUMENTE |
Bulgara | Ceha slovaca | Croata | Engleza | Estona | Finlandeza | Franceza |
Germana | Italiana | Letona | Lituaniana | Maghiara | Olandeza | Poloneza |
Sarba | Slovena | Spaniola | Suedeza | Turca | Ucraineana |
Visual FoxPro is a database management language, and we use it to build data-centric applications. Our applications manipulate data in many different ways. We present data for editing and/or review on screen, we produce reports on paper or on screen, we even send data or information over phone lines through fax machines and the Internet. Manipulating data is what we do. Is there any way to reduce the effort needed to manage this data? Can we create reusable data-manipulation objects so we write code only once and use it in various places? These are some of the questions that this chapter will address.
There is a common set of things that we need to do with data and the tables and views that hold it. The code for these common operations can be designed into our classes. Once we design these behaviors into our classes, theyll automatically be included in the objects we use in our applications. The whole idea is to relegate these responsibilities to our classes so that we dont need to concern ourselves with them again. We can just call on the behaviors and let the class code handle them for us. In this section of the chapter we will look at the various behaviors of concern. In later sections we will construct classes that handle these behaviors for us.
So what is included in this list of behaviors? The following sections will discuss each of these behaviors in detail:
In procedural applications, one central procedure usually opened and/or closed a table for us. This gave us the ability to centralize problems, such as deciding which directory of data to use and what options to include on the USE command line. Once it was written, we would call this procedure and pass it the name of the table we wanted to open.
With Visual FoxPro, we initially seem to have given up this ability. If we are using the form designer and its data environment, we find out that the path to the database for each table is hard-coded into the forms data environment. However, if we create our own classes for managing data, we can provide the functionality to alter the path to the database as well as provide any options we might want regarding the opening of a data source. The same things are true of closing a data source.
Detecting when some aspect of the data has changed would allow us to update the sources only if they needed to be updated. This can improve the performance of our systems. It takes time to write to a file; if we can limit that writing to when changes occur, we can reduce the overhead in our applications.
Visual FoxPro gives us the ability, in the language, to check for changes to the data. It can be as easy as using a combination of GetNextModified() and GetFldState(). We could probably do this in just a few lines of code. However, if we need those few lines of code everywhere, we might update a table or view which would add up to a lot of lines of code. By using data classes, we can centralize this responsibility and forget about it.
Every form should give the user the option of accepting or rejecting edits, and leaving the data unchanged. In Visual FoxPro, the TableUpdate() and TableRevert() functions handle this for us. Although these two functions are fairly simple, we dont want to rewrite them for every table we need to manage. By using data management classes, we can centralize this code and reuse it as needed.
Navigation through data can be simple or complex, depending on the data being presented. Also, there is the issue of views, both local and remote; and how we implement navigation with those is quite different from the way we would do it when using tables directly.
In our forms, we dont want to write complex code to handle all the possibilities. It is much better to use a data manager class that is programmed to do the necessary things and then selectively instantiate the correct data manager for the situation. By using good object-oriented design principles, we can make these data managers interchangeable, so that the code in our form is the same regardless of which data manager we use. If we have a data situation where navigation actions are not applicable, then those methods of our data manager would be empty and would faithfully do nothing if called.
As mentioned in the previous section, our data can take many different forms. We can use tables directly, we can use local views, or we can use remote views. We can even have systems where forms may change their data source at runtime.
We want to have consistent code for accessing data within our forms and controls, but these different data sources require that different code be executed to do various things with the data. Data manager classes can provide a consistent interface to our forms and controls, and still execute different code when those methods are called. We can have a data manager class for each type of data source we need and instantiate the appropriate one for the job at hand. For example you may have the need to deal with test data and real data in your application or you may supply tutorial data for training purposes. Using a data manager can allow you to provide for these different data sources.
Before we venture into a discussion of data manipulation classes, lets take a moment to investigate the data-buffering capabilities of Visual FoxPro. To understand buffering, we can look back at our experiences with earlier versions of FoxPro. Many of us had the habit, in FoxPro 2.x, of using memory variables for editing data. This was because we wanted the ability to decide whether or not the users changes were written to the table. The assumption, at that time, was that if we connected the data-entry screen to the fields in the table, we were directly editing the record in the table. If you ever watched the disk drive light, youd have to conclude that this wasnt true. If the editing were truly direct to disk, then the drive light would have flashed every time the user pressed a key, but it did not. That means that FoxPro must have been working in some kind of a memory buffer.
The problem we had in FoxPro 2.x was that we weren't given any control over when and if that memory buffer would be committed to disk. We knew it would be committed sooner or later, and that was that. Visual FoxPro has given us access to control that memory buffer. We can now control when and if the memory buffer will be written to the table. This allows us the code simplicity of 'direct editing' but we still have the control of selectively updating.
Visual FoxPro provides five types of buffering. The first type is none, which causes the data to be handled the same way it was in FoxPro 2.x. The other four provide options for buffering optimistically or pessimistically, and to buffer a single row at a time, or the entire table.
The table vs. row aspect of buffering controls the amount of pending data that will be held in the buffer for a table or view. If buffering is set to row, then only one record can be buffered at a time; a setting of table will allow multiple records to be buffered simultaneously for that table or view.
The optimistic and pessimistic options control the multi-user aspects of the data buffering. Optimistic buffering doesn't lock records while the data is being edited, and it doesn't prevent multiple users from working on the same records simultaneously. Instead, the optimistic setting locks the records when we attempt to update the table from the buffer, and it detects any logical data conflicts at that time. Pessimistic buffering works the other way. It locks the record when a user begins to edit it and won't allow any other user to begin editing until the lock is released.
By combining these two settings, table/row and optimistic/pessimistic, we get four buffering modes: Optimistic Row, Pessimistic Row, Optimistic Table, and Pessimistic Table. Add these four to the setting for no buffering at all, and we see the five buffering settings.
Which buffering mode do I use?
If we were to survey all Visual FoxPro developers, we would probably find that there is almost an even split between the optimistic and pessimistic school of thought. In fact, there is no 'correct' oneif there were, Microsoft would not have given us a choice.
We need to understand clearly how the two approaches work and then use the one that is most appropriate for our requirements. Table 1 shows a comparison of the optimistic and pessimistic options.
Table 1. Comparison of optimistic and pessimistic buffering approaches.
Issue |
Optimistic |
Pessimistic |
Resource Contention |
Does not hold locks at the server during editing. |
Holds a lock at the server for every edited record until that record is either committed or reverted. |
Conflict Detection |
Potential conflicts are detected at the time of committing the record to the table. |
Attempts to insure write rights by locking records before the editing is allowed to begin. There is still a possibility of a failure to update caused by other issues like network cable failure, or server crash, etc. |
Programming |
Update routines must sense any conflict at the time of commit and respond to them in some way. |
Routines that begin an edit must sense errors generated if a record is not available and respond to that occurrence. |
You can see in Table 1 that neither the optimistic nor pessimistic approach to buffering is really any better than the other. They both manage multi-user conflict issues, but they do it in different ways. Each one has its own set of requirements for our code.
The other issue related to buffering is the row vs. table setting. This one does have a 'better' approach, depending on the situation. Lets first consider overall buffering. When the buffer mode is set to row, Visual FoxPro will allow one record to be 'dirty' (meaning that the record has pending changes) at a time. To enforce this one-record limit, Visual FoxPro will attempt to update the table whenever the record pointer is moved in that work area. If you think about this, you can see that it is a potential problem. If we want buffered editing and we choose a row-buffering mode, we must ensure that no operation that ever occurs during an edit will attempt to move the record pointer in the work area of any cursor that has pending changes. If the record pointer is moved, there will be an automatic attempt to commit the changes. This automatic updating defeats the whole purpose of buffering the edit in the first place, which was for us to take control of writing or reverting the edit.
It isn't necessary to use row buffering to limit edits to one record at a time. We can limit the number of records edited in the way we program our forms. Therefore, we can use table buffering, preventing any automatic updating, and still restrict the user to editing one record at a time.
Row buffering is required in one situation: when trying to create an index on a view. If a view is table buffered and we issue an INDEX ON command, Visual FoxPro will generate an error. We cannot index a table-buffered view; therefore, it's necessary to put a view into row buffering while we create any indexes and then change it to table buffering once the indexes have been built. We can use the CursorSetProp() function to do this:
* Indexing a view
LOCAL lnBufferMode
SELECT TheView
lnBufferMode = CursorGetProp( 'BUFFERING', 'TheView' )
CursorSetProp( 'BUFFERING', 3, 'TheView' )
INDEX ON Name TAG Name
CursorSetProp( 'BUFFERING', lnBufferMode, 'TheView' )
Buffering is the technology in Visual FoxPro that we use to manage control of data editing for a single cursor. To coordinate updating multiple cursors, we use transactions, which are described later in this chapter.
There are many approaches we can take to provide the functionality we are after. We can use the Visual FoxPro base classes or we can build our own classes. We can use the classes that our forms are based on and put the data-handling code in those classes. We will examine each of these approaches and see the benefits and problems associated with them.
The final decision of which approach is best will be dependent on the application we are building and its requirements. Some approaches are simpler to build but less flexible, while others are extremely flexible but require more up-front work in planning and design.
Before we can investigate creating our own data manager classes, we need to look at the data classes that are native to Visual FoxPro. Visual FoxPro provides three base classes that directly manage data: DataEnvironment, Cursor, and Relation. Each of these classes is described in detail in the following sections.
DataEnvironment class
Visual FoxPros DataEnvironment class is the container that manages the data for a FormSet, Form, Report, or Toolbar class. The DataEnvironment can open tables and views automatically or on command from your form code. The classes that a DataEnvironment can contain are limited to Cursor and Relation.
The following tables list the properties, events, and methods that are native to the DataEnvironment class with a description of their purpose. Table 2 lists the properties.
Table 2. Properties of the DataEnvironment class.
Property Name |
Purpose |
AutoCloseTables |
Setting this property to .T. (default) will cause the DataEnvironment to close its cursors on destruction of itself. A setting of .F. will not automatically close the cursors, and the closing must be done programmatically. |
AutoOpenTables |
Similar to the AutoCloseTables except that it controls the opening of the tables. |
Class |
Contains the name of the class that the DataEnvironment is created from. |
ClassLibrary |
The name of the ClassLibrary that the DataEnvironment class is contained in. |
Comment |
Programmers comment text. |
InitialSelectedAlias |
Selects an alias associated with one of the DataEnvironments cursors to be the initially selected work area on loading the DataEnvironment, |
Name |
The name of the DataEnvironment object. |
OpenViews |
Controls the opening of any views that may be in the DataEnvironment. Accepts one of four settings numbered from 0 to 3. 0 Opens all views automatically, 1 Opens only local views, 2 Opens only remote views, and 3 Opens no views automatically. Any views that are not opened automatically must be opened programmatically |
Parent |
An object reference to the parent container of the DataEnvironment. For a DataEnvironment contained within a form, the DataEnvironments parent property would be an object reference to the form. |
ParentClass |
The parent class that this class inherits from. |
Tag |
A character property that can be used by the programmer to hold any extra data that the programmer may need. |
Table 3 shows the methods of the DataEnvironment class. A few methods have been left out of this list because they are not related to the data aspects of the class: ReadExpression, ReadMethod, WriteExpression, and SaveAsClass.
Table 3. Methods of the DataEnvironment class.
Method |
Purpose |
AddObject |
Used to add an object to the DataEnvironment at runtime. |
AddProperty |
Used to add a property to the DataEnvironment at runtime. |
CloseTables |
Closes the tables and views in the data environment. Automatically called if the AutoCloseTables property is set to .T.; must be explicitly called if the AutoCloseTables property is set to .F. |
NewObject |
Adds an object to the DataEnvironment at runtime. The difference between this method and the AddObject method is that with AddObject the class library for the object being added must be open prior to using the AddObject. With NewObject the class library can be named in the call to NewObject thereby obviating the need to open the class library separately. |
OpenTables |
Opens the tables and views. Like CloseTables, it respects the setting of AutoOpenTables. If AutoOpenTables is set to .T. the OpenTables method is automatically called upon creation of the Data Environment. If AutoOpenTables is set to .F. then the OpenTables method must be called explicitly. |
RemoveObject |
Removes a cursor or relation object from the DataEnvironment object. |
Table 4 lists the DataEnvironments event methods.
Table 4. Event methods of the DataEnvironment class.
Event Method |
Purpose |
AfterCloseTables |
Fires after the tables and/or views of the DataEnvironment have been closed. Fires after the CloseTables method finishes and before the Destroy event for the DataEnvironment. For a Form or FormSet, this event fires after the Unload of the Form or FormSet. |
BeforeOpenTables |
Fires just before the tables and/or views of the DataEnvironment are opened. In a Form or FormSet this event fires before the Forms or FormSets Load event. |
Destroy |
Fires during the destruction of the DataEnvironment. The Destroy for the DataEnvironment fires before the Destroy of its contained objects, thereby allowing the code in the Destroy event to refer to the contained objects. |
Error |
Fires whenever an error occurs in the code of any of the methods or events for this object. |
Init |
Executes once on creation of the DataEnvironment. |
The OpenTables and CloseTables methods can act like event methods when the respective Auto properties are set to .T. That is, they will run automatically during creation and destruction of the DataEnvironment object.
It is important to understand the firing order of events during the creation and destruction of the DataEnvironment. Tables 5 and 6 show the creation order and the destruction order, respectively. Both of these tables assume that the AutoOpenTables and AutoCloseTables are set to .T. The event sequence is for a form named form1.
Table 5. The sequence of events during form
creation with a DataEnvironment.
Event |
form1.dataenvironment.opentables() |
form1.dataenvironment.beforeopentables() |
form1.load() |
form1.dataenvironment.cursor1.Init() |
form1.dataenvironment.Init() |
form1.Init() |
Table 6. The sequence of events during form
destruction with a DataEnvironment.
Event |
form1.Destroy() |
form1.Unload() |
form1.dataenvironment.CloseTables() |
form1.dataenvironment.AfterCloseTables() |
form1.dataenvironment.Destroy() |
form1.dataenvironment.cursor1.Destroy() |
An important thing to note in the creation sequence is that the OpenTables method is called before the BeforeOpenTables event fires. This is because the BeforeOpenTables event fires just before the tables and views are opened. Because the OpenTables method opens them, the BeforeOpenTables event will not be fired until after the OpenTables method has started. BeforeOpenTables fires only in response to an execution of the OpenTables methodeither automatically as a result of the AutoOpenTables property being .T. or when the OpenTables method is called in code.
Data sessions
Visual FoxPros concept of data sessions should not be confused with the DataEnvironment. A data session in Visual FoxPro is a set of work areas where tables and views can be opened and manipulated. Visual FoxPro starts in the command window with the default data session, which is numbered 1. Forms, formsets, reports, and toolbars can have what is called a private data session. When one of these objects requests a private data session from Visual FoxPro, a new, separate set of work areas is created and given a new data session number. The default data session (1) still exists, but the object with the private data session is manipulating its own set of work areas and does not affect the default or any other data session that might exist.
Using private data sessions can allow great flexibility in the options you give your users. Just as multiple users in an application each can access data in the app in their own way, private data sessions allow a user to open more than one copy of a form and work on different records in each copy simultaneously, without those forms interfering with each other.
To understand this, imagine that we have a form with a private data session. This form opens some tables and does some work to prepare data for a report. When we build the report, do we choose the default data session or a private one? If we give the report a private data session, it wont be able to see the tables in the forms data session, and we just set those up for the report. We might suspect that choosing the default data session is equally poor thinking, that it would use the command windows data session, but we would be wrong. Because the report is running from code inside the form, the forms data session is the default data session for that report. Therefore, if we build the report to use the default data session and then run that report from inside the form, the report will use the forms data session.
Cursor class
The Visual FoxPro cursor object is derived from the cursor class, and can be contained within a DataEnvironment object. This object controls access to a table or a view. We can use a number of properties and events associated with a cursor object to affect its nature and behavior. Tables 7, 8, and 9 list and describe the properties, methods, and events respectively. Again, the ReadExpression, ReadMethod, WriteExpression, and SaveAsClass methods are not listed.
A little preface to the properties of cursor objects: Trying to change the value of any cursor property except for Order and Filter at runtime will cause an error. To change a property other than Order or Filter at runtime, you must first call the DataEnvironments CloseTables method, then make your changes, and finally call the DataEnvironments OpenTables method. Remember, this means that all tables will be closed and reopened, which may take some management. We will present example code later in the chapter.
Table 7. Properties of the cursor class.
Property |
Description |
Alias |
Contains the alias name for this cursor. Usually the alias name will default to the same name as the table or view that is the CursorSource for this cursor. This property can be modified to cause the cursor to have any alias name you like. |
BaseClass |
The name of the BaseClass, Cursor, for this object. |
BufferModeOverride |
This is the property that affects the buffer mode for the cursor. There are six possible settings: |
0 - No buffering at all |
|
1 - Use the BufferMode of the form |
|
2 - Pessimistic Row Buffering |
|
3 - Optimistic Row Buffering |
|
4 - Pessimistic Table Buffering |
|
5 - Optimistic Table Buffering |
|
These buffer mode settings were described earlier in this chapter. Setting 1 is ambiguous because the form can only have optimistic or pessimistic buffering set with no regard to table or row. It is suggested that a setting of 1 not be used for this property. |
|
Class |
The name of the class that this object was created fromusually Cursor, but it could be a subclass of Cursor, as we will see later. |
ClassLibrary |
The ClassLibrary where the class is defined. |
Comment |
Programmers comments about this cursor. |
CursorSource |
The name of the table or view that this cursor is related to. In the case of a table in a database this will be the long table name; for free tables this will be the complete path and table name. |
Database |
The name of the database that the table or view is stored in. Includes the complete path. |
Exclusive |
Logical property that controls whether this cursor is opened exclusively or in shared mode. A value of .T. causes the cursor to be opened exclusively. |
Filter |
This property stores an expression that is used as a filter on the data in the cursor. For example, if we wanted to filter a cursor to show only those records where the field State had a value of 'NY' we would put [State = 'NY'] in this property without the square brackets. This is one of two properties that can be changed at runtime without causing an error. |
Name |
The name of this cursor object. Do not confuse this property with the Alias property. When you want to select the work area where this cursor is open, use Alias; when you want to manipulate this cursor object, such as modifying the Filter property, use Name. |
NoDataOnLoad |
This property applies only to cursors that have a view as the CursorSource. If this property is set to .T. then the views structure will be open but no data will be obtained for the view. This can allow you to open a view without running the query that populates it. You would then need to use the Requery() function to fetch data at a later point in time. Setting up a view this way can be very helpful, especially with parameterized views. Using NoDataOnLoad true ??? Does this make sense? NoDataOnLoad true??? will prevent the dialogs asking for the parameter values from being shown to the user as the form is created. You can set the parameter values later and then Requery() the view. This can also speed up the creation of indexes on a view since you would be able to index a view that has no records, then fetch the data. |
Order |
This property contains the name of the index tag that is controlling the order of the view. This is the second property that you can freely change at runtime. Keep in mind, though, that changing the order on a cursor may break relations that are set, and will move the record pointer in that cursor to the first logical record. |
Parent |
This is an object reference to the DataEnvironment that contains this cursor object. |
ParentClass |
If this cursor object is derived from a subclass, this property contains the name of the parent class. |
ReadOnly |
This logical property controls the read-only vs. read/write status of the cursor. Setting this property to .T. will disallow any data from being written to the cursor. If you set ReadOnly to .T. for a view, either local or remote, you will not be able to use the Requery() function on that view. Your only recourse to repopulating the view will be to close it and then reopen it. |
Tag |
A character property that can be used by the programmer to hold any extra data that the programmer may need. |
Table 8 describes the methods for a cursor object.
Table 8. Methods for the cursor object.
Method |
Description |
AddProperty |
Allows the addition of new properties to the object at runtime. |
Table 9 lists the events for the cursor object.
Table 9. Events for the cursor object.
Event Method |
Description |
Destroy |
Fires during the destruction of the Cursor object and before the cursor is completely gone from memory. |
Error |
Fires whenever an error occurs in the code of the object's methods or events. |
Init |
Executes once on creation of the Cursor object. |
So far we have cursors that are associated with tables or views and a DataEnvironment that can contain those cursors. What more do we need? Well, we need a way to establish relations between the cursorsand that is the purpose of the Relation class.
Relation class
As we examine the properties of this class, we'll see that it maps very easily to what we already know about the SET RELATION command in Visual FoxPro.
Table 10 lists the properties of the Relation class. For the sake of clarity, we have omitted describing again those properties already listed for the DataEnvironment and Cursor classes.
Table 10. Properties of the Relation class.
Property |
Description |
ChildAlias |
The alias name for the cursor that is the target of the relation. |
ChildOrder |
The name of the index tag for the target of the relation. |
OneToMany |
Logical where a value of .T. is equivalent to the SET SKIP TO command. |
ParentAlias |
The alias name of the source of the relation. |
RelationalExpr |
The expression that the relation is based on. |
As an example of using the properties of the Relation object, assume we have two cursors with the alias names of Customer and Invoice. The primary key for the Customer cursor is CustID, and the index tag name for the Invoice table that is keyed on CustID is InvCust. The following code shows the values of the Relation object's properties:
Relation.ChildAlias = 'Invoice'
Relation.ChildOrder = 'InvCust'
Relation.ParentAlias = 'Customer'
Relation.RelationalExpr = 'CustID'
Of course, if we wanted this relationship to be a one-to-many relation, we would set the OneToMany property to .T.
Subclassing the native Visual FoxPro data management classes
As with any base class in Visual FoxPro, we can subclass the data classes. The limitation is that we cannot do it visually with the class designerit must be done in code with a program file. This isn't really a limitation on what we can do with our subclasses; it is only a limitation on how we can build them. In a subclass of these data classes, we can add new properties and methods, we can provide default code for the existing events and methods or for our new methods, and we can provide default values for the propertiesboth the native ones and any new ones we may have added. Listing 7-1 shows an example of subclassing the DataEnvironment class.
Listing 7-1. An example of subclassing a DataEnvironment.
DEFINE CLASS deBase AS DataEnvironment
* Set automatic opening of tables to false
AutoOpenTables = .F.
* Modify the Init to accept a path to the databases
PROCEDURE Init
LPARAMETERS pcDbcPath
LOCAL lnCursors, laCursors(1), loObjName, kursor
IF EMPTY( pcDbcPath )
* Problem, the path is no good, so leave things alone
RETURN
ENDIF
IF TYPE( 'pcDbcPath' ) <> 'C'
* Problem, the path is the wrong data type, so leave things alone
RETURN
ENDIF
* Remove all leading and trailing spaces
pcDbcPath = ALLTRIM( pcDbcPath )
* Make sure the path ends with a
* You can use VFP 6.0s new ADDBS() function here
pcDbcPath = ADDBS(pcDbcPath)
* Get a list of the cursors
lnCursors = AMEMBERS( laCursors, THIS, 2 )
* If there aren't any, get out
IF lnCursors = 0
RETURN
ENDIF
* Now work through the cursors
FOR EACH kursor IN laCursors
loObjName = EVALUATE( 'THIS.' + kursor )
IF loObjName.BaseClass <> 'Cursor'
ENDIF
WITH loObjName
IF NOT EMPTY( .Database)
* If this cursor has a database assigned
.Database = pcDbcPath + SUBSTR( .Database;
RAT( '', .Database ) + 1 )
ENDIF
ENDWITH
ENDFOR
* Now open the cursors
THIS.OpenTables()
ENDPROC
ENDDEFINE
This example isn't really usable because there is no way to add the cursors to this DataEnvironment. Its purpose is simply to act as a base from which we will further subclass and add other things. In object-oriented design, this type of class is referred to as abstractthat is, a class that will never be used to create an object.
There are a few things to notice in this class definition. First, note the spelling of the variable used to refer to the cursors in our list. We used the spelling kursor (with a 'k') to avoid using the Visual FoxPro keyword cursor, because it's always a good idea to avoid using keywords for variable, field, table, and file names. This prevents some bugs from showing up that can be very difficult to nail down later.
Also, we used the AMEMBERS() function to get the list of cursors. We need to do this because the DataEnvironment class has no collection of its contained members like other containers. Once the list is obtained, we use the FOR EACH construct to work through the list. The line
loObjName = EVALUATE( 'THIS.' + kursor )
is creating an object reference out of the name of the cursor object. We need to do this because, for any row in our list of cursors, the value is the object name and we need a reference to the object. We must prepend the cursors name with its containers name to get that object reference. We use the indirect reference of THIS to refer to the DataEnvironment without knowing what its name might be. When this line is executed, the variable loObjName will be an object reference to the cursor we are working with, and it allows us to continue with setting the Database property.
We now have a DataEnvironment class that will set the database path for our cursors at runtime. Now lets create a subclass of this class and try to get some data into the DataEnvironment. Listing 7-2 shows a class definition for a subclass of our deBase class.
Listing 7-2. A subclass of our deBase DataEnvironment class.
* Define
DEFINE CLASS deClntProj AS deBase
* Modify the Init to accept a path to the databases
PROCEDURE Init
LPARAMETERS pcDbcPath
* Add cursors and relation
WITH THIS
* Add the Clients cursor
.AddObject('Cursor1','Cursor')
WITH .Cursor1
.CursorSource = 'Clients'
.Database = '.Datatime and billing.dbc'
.Alias = 'Clients'
.Order = 'LastName'
ENDWITH
* Add the projects cursor
.AddObject('Cursor2','Cursor')
WITH .Cursor2
.CursorSource = 'Projects'
.Database = '.Datatime and billing.dbc'
.Alias = 'Projects'
.Order = 'ClientID'
ENDWITH
* Add the one-to-many relation
.AddObject('Relation1','Relation')
WITH .Relation1
.ParentAlias = 'Clients'
.ChildAlias = 'Projects'
.ChildOrder = 'ClientID'
.RelationalExpr = 'iClientId'
.OneToMany = .T.
ENDWITH
ENDWITH
DoDefault( pcDbcPath )
ENDPROC
ENDDEFINE
This DataEnvironment (DE) class uses the tables in the Time and Billing database that are on the CD for this book. In the Init method of the object, we add the cursors and relation to the DE and set the properties for them. At the end of the Init method for this class, we call the Init method of the deBase class with the DoDefault() function. We know that the deBase Init method will take the passed parameter and adjust the path to the databases for each cursor and then call the OpenTables() method to open the cursors for use.
Regardless of how we might get this DE into a form, we can already see some problems with this approach. The first problem is that we will need a subclass for every conceivable DE that a form may need, and these DEs must be programmed in code. That completely precludes getting any benefit from the visual designer that Visual FoxPros form designer gives for building DEs.
Lets see if there is a way to reap the benefits of having our own data-management class while still retaining the ability to visually design our DEs.
We have seen in the previous section that we can, in fact, subclass the Visual FoxPro data-management base classes. We can add useful behavior to them through this subclassing. However, we found a few problems with that approach, including the requirement to create DataEnvironment classes for every data configuration a form might need. Another problem was the preclusion of ever using the form designers DataEnvironment designer.
This second problem is more far-reaching than we might think at first. If we don't use the form designers DE designer, we give up the value of field mapping, which allows us to connect a data type with a specific control class and have that class instantiated whenever we drag and drop a field of that data type onto our form. If we are not using the visual DE designer, then we aren't dragging and dropping our fields onto the form, so this feature is lost.
Is there a way we can get the benefits of a smart data-manager class and still retain the benefits of using the visual tools and their features? The answer is yes.
In order to accomplish our goal, we will design a data-management approach that spreads the responsibility of the data over a number of classes. Because we want to use the features of the form designer, we'll start our data system with a form class named frmData.
Data form class
The data form class will be responsible for the behaviors listed in Table 11.
Table 11. Responsibilities of our data-aware form class.
Data Behavior |
Provide the data behaviors |
Read and save the contents of the DataEnvironment |
Clear out the DataEnvironment |
Reconstruct the DataEnvironment using our Cursor and Relation classes |
To accomplish these goals, we need to enter into the process at a point during form creation where the DE exists and the controls are not yet bound to their data. This is because, in the process of meeting these responsibilities, we will be destroying the DE. This could cause problems if the controls are already data bound. The form's Load event will fire at the point in time that we need it.
Setting the path
We will support the dynamic setting of data paths by giving our form class a property to be used to point to the path for the data. To handle the directory for our data, we will add a property named DataDirectory to the data-aware form class.
Listing 7-3 shows the code we put in our data-aware form classs Load event.
Listing 7-3. The frmData classs Load event.
* This is our data aware form class
* Close the tables in the DE
THISFORM.DataEnvironment.CloseTables()
* Now we read the current DE into an array structure
LOCAL laDEStuff(1), laCursors(1,12), laRelations(1,8), loObject, lnRow, loParms, lnCnt
WITH THISFORM
* Check to see if there is a data environment
IF TYPE('THISFORM.Dataenvironment.Name') = 'C'
* Get a list of all objects using two arrays, one for cursors and one
for relations
AMEMBERS( laDEStuff, .Dataenvironment, 2 )
FOR lnCnt = 1 to alen(laDEStuff,1)
loObject = EVALUATE( 'THISFORM.Dataenvironment.' + laDEStuff(lnCnt))
IF loObject.BaseClass = 'Cursor'
* Populate the columns with the cursor properties
IF NOT EMPTY( laCursors(ALEN(laCursors,1),1) )
* If the last row is full add a new one
DIMENSION laCursors( ALEN(laCursors,1)+1, ALEN(
laCursors, 2 ) )
ENDIF
* Get the row number
lnRow = ALEN( laCursors,1)
laCursors(lnRow,1) = loObject.Name
laCursors(lnRow,2) = loObject.Alias
laCursors(lnRow,3) = loObject.BufferModeOverride
laCursors(lnRow,4) = loObject.Comment
laCursors(lnRow,5) = loObject.CursorSource
laCursors(lnRow,6) = loObject.Database
laCursors(lnRow,7) = loObject.Exclusive
laCursors(lnRow,8) = loObject.Filter
laCursors(lnRow,9) = loObject.NoDataOnLoad
laCursors(lnRow,10) = loObject.Order
laCursors(lnRow,11) = loObject.ReadOnly
laCursors(lnRow,12) = loObject.Tag
ENDIF
IF loObject.BaseClass = 'Relation'
* Populate the columns with the relation properties
IF NOT EMPTY( laRelations(ALEN(laRelations,1),1) )
* If the last row is full add a new one
DIMENSION laRelations( ALEN(laRelations,1)+1,
ALEN( laRelations, 2 ) )
ENDIF
* Get the row number
lnRow = ALEN( laRelations,1)
laRelations(lnRow,1) = loObject.Name
laRelations(lnRow,2) = loObject.ChildAlias
laRelations(lnRow,3) = loObject.ChildOrder
laRelations(lnRow,4) = loObject.Comment
laRelations(lnRow,5) = loObject.OneToMany
laRelations(lnRow,6) = loObject.ParentAlias
laRelations(lnRow,7) = loObject.RelationalExpr
laRelations(lnRow,8) = loObject.Tag
ENDIF
ENDFOR
ENDIF
* Ok now we have read the members of the DE let's replace them with
* our own classes
WITH .DataEnvironment
* For each object in the DE call the RemoveObject
FOR lnCnt = 1 TO ALEN( laDEStuff )
.RemoveObject( laDEStuff( lnCnt ) )
ENDFOR
* Now let's add our classes
* First we create an object for parameter passing.
loParms = CreateObject('Cursor')
FOR lnCnt = 1 TO ALEN( laCursors,1 )
loParms.Alias = laCursors(lnCnt,2)
loParms.BufferModeOverride = laCursors(lnCnt,3)
loParms.Comment = laCursors(lnCnt,4)
loParms.CursorSource = laCursors(lnCnt,5)
IF NOT EMPTY(THISFORM.DataDirectory)
THISFORM.DataDirectory = ADDBS(THISFORM.DataDirectory)
loParms.Database = THISFORM.DataDirectory + ;
SUBSTR(laCursors(lnCnt,6),RAT('',laCursors(lnCnt,6))+1)
ELSE
loParms.Database = laCursors(lnCnt,6)
ENDIF
loParms.Exclusive = laCursors(lnCnt,7)
loParms.Filter = laCursors(lnCnt,8)
loParms.NoDataOnLoad = laCursors(lnCnt,9)
loParms.Order = laCursors(lnCnt,10)
loParms.ReadOnly = laCursors(lnCnt,11)
loParms.Tag = laCursors(lnCnt,12)
.NewObject( laCursors(lnCnt,1), THISFORM.CursorClassName, ;
THISFORM.CursorClassLibrary,, loParms )
ENDFOR
loParms = CreateObject('Relation')
FOR lnCnt = 1 TO ALEN( laRelations,1 )
loParms.ChildAlias = laRelations(lnCnt,2)
loParms.ChildOrder = laRelations(lnCnt,3)
loParms.Comment = laRelations(lnCnt,4)
loParms.OneToMany = laRelations(lnCnt,5)
loParms.ParentAlias = laRelations(lnCnt,6)
loParms.RelationalExpr = laRelations(lnCnt,7)
loParms.Tag = laRelations(lnCnt,8)
.NewObject( laRelations(lnCnt,1), THISFORM.RelationClassName, ;
THISFORM.RelationClassLibrary,, loParms )
ENDFOR
* Now open the tables
IF .AutoOpenTables
.OpenTables()
ENDIF
ENDWITH
ENDWITH
Let's look at this code in some detail to see exactly what it's doing. The first thing we do is to close all tables in the existing DataEnvironment of the form. Then we build an array of all member objects, the cursors and relations, which are in the DataEnvironment. We work through that array and build two separate arraysone for the cursors and one for the relations. In each of these arrays we are storing the value for all pertinent properties for each object.
Once we have the objects stored into the arrays, we remove each object from the DataEnvironment and replace it with an object based on our own class definitions. We are using a parameter object to pass the property settings to each object as we create it.
Notice that we are using the new VFP 6.0 method named NewObject() to accomplish this. This new method allows us to create objects from classes without using a SET PROCEDURE or SET CLASSLIB command to first open the class definitions. With NewObject, we simply pass the name of the library as an argument to the function. Notice, also, that we are using properties of the form to hold the class names and the library names. This allows us the flexibility to use different classes in different forms by simply changing the value of those form properties. In frmData these properties are set to default values of cursor class csrBase in library prgsdataclasses.prg, and relation class of relBase in the same library.
Finally, we check the AutoOpenTables property of the DataEnvironment; if it is True, we call OpenTables to open all tables for the form.
Cursor and Relation classes
Because our code in the form class is using certain cursor and relation classes, we should look at those class definitions. Listing 7-4 shows the code for the csrBase class.
Listing 7-4. The csrBase class definition.
DEFINE CLASS csrBase AS Cursor
PROCEDURE Init
LPARAMETERS poParms
WITH THIS
.Alias = poParms.Alias
.BufferModeOverride = poParms.BufferModeOverride
.Comment = poParms.Comment
.CursorSource = poParms.CursorSource
.Database = poParms.Database
.Exclusive = poParms.Exclusive
.Filter = poParms.Filter
.NoDataOnLoad = poParms.NoDataOnLoad
.Order = poParms.Order
.ReadOnly = poParms.ReadOnly
.Tag = poParms.Tag
ENDWITH
ENDPROC
ENDDEFINE
The csrBase class simply accepts the parameter object and then sets its own properties to the values in the parameter object. Later in this chapter, we'll examine how we might enhance this class to provide more functionality.
Listing 7-5 shows the class definition of the relBase class used in our frmData class.
Listing 7-5. The relBase class definition.
* Our base relation class
DEFINE CLASS relBase AS Relation
PROCEDURE Init
LPARAMETERS poParms
WITH THIS
.ChildAlias = poParms.ChildAlias
.ChildOrder = poParms.ChildOrder
.Comment = poParms.Comment
.OneToMany = poParms.OneToMany
.ParentAlias = poParms.ParentAlias
.RelationalExpr = poParms.RelationalExpr
.Tag = poParms.Tag
ENDWITH
ENDPROC
ENDDEFINE
This relation class is essentially the same as the csrBase class, except that it's a relation rather than a cursor. It accepts a parameter object and sets its properties according to those of the parameter object.
Enhancing the cursor class
To really appreciate the power and flexibility of this approach to handling data, we need to investigate enhancing our cursor classes. We will add two methods to our cursor classes, named CursorUpdate and CursorRevert. Listing 7-6 shows the new cursor class definition.
Listing 7-6. Our enhanced cursor class.
* Now for an enhanced cursor class
DEFINE CLASS csrUpdate AS csrBase
PROCEDURE CursorUpdate
LOCAL llRet
IF CURSORGETPROP('BUFFERING',THIS.Alias) > 1
llRet = TableUpdate( 0,.F., THIS.Alias )
ELSE
llRet = .T.
ENDIF
RETURN llRet
ENDPROC
PROCEDURE CursorRevert
LOCAL llRet
IF CURSORGETPROP('BUFFERING',THIS.Alias) > 1
llRet = TableRevert( .T., THIS.Alias )
ELSE
llRet = .T.
ENDIF
RETURN llRet
ENDPROC
ENDDEFINE
The enhancement here is minor, but it does show the available possibilities. Our enhancement simply provides one method for updating and another for reverting the cursors buffer. The code in each method first checks to see whether the cursor is buffered and then either calls the appropriate function and returns the result, or returns True, indicating a successful completion of the operation.
Adding these methods to the class is only one example of what can be done. Making other enhancements to these classes is left as an exercise to the reader. (Weve always wanted to say that!)
Data behavior class
In the classes we've already designed, we have dealt with a number of the data behaviors presented at the beginning of this chapter: opening and closing tables and views, updating or reverting data edits, and providing data source independence. There are two behaviors left unanswered: navigating through the data and detecting changes to the data. We will address these behaviors in the data behavior class as well as expand on the 'saving of changes' behavior.
Why not implement these behaviors in the classes we've already defined? We could use the frmData class and add methods to it for these behaviors. It would also reduce the number of objects that exist at runtime. As a matter of fact, we are going to implement these behaviors in our frmData class.
The issue of whether we use the frmData class or define another class is an implementation detail and not a design issue. The design issue is that we have a class that handles these behaviorsnot which class that is.
Dividing the behaviors
We have two behaviors left to handle; lets address navigation first. Navigation involves moving through the data forward or backward, and it may involve searching for a particular value or criteria. It may involve a multi-table situation, a single-table situation, local views, or remote views.
We can start by designing the interface to the behaviors. What methods will exist and how are they called? There are five basic navigation operations: move to the next record, move to the previous record, move to the last record, move to the first record, and search for a record. These behaviors can be presented by creating the methods shown in Table 12.
Table 12. The data-manipulation methods of our frmData class.
Method |
Description |
GoNext |
Moves to the next record in the MasterAlias of the form or the InitialSelectedAlias of the DE and refreshes the IsAtEof property of the form. The MasterAlias is something that we will add to our frmData class later. |
GoPrevious |
Moves to the previous record in the MasterAlias of the form or the InitialSelectedAlias of the DE and refreshes the IsAtBof property of the form. |
GoBottom |
Moves to the last record in the MasterAlias of the form or the InitialSelectedAlias of the DE and refreshes the IsAtEof property of the form. |
GoTop |
Moves to the first record in the MasterAlias of the form or the InitialSelectedAlias of the DE and refreshes the IsAtBof property of the form. |
GoSearch |
Brings up a search form, allowing the user to search for a record by various criteria, and refreshes both the IsAtEof and IsAtBof properties of the form. |
SetEof |
Sets the IsAtEof property. |
SetBof |
Sets the IsAtBof property. |
Note that Table 12 lists two additional methods, SetEof and SetBof. These methods will be used to set logical properties of frmData to indicate that the record pointer is at the logical last and/or first record, respectively.
In addition to the methods mentioned in Table 12, we will need some new properties to keep track of record positions and also to enable the developer to turn certain behaviors on or off for particular forms. Table 13 lists the properties we will add to our frmData class.
Table 13. Properties used by the navigation behaviors of the frmData class.
Property |
Description |
IsAtEof |
Logical .T. if the record pointer in the MasterAlias or the InitialSelectedAlias of the DE is on the last record in the table. |
IsAtBof |
Logical .T. if the record pointer in the MasterAlias or the InitialSelectedAlias of the DE is on the first record in the table. |
AllowGoNext |
Logical for enabling or disabling the GoNext method. |
AllowGoPrevious |
Logical for enabling or disabling the GoPrevious method. |
AllowGoBottom |
Logical for enabling or disabling the GoBottom method. |
AllowGoTop |
Logical for enabling or disabling the GoTop method. |
AllowGoSearch |
Logical for enabling or disabling the GoSearch method. |
SearchFormName |
The name of the form to run when GoSearch is fired. |
MasterAlias |
Form property to hold the name of an alias that should be used by the navigation methods. If this property is blank, the InitialSelectedAlias of the DE will be used. If both this and the InitialSelectedAlias are blank, then the current work area is used. |
Using the properties in Table 13, the developer can turn any of the behaviors on or off. The Allow* properties are used to do this. An Allow* property that is set to .F. will disallow that behavior in the form. These properties will be used in the code of the Go* methods and can also be used by the developer to enable or disable any button or toolbar that might provide access to the behaviors. The IsAtEof and IsAtBof properties can also be used to enable and disable buttons controlling the navigation.
Lets look at some code that implements these behaviors. Listing 7-7 shows the code in the GoNext method of our frmData class.
Listing 7-7. The GoNext method of our frmData class.
* Moves to the next record
* First check to see if this behavior is turned on
IF NOT THISFORM.AllowGoNext
* Short circuit
RETURN
ENDIF
* Declare local variables
LOCAL lcAlias, llCheckDE
* Save the current work area
lcAlias = ALIAS()
* Now select the work area of concern
IF NOT EMPTY(THISFORM.MasterAlias)
* If we have a masterAlias set
IF USED(THISFORM.MasterAlias)
* If there is a work area of that name, select it
SELECT (THISFORM.MasterAlias)
ELSE
* If there is no work area set to check the DE
llCheckDE = .T.
ENDIF
ELSE
* If there is no MasterAlias set to check the DE
llCheckDE = .T.
ENDIF
IF llCheckDE
* If we need to check the DE
IF NOT EMPTY(THISFORM.DataEnvironment.InitialSelectedAlias)
* If there is an InitialSelectedAlias
IF USED(THISFORM.DataEnvironment.InitialSelectedAlias)
* If there is a work area of that name, select it
SELECT (THISFORM.DataEnvironment.InitialSelectedAlias)
ENDIF
ENDIF
ENDIF
* Now move the pointer
IF NOT THISFORM.IsAtEOF
SKIP
ENDIF
* Call the SetEof/Bof to set the IsAtEof/Bof property
THISFORM.SetEof()
THISFORM.SetBof()
* Now reselect the work area
IF NOT EMPTY( lcAlias )
SELECT ( lcAlias )
ELSE
SELECT 0
ENDIF
* Now refresh the display
THISFORM.Refresh()
* And return
RETURN
This seems like a lot of code, but if you take it apart you can see exactly what it's doing. First, it checks to see if the behavior is turned on; if it is not, it gets out without doing anything. Next, it checks the aliases to find out which one it should be moving in. If the form has a MasterAlias set, it uses that one. If the MasterAlias is not set, we check the InitialSelectedAlias of the data environment. If neither of those is set, it uses the currently selected work area.
Once it has the proper work area selected, it moves the record pointer forward one record. Then it calls the forms SetEof and SetBof methods (seen below), and finally it refreshes the form display. It then reselects the work area that was current when it was called.
The GoTop, GoPrevious, and GoBottom methods are all essentially the same except for the record movement. This raises the question of whether we could design this better and reduce the repeated code in these methods. By adding a method to our class for selecting the master alias, we could remove that portion of code from these methods. Listing 7-8 shows the SelectMasterAlias method we have added.
Listing 7-8. The SelectMasterAlias method of our frmData class.
* Selects the master alias for this form
* Declare local variables
LOCAL llCheckDE
* Select the work area of concern
IF NOT EMPTY(THISFORM.MasterAlias)
* If we have a masterAlias set
IF USED(THISFORM.MasterAlias)
* If there is a work area of that name, select it
SELECT (THISFORM.MasterAlias)
ELSE
* If there is no work area set to check the DE
llCheckDE = .T.
ENDIF
ELSE
* If there is no MasterAlias set to check the DE
llCheckDE = .T.
ENDIF
IF llCheckDE
* If we need to check the DE
IF NOT EMPTY(THISFORM.DataEnvironment.InitialSelectedAlias)
* If there is an InitialSelectedAlias
IF USED(THISFORM.DataEnvironment.InitialSelectedAlias)
* If there is a work area of that name, select it
SELECT (THISFORM.DataEnvironment.InitialSelectedAlias)
ENDIF
ENDIF
ENDIF
RETURN
By adding this method, we reduce the amount of code in the Go* methods. Listing 7-9 shows the revised GoNext method.
Listing 7-9. The GoNext method revised to use the SelectMasterAlias method of the frmData class.
* Moves to the next record
* First check to see if this behavior is turned on
IF NOT THISFORM.AllowGoNext
* Short circuit
RETURN
ENDIF
* Declare local variables
LOCAL lcAlias
* Save the current work area
lcAlias = ALIAS()
* Now select the work area of concern
THISFORM.SelectMasterAlias()
* Now move the pointer
IF NOT THISFORM.IsAtEof
SKIP
ENDIF
* Call the SetEof/Bof to set the IsAtEof/Bof property
THISFORM.SetEof()
THISFORM.SetBof()
* Now reselect the work area
IF NOT EMPTY( lcAlias )
SELECT ( lcAlias )
ELSE
SELECT 0
ENDIF
* Now refresh the display
THISFORM.Refresh()
* And return
RETURN
Notice how much simpler this method has become. Besides, with all of the selecting code moved into a separate method, if we find a better or faster way to select the work area of concern, we have only one place to implement it and it will affect all of the other methods.
The only Go* method that is quite different from the others is the GoSearch method. Listing 7-10 shows the code in this method.
Listing 7-10. The GoSearch method of our frmData class.
* Calls the search form
* First check to see if this behavior is turned on
IF NOT THISFORM.AllowGoSearch
* Short circuit
RETURN
ENDIF
* An assertion for the developer
ASSERT THISFORM.AllowGoSearch AND NOT EMPTY( THISFORM.SearchFormName ) ;
MESSAGE 'You have Allow searching turned on and there is no search ' + ;
'form set up in the SearchFormName property.'
* Now check to see if there is a search form set up
IF EMPTY( THISFORM.SearchFormName )
* Short circuit
RETURN
ENDIF
* The search form should have its data session set to default so it participates in this
* form's data session. It will be passed the alias to search in and it should leave
* the record pointers at the result of the search. The search form also must be modal.
* Declare local variables
LOCAL lcAlias
* Save the current work area
lcAlias = ALIAS()
* Now select the work area of concern
THISFORM.SelectMasterAlias()
* Now run the search form
DO FORM (THISFORM.SearchFormName) WITH ALIAS()
IF NOT EMPTY( lcAlias )
SELECT ( lcAlias )
ELSE
SELECT 0
ENDIF
* Call the SetEof/Bof to set the IsAtEof/Bof property
THISFORM.SetBof()
THISFORM.SetEof()
* Now refresh the display
THISFORM.Refresh()
* and return
RETURN
This method checks the AllowGoSearch property first to see if the behavior is turned on. It then also checks to be sure that there is a SearchFormName set up. Notice the ASSERT line, which warns the developer that he/she has set inconsistent properties at development time. The developer should fix this problem before the system is released to users.
Once we know that the behavior is turned on and that we have a search form name to use, we run the search form. The search form is designed with its data session set to default, so it participates in the data session of the form that runs it. This way, the search form can locate the desired record and then simply return to this form. Because the search form is participating in this forms data session, any record movement will be reflected in the calling form.
It is also necessary for the search form to be modal so that the code in the GoSearch method will wait for the search form to complete its work before continuing.
The next couple of methods we need to examine are the SetEof and SetBof methods. These two are very similar, so we'll look at one of them and discuss the other one. Listing 7-11 shows the SetEof method of our frmData class.
Listing 7-11. The SetEof method of our frmData class.
* This method can be used to both set the IsAtEof property
* and to return the IsAtEof property setting
* Declare local variables
LOCAL lnRecNo, llEof, lcAlias, llCheckDE
* Save the current work area
lcAlias = ALIAS()
* Now select the work area of concern
THISFORM.SelectMasterAlias()
* Check to see if there is anything open
IF NOT EMPTY( ALIAS() )
* Check to see if we are at eof already
IF NOT EOF()
* If not save the record number we are on
lnRecno = RECNO()
SKIP
llEof = EOF()
GOTO lnRecno
ELSE
llEof = .T.
ENDIF
ELSE
* Nothing open so set eof to .T.
llEof = .T.
ENDIF
* Set the form property
THISFORM.IsAtEof = llEof
* Now restore the work area
IF NOT EMPTY( lcAlias )
SELECT ( lcAlias )
ELSE
SELECT 0
ENDIF
* Return the property setting
RETURN llEof
This method begins much the same as the Go* methods, by saving the current work areas alias and then selecting the master alias for the form. It then checks to see that something is open in the selected work area. If something is open, the method saves the record number, moves one record forward, checks to see if it is at EOF, and then puts the record pointer back. It then sets the forms IsAtEof property and returns the IsAtEof value. This method can be used to set the property and check the status. Its return value tells us if we are at EOF. Although this dual personality overloads the method, it does provide for checking the return value when we're moving around in the alias and checking only the property when we dont need to execute the code. When might we just check the property without calling the method? Perhaps in a forms refresh method that is setting the enabled and disabled properties of some navigation buttons. Obviously the code in the Go* methods calls this method to set the properties, so calling the methods again in the refresh for the form would run the code twice and slow the performance accordingly. The properties are there for this purpose.
The SetBof method is much the same, except it moves backward one record rather than forward, and it sets and returns the BOF value.
Whats the point of all this? The project named Chapter7 includes a form named DataForm. If you open this form for editing and double-click it to get to the code windows, you'll find that the form has no code. Instead, it uses a container class that has buttons for the navigation behaviors. You can see those classes in the Controls class library, which is part of the project. (We'll discuss control classes in a later chapter.) Figure 1 shows a screen shot of DataForm.
Figure 1. The DataForm from the Chapter7 project.
Run the DataForm form and you'll see that the navigation buttons work flawlessly. You can edit the form and change the Allow* properties and then run it to see the behavior changes. By using the frmData class for this form, we have allowed the developer to modify the behavior of the form by simply changing properties. The developer does not have to write any record-navigation code unless he or she wants something other than the default behavior our class provides.
Keep in mind that creating classes like these does not restrict developers. They always have the option of overriding our class code with their own code to get alternate behaviors.
The last of the behaviors that we listed at the beginning of this chapter is detecting that data has changed. This detection can be useful in preventing unnecessary updates to the tables and views. If the user has not changed anything, we should not go through the process of updating the cursors.
Visual FoxPro gives us two functions that can be useful in detecting changes to data. These are the GetFldState() and the GetNextModified() functions. GetFldState() will tell us the condition of the fields and the deleted status flag for a record, while GetNextModified() will find the modified records in a table-buffered cursor. It's important to note that GetNextModified moves the record pointer in the affected alias and therefore will cause the evaluation of any field or table validation rules, as well as any uniqueness checks on primary and candidate indexes.
We'll implement the detection of changes in our frmData class in conjunction with methods for saving or reverting an edit. We will add three methods: DoSave to save our changes, DoRevert to revert an edit session, and DetectChanges to detect any changes.
Our DoSave method will call each cursors CursorUpdate method, while DoRevert will call the cursors CursorRevert method. Both of these methods will first call DetectChanges to find out if any updating or reverting is necessary.
Listing 7-12 shows the code in the DetectChanges method.
Listing 7-12. The DetectChanges method of our frmData class.
* Checks all cursors for any changes and returns .T. if there are any
* updates pending and .F. if all buffers are clean
* Declare local variables
LOCAL laMembers(1), lnRecno, lcAlias, llRet, cName
* Save current alias
lcAlias = ALIAS()
* Get an array of all the member objects in the DE
AMEMBERS( laMembers, THISFORM.DataEnvironment, 2 )
* Work through the array
FOR EACH cName IN laMembers
IF THISFORM.DataEnvironment.&cName..BaseClass <> 'Cursor'
* If this item is not a cursor loop
ENDIF
* Select the work area for this cursor
SELECT ( THISFORM.DataEnvironment.&cName..Alias )
* Save the current record number
lnRecno = RECNO()
IF GetNextModified(0) <> 0
* If there is a modified record
* Return to the original record
GOTO lnRecno
* Set the return variable to indicate changes
llRet = .T.
* Get out of here
EXIT
ENDIF
ENDFOR
* Restore the work area
IF NOT EMPTY( lcAlias )
SELECT ( lcAlias )
ELSE
SELECT 0
ENDIF
* Return the status
RETURN llRet
The above code starts by getting an array of the member objects in the forms DataEnvironment. It then works through that array and checks each cursor for any changes. If it finds any change to any cursor it returns .T.otherwise it returns .F. This method can be called from our DoSave and DoRevert methods early on, and its return value can be used to determine whether or not we continue with the process.
Macro Note
A macro expansion using the ampersand (&) uses a period (.) as the symbol to mark the end of the variable name that is to be expanded. When you want the expanded variable to be followed by a period, you must use two periods, as in this line:
THISFORM.DataEnvironment.&cName..BaseClass
Managing multi-table updatingtransactions
Before we venture into our DoSave method, we need to consider another issue: How do we handle the problem of multi-table updates? On initial examination it may not seem like much of a problem; we could just update each table in sequence. But if we examine a real-world situation, we'll see the problem with this simple approach.
Consider a form for entering an invoice. The user enters the customer identification, the invoice information, the invoice details, and so on. When the user saves his or her work, we need to update the invoice table, the invoice details table, the inventory table (to reduce the amount on hand), and the accounts receivable table. What happens if the save routine updates the invoice and invoice detail tables and then fails on the inventory table? This would leave our database in an unbalanced state: Inventory would indicate items available that had been sold, and accounts receivable would not reflect an invoice properly.
In situations like this, it's much better to tell the user that we were unable to save the invoice and leave the tables in a known statethat is, not including the new invoice. The user can re-enter the invoice at a later time. This is much better than having the invoice half-entered into the system.
Visual FoxPro gives us the ability to handle this with transactions. A transaction groups a number of updates so they are treated as an all-or-none operation. We used three commands to create transactions, which are described in Table 14.
Table 14. Commands used for processing transactions.
Command |
Description |
BEGIN TRANSACTION |
Marks the beginning of a transaction. All cursor operations between this and the ending command will be cached and not committed until the ending command is encountered. |
END TRANSACTION |
Marks the end of a successful transaction. Commits the cursor operations. |
ROLLBACK |
Marks the end of an unsuccessful transaction. All cached cursor operations are discarded. |
Transactions can be nested inside one another to a level of five deep. This allows us to use transactions in our code while other transactions are being used in the trigger or validation operations of the database. The function TxnLevel() can be used to determine at what level of nesting a transaction resides.
Transactions in Visual FoxPro work by caching each operation in order. As each operation in the transaction completes, the necessary locks are obtained and held for the duration of the transaction. This ensures that at the END TRANSACTION there will not be any locking problems in committing the transaction. Nothing is written to the cursors during the transaction. Upon encountering the END TRANSACTION command, Visual FoxPro writes all updates to the cursors and releases the locks it obtained. A ROLLBACK discards all updates and also releases the locks.
It is evident that if a transaction were holding locks, it would be wise to locate the BEGIN TRANSACTION and END TRANSACTION or ROLLBACK commands as physically close to each other as is possible in our code. This will cause the locks to be held for as short a time as possible. Any calculations or processing required to complete the updates should be done before the transaction is begun.
We are now ready to examine the DoSave method of our form class. In our frmData class, we will keep the entire transaction in the DoSave method. Listing 7-13 shows the code for the DoSave method.
Listing 7-13. The DoSave method of our frmData class.
* Saves all changes to the cursors
* First find out if we have any changes to handle
IF NOT THISFORM.DetectChanges()
* There are no changes so get out
RETURN .T.
ENDIF
* Declare local variables
LOCAL llRet, cName, laMembers(1)
llRet = .T.
* Get a list of the DE members
AMEMBERS( laMembers, THISFORM.DataEnvironment, 2 )
* Now work through the members and call the ValidateAlias method of this form
FOR EACH cName IN laMembers
IF THISFORM.DataEnvironment.&cName..BaseClass <> 'Cursor'
* If it is not a cursor
ENDIF
* Call the ValidateAlias and pass this alias name to it
IF NOT THISFORM.ValidateAlias( THISFORM.DataEnvironment.&cName..Alias )
* If validate failed
llRet = .F.
EXIT
ENDIF
ENDFOR
* If validate alias failed get out
IF NOT llRet
RETURN llRet
ENDIF
* Now begin the transaction
BEGIN TRANSACTION
* Work through the cursors and update them
FOR EACH cName IN laMembers
IF THISFORM.DataEnvironment.&cName..BaseClass <> 'Cursor'
* If it is not a cursor loop
ENDIF
IF PEMSTATUS(THISFORM.DataEnvironment.&cName , 'CURSORUPDATE', 5 )
* If the cursor has a CursorUpdate method call it
llRet = THISFORM.DataEnvironment.&cName..CursorUpdate()
ELSE
* Otherwise do a table update
IF CURSORGETPROP('BUFFERING',THISFORM.DataEnvironment.&cName..Alias) > 1
llRet = TableUpdate( 0,.F., THISFORM.DataEnvironment.&cName..Alias )
* If you choose to allow multiple records to be edited the above
* command line would be
*llRet = TableUpdate( 1,.F., THISFORM.DataEnvironment.&cName..Alias )
ELSE
llRet = .T.
ENDIF
ENDIF
IF NOT llRet
* If the update failed get out
EXIT
ENDIF
ENDFOR
IF llRet
* If all updates were successful commit
END TRANSACTION
ELSE
* If any update failed rollback and revert the buffers
ROLLBACK
THISFORM.DoRevert()
ENDIF
RETURN llRet
The DoSave code starts by checking the DetectChanges method to see if there is anything to save. It then creates an array of all member objects of the forms DataEnvironment. The first FOR EACH loop works through the array and for each cursor it calls a method of the form, named ValidateAlias. The ValidateAlias method in our form class is empty except for an LPARAMETERS statement. This method allows developers to write any code they want to use to validate each alias for the form. The developer can return .F. from the ValidateAlias to stop the saving operation. If the developer doesn't write any code in ValidateAlias, then the loop will progress to completion and continue with the saving.
Before the second FOR EACH loop, a BEGIN TRANSACTION is issued. The second FOR EACH loop works through the cursors again. It checks each cursor to see if it has a CursorUpdate method; if it has one, the code calls that method and checks the return value. If a cursor does not have a CursorUpdate method, a TableUpdate is issued and the return value is checked. The variable llRet stores the return from these calls. If any cursor fails to update, the loop is exited.
The IF statement after the loop is used to decide whether to issue an END TRANSACTION and commit the changes, or issue a ROLLBACK and discard the changes. If a ROLLBACK is issued, the forms DoRevert method is called to revert the buffers to their original state. This is necessary because the ROLLBACK will discard the cached updates, but it does not change the buffered edits. You may ask why we throw away the users work when the problem may be a simple violation of a field rule; the answer is that we are calling the ValidateAlias for the developer to check the rules and validate the record before we do any saving. These fixable errors on the part of the user should be handled thereif the developer decided not to handle them, that is his or her decision. Using only a ROLLBACK would leave the buffers 'dirty' and could cause problems when trying to close the form. Also, issuing the DoRevert will ensure that the user sees the data as it appears on disk after the failed update.
Listing 7-14 shows the DoRevert method code for our form class.
Listing 7-14. The DoRevert method of our frmData class.
* Reverts all changes to the cursors
* First find out if we have any changes to handle
IF NOT THISFORM.DetectChanges()
* There are no changes so get out
RETURN .T.
ENDIF
* Declare local variables
LOCAL cName, laMembers(1)
* Get a list of the DE members
AMEMBERS( laMembers, THISFORM.DataEnvironment, 2 )
* Work through the cursors and update them
FOR EACH cName IN laMembers
IF THISFORM.DataEnvironment.&cName..BaseClass <> 'Cursor'
* If it is not a cursor loop
ENDIF
IF PEMSTATUS(THISFORM.DataEnvironment.&cName , 'CURSORREVERT', 5 )
* If the cursor has a CursorRevert method call it
THISFORM.DataEnvironment.&cName..CursorRevert()
ELSE
* Otherwise do a table revert
IF CURSORGETPROP('BUFFERING',THISFORM.DataEnvironment.&cName..Alias) > 1
TableRevert( .T., THISFORM.DataEnvironment.&cName..Alias )
ENDIF
ENDIF
ENDFOR
THISFORM.Refresh()
RETURN
The DoRevert method is very similar to the DoSave method. There is no call to ValidateAlias because it isn't needed. The reverts are not wrapped in a transaction because it isn't necessary when reverting.
As mentioned in Chapter 5, Data - Views, working with views in most cases is no different from working with tables. One area in which some differences are encountered is in data management. With views (whether local views populated with data from Visual FoxPro tables, or remote views populated with data from SQL/Server or some other database server), there is no natural concept of 'previous', 'next', 'first', and 'last'. Likewise, there is no concept of EOF() or BOF(), therefore no need to keep track of ones position within a table.
This is not to say that we can't implement a mechanism for accessing records in a sequential manner, but it will be done via a much different technique than SKIP, SKIP 1, GOTO TOP and GOTO BOTTOM.
With that caveat, all other concepts presented in this chapter apply equally well when working with views or directly with Visual FoxPro tables. The important thing to remember is that some applications may need to implement a mechanism to turn sequential navigation features on and off automatically, depending on the type of data a particular cursor is working with. This is easily determined in the runtime environment by using CURSORGETPROP('SourceType'). This function returns 1 for a local SQL view, 2 for a remote SQL view, and 3 when the data source is a table.
Remember also that views, whether local or remote, can have more than one record and they may need GoNext and GoPrevious. Also, the design of our frmData class allows subclassing for a frmDataView, which would have different code in the Go* methods.
At the start of this chapter, we listed the behaviors we were concerned with and described them. We then investigated the base data-management classes in Visual FoxPro and found them to be insufficient for implementing our solution.
In reviewing the possible approaches to the problems, we decided that a form class was a good candidate for handling our data-management operations, so we built the frmData class. We coded the methods in this form class to provide the behaviors we wanted. In doing this we were careful to provide properties for the developer to control the details of the behaviors, thus eliminating the requirement to subclass this form class to alter behavior in minor ways.
Our form class uses subclasses of the Cursor and Relation base classes to re-create the DataEnvironment at runtime. This was done this way to allow the developer to use the visual data environment designer in Visual FoxPros form designer to build the data for the form and yet provide all of the additional functionality that our classes provide at runtime.
In Chapter 8 we will see another approach to handling data access that will evolve this approach to a higher level.
Politica de confidentialitate | Termeni si conditii de utilizare |
Vizualizari: 1011
Importanta:
Termeni si conditii de utilizare | Contact
© SCRIGROUP 2024 . All rights reserved