Introduction
Buffering is a feature in Visual FoxPro that allows us to give the user "undo" and "save" capabilities.
In the old FoxPro 2.x days, programmers either didn't provide this capability, or edited memory variables, and copied information between these variables and the table fields with the SCATTER and GATHER commands. Nowadays, the recommended approach for this purpose is to use Visual FoxPro's built-in capabilities, namely, buffering. Another feature that is seldom required nowadays is record locking - most of the time, you will be using Visual FoxPro's implicit record locking.
If you use a commercial framework, everything related with buffering is most likely already done for you in the standard editing forms, but a general understanding of what is going on is appropriate, for debugging, and for certain tasks where you have to manipulate the tables yourself, without the aid of the framework.
Overview
The basic steps to use buffering, which I will explain in detail later, are:
I will also explain the following related points:
The built-in buffers
When buffering is enabled, Visual FoxPro creates two levels of buffers for each modified record. That makes a total of three different versions of each record:
Versions (1) and (2) of the record might be different if another user made changes to the record in the meantime, depending on the buffering mode.
Choose a buffering mode
You can choose between record and table buffering:
Pessimistic buffering may, at first, look as the better choice, but many programmers prefer optimistic buffering, because it avoids unnecessary network traffic. It also avoids the record being locked for a long time (which may interfere with other users accessing the records). And finally, optimistic buffering is the only option available for a remoteserver (client-server).
Optimistic buffering isn't really unsafe, as it may seem at first sight: if another user applied changes in the meantime, when saving a record with the default options, Visual FoxPro will detect this, and the save will fail. The user may have to apply changes again, but only to the single record that caused the conflict. Besides, this situation shouldn't arise frequently in practice.
Open a table or view
There is nothing mysterious about this. Let's suppose you want to work with a client table; just open the table as usual, with the command:
USE Client
Enable buffering
Before you can enable buffering for any table, you have to give the command
SET MULTILOCKS ON
The technical reason isn't quite clear for me: Visual FoxPro requires this even for record buffering. So let's just accept this as a Visual FoxPro requirement.
Now, let's suppose you decide to use optimistic buffering, which I recommend in most cases. To enable buffering, issue one of the following commands:
CursorSetProp("Buffering", 3, "Client") && Record buffering CursorSetProp("Buffering", 5, "Client") && Table buffering
The alias can be omitted if it is currently selected.
For pessimistic buffering, the second parameter is 2 (record buffering) or 4 (table buffering). To disable buffering, the parameter is 1.
Do some changes
If buffering is enabled, whenever you issue the REPLACE command, or some other command that changes records in the buffered table, the changes are done only in the buffers - the table on disk remains unchanged, until you decide to save the changes.
Also, now you can set the .ControlSource property of a TextBox - or of any other control that has this property - directly to a field in the table, for instance, .ControlSource = "Client.FirstName". Remember, because of buffering, changes will not be written directly to the table.
Undo changes
To undo changes, just give the command:
TableRevert()
This will undo changes to the current record, in the current table. See the help file for additional options. Save changes
To save changes, you might give the command:
TableUpdate()
Apart from the Visual FoxPro buffers, there can also be buffers at the operating system level, which don't allow the changes to be saved immediately. This can cause trouble (data and index corruption) in the case of a power shortage. Often, the operating system can be forced to commit the changes immediately (and reduce the risk of data corruption), with the command:
FLUSH
Check whether the save was successful
To see whether the record could be saved or not, check the return value of TableUpdate(), for instance:
llCouldSave = TableUpdate() if llCouldSave ...
if TableUpdate() ...
if not TableUpdate() local laErrorInfo(1) aError(laErrorInfo) MessageBox("Error saving record. Error #" + trans(laErrorInfo(1)) + chr(13) + chr(10); + laErrorInfo(2), 16, "Can't save")
For an update conflict (another user made changes to the same record), especially error #1585, there isn't much you can do but cancel with TableRevert(). Overwriting changes done by other users is possible, but risky.
For most other errors, the user can correct the input and try to save again. Deleting records
For deleting records, buffering has to be considered too. Specifically, the command:
DELETE
New records
When editing existing records, function recno() will return the same value whether buffering is used or not: the position of the record.
On the other hand, if you create a new record, while it isn't saved to disk, Visual FoxPro has to assign a temporary record number, since it doesn't yet know where the record will be saved in the end, or whether it will be saved at all.
What Visual FoxPro does, in this case, is assign negative record numbers for the temporary records.
For instance, if you have table buffering, and three new (not yet saved) records, they will be assigned record numbers -1, -2, and -3.
Finding modified records
When there are several records with pending changes, function GetNextModified() gets the record number of the next record with pending changes.
Checking for changes
Sometimes you need to know whether the current record has pending changes, or what fields were modified. This is done with GetFldState(). Let's suppose that table "Client" has 4 fields. The following example:
? GetFldState(-1)
might return "11121", meaning that the third field is modified (the additional character, at the beginning, is for the deletion status, which might also change).
And the following example:
? GetFldState("ClientCode")
will return 1 (this time, of type numeric) if field ClientCode is unchanged, 2 if it is changed.
For new records, the values returned become 3 and 4, instead of 1 and 2.
It should be noted that, if a user changes a field, and then changes it back to its previous value again, this function will still treat the field as modified. Implicit saving
When buffering is used, TableUpdate() sometimes gets called implicitly. This happens especially when row buffering is used, and another record is selected. If you aren't aware of this, you can get "interesting" results.
To avoid trouble, if you have record buffering, you should make sure that the current record is either saved or discarded explicitly, before the user is allowed to select another record.
Also, before closing the table, you should make sure that pending changes are either saved or discarded explicitly.
Current and old values
If you use buffering, a command such as:
? FirstName
will show you the current value of the specified field, as modified by the current user or data session.
Two additional commands let you access the other values of a field.
? OldVal("FirstName")
will show you the value of the field, before it was modified. This saves you the trouble of saving the original value explicitly.
And the function:
? CurVal("FirstName")
will fetch the value from disk again - in case another user or session changed it.
Transactions
A topic related to buffers is the use of transactions. Briefly, transactions allow you to save changes to several tables, on an "all-or-nothing" basis. Transactions can be used in combination with buffering, but this isn't a requirement.
BEGIN TRANSACTION starts the transaction. After that, you make changes (or save them, with TableUpdate()) to several tables.
END TRANSACTION will commit (confirm) all the changes done since the last BEGIN TRANSACTION, while ROLLBACK will undo all changes done since the last BEGIN TRANSACTION.
All records involved in a transaction are automatically locked until the transaction is ended; therefore, it is recommended to keep the transaction short in time.
Transactions can only be used on tables in a database container - not on free tables.
A simple example for two tables:
BEGIN TRANSACTION IF TableUpdate(.T., .F., "Table1") and TableUpdate(.T., .F., "Table2") END TRANSACTION && Commit changes ELSE ROLLBACK && Undo changes ENDIF
Version differences
I have tried to keep the syntax simple in the above examples. Therefore, for earlier versions, you should apply the following changes:
For Visual FoxPro 3, you have to put "=" in front of some functions.
trans(...) should be replaced with alltrim(str(...)), if you use Visual FoxPro 3 or 5.
Summary
Buffering allows you to have pending changes to one record, or to several records at a time. It also allows you to postpone the execution of triggers, until the record fulfils all rules. A thorough understanding of buffering is an important step in mastering Visual FoxPro.