Help  |   Contact Us  |   About Us  |   News and Events
Books, conferences, and other information about... Moving to Linux Switching to OOo Building Linux Apps Using Visual FoxPro
Buy      Download      Your Account      Catalog      Support      Conferences      Resources      Fun

1001 Things You Wanted to Know About Visual FoxPro

If you have a question about the technical contents of this book, please contact the author(s). Their email address(es) can usually be found in their bios in the About the Author section at the front of the book or on the author's bio page (click on the author's name on the book's main page.

Mark Austen, our post-publishing reviewer, pointed out to us that we have made explicit use of the LockScreen setting in several fragments of code but that we have not actually discussed, anywhere in the book, the use of LockScreen or pointed out the potential pitfalls associated with it. There is actually a good reason for this, we forgot!

The function of the LockScreen property (which, by the way, is available on _Screen and Toolbars as well as in Forms) is to prevent Visual FoxPro from re-painting the screen while it is set. Using LockScreen to control when the screen is re-painted has two major benefits. First it stops unsightly ‘partial’ refreshes of the screen from being visible to the end user and allows your application to transition between forms (or pages on a form) in a crisp and precise manner. Second, by disabling the need for Visual FoxPro to update the screen image while processing, it actually does speed up the application too.

The ‘xFrm’ form class in Chapter 3’s RootClas.VCX includes the standard methodology that we use for handling the form level refresh. This consists of a custom method named RefreshForm() which is actually a wrapper around the native Refresh() method. Here is the code:

*** This method wraps the call to the Refresh method inside
*** a LockScreen and includes calls to Before and After Hooks
.LockScreen = .T.
*** Call Before Hook
llOk = .BeforeRefresh()
IF llOk
*** Before was OK, so call Refresh
*** And then the After Hook
.LockScreen = .F.
In our working applications, we never call the form level refresh directly, instead we always call this custom method. As you can see not only does it provides us with the ability to implement “before” and “after” code (an example of using “Hook” methods) it also wraps the entire proceedings in a single pair of LockScreen commands. As a general rule the only other place that we ever use LockScreen is inside a single method where we are updating the screen explicitly without reference to RefreshForm. Such methods never call any other method that must invoke a change to the LockScreen setting. Why is this important? Consider the following (fictional) method code:

ThisForm.LockScreen = .T.
IF ! ThisForm.CheckValues()
ThisForm.LockScreen = .F.
The intention is clear enough – first we lock the screen and then depending on the result of the call to CheckValues() we either revert changes or leave changes alone before refreshing the screen and unlocking it. This is fine as it stands, however, what if the code in CheckValues() also has LockScreen settings…

ThisForm.LockScreen = .T.
FOR EACH loCtl IN ThisForm.Controls
*** Check values (or whatever)
ThisForm.LockScreen = .F.
Now when the first method runs, the LockScreen which it sets will be cancelled by the one in CheckValues() before the refresh is done – thereby defeating the object of using the LockScreen in the first place. The solution is simply never to use explicit LockScreen settings in methods which are being called from higher level methods, but instead to store the setting on entry, force whatever setting you actually want and then restore the original status on exit. In this way you will never inadvertently change the settings which have been established by a higher level method. If our CheckValues() method were re-coded as follows, there would have been no problem:

LOCAL llLock
llLock = ThisForm.LockScreen
ThisForm.LockScreen = .T.
FOR EACH loCtl IN ThisForm.Controls
*** Check values (or whatever)
ThisForm.LockScreen = llLock

Chapter 1 (Page 3) (2002/8/12)

The “R” switch referred to is no longer applicable. Certainly since the release of VFP 6.0 SP5 it no longer works and the documentation for Visual FoxPro Version 7.0 notes that “/regserver” should be used instead.

Chapter 1 (Page 8) (2002/8/12)

There is a misprint in the example path, the line: SET PATH TO G:\VFP60;C:\VFP60\TIPSBOOK\;DATA;FORMS;LIBS;PROGS;UTILS;
Should not have a final semi-colon and should read:

[1] Chapter 1 (Page 17)

In the code for "Creating an environment setting class" we merely state, without explaining why, that the Init() method of the class calls the custom SetOptions() method. For an explanation of why this is done that way, see Page 67: "Code in methods not events"

[2] Chapter 1 (Page 22)

In the "How do I create a Splash screen" section we refer to setting the TitleBar property to .F. This is an error, because the TitleBar property is numeric where the value 1 is "ON" and 0 is "OFF".

[3] Chapter 2 (Page 36)

The code in the DateInWords() function, although functionally correct could be streamlined. The result of a change in approach is also evidenced by the fact that although used as a single variable, lcSuffix is actually declared as an Array. The function which looks like this:

FUNCTION DateInWords( tdDate )
LOCAL lnDay, lnNdx, lcSuffix[31]

*** Initialize suffix for day
lnDay = DAY( tdDate )
lnNdx = lnDay % 10

IF NOT BETWEEN( lnNdx, 1, 3 )
   lcSuffix = 'th'
   IF INT( lnDay / 10 ) = 1
     lcSuffix = 'th'
     lcSuffix = SUBSTR( 'stndrd', ( 2 * lnNdx ) - 1, 2 )

Can be simplified if re-written like this:

FUNCTION DateInWords( tdDate )
LOCAL lnDay, lnNdx, lcSuffix

*** Initialize suffix for day
lnDay = DAY( tdDate )
lnNdx = lnDay % 10

IF ! BETWEEN( lnNdx, 1, 3 )
   lcSuffix = 'th'
   lcSuffix = SUBSTR( 'stndrd', ( 2 * lnNdx ) - 1, 2 )

Chapter 2 (Page 37) (2002/8/12)

The code for the CalcAge() function is incorrectly printed, although the code in the sample files is correct. The correct listing for the function is: FUNCTION CalcAge( tdDob, tdBaseDate )
*** Default Base Date to Today if empty
IF TYPE( "tdBaseDate" ) # "D" OR EMPTY(tdBaseDate)
tdBaseDate = DATE()

LOCAL lnYrs, lnMth, lcRetVal, lnBaseYear, lnBaseMnth
lnYrs = YEAR( tdBaseDate ) - YEAR( tdDob )

*** Calculate this year's Birthday
ldCurBdy = DATE( YEAR( tdBaseDate ), MONTH( tdDob ), DAY( tdDob ) )

*** Calculate Age
IF ldCurBdy > tdBaseDate
lnYrs = lnYrs - 1
*** If the birthday is in the same month as the default base date
*** then set the months to 11. Otherwise, subtract the base date's
*** month from the date of birth's month.
IF MONTH( tdDob ) = MONTH( tdBaseDate )
lnMth = 11
lnMth = 12 - ( MONTH( tdDob ) - MONTH( tdBaseDate ) )
lnMth = MONTH( tdBaseDate ) - MONTH( tdDob )

*** Format Output String
lcRetVal = PADL( lnYrs, 4 ) + " Years, " ;
+ PADL( lnMth, 2, '0' ) + " Month" + IIF( lnMth = 1, "", "s" )

[4] Chapter 2 (Page 42)

In the "String Functions" section we make the following statement:

"But did you know that you can use this line of code:

cString1 – cString2 – cString3

to accomplish the same thing as this one?

RTRIM( cString1 ) + RTRIM( cString2 ) + RTRIM( cString3 ) "

However, this is not entirely accurate because the "-" operator ALLTRIMs the strings involved but it retains all the extra spaces and moves them, to the end of the concatenated result. It actually accomplishes the same as:

lnLen = LEN( cString1 ) + LEN( cString2 ) + LEN( cString3 )
PADR( RTRIM( cString1 ) + RTRIM( cString2 ) + RTRIM( cString3 ), lnLen )

Chapter 4 (Page 85) (2002/8/12)

There is a misprint in the code printed toward the bottom of the page. The line: TextBox::GetFocus()

Should, of course, read:

[5] Chapter 4 (Page 86)

In the section on "TextBoxes" there are a couple of minor omissions, one in the text, and one in code. The text:

"This single line of code in the text box's GotFocus() method accomplishes the same thing as the four lines listed above:

This.SetFocus() "

is essentially correct but omits to mention that the execution of the SetFocus() call will cause the control's Valid() to fire. This may, depending upon the content of the valid be undesirable, if not unsightly.

The code in the custom SetInputMask() method of the TxtBase class will fail if you bind the control to a property of a container because the current code checks only for an explicit 'ThisForm' reference in the line:

*** Don't attempt to check the properties of the underlying
*** field if we are bound to a form property
IF UPPER( lcAlias ) # 'THISFORM'

This code should be amended, to make it more generic to:

*** Don't attempt to check the properties of the underlying
*** field if we are bound to an object's property
IF LEFT( UPPER( lcAlias ), 4 ) # 'THIS'

[10] Chapter 4 (Page 86)

The SetInputMask() method of CH04::VCX::txtBase may cause unexpected errors in certain situations. While it is very handy for preventing overflow errors in bound controls where an InputMask or MaxLength has not been specified, it will cause an error if the ControlSource of the textbox is an expression that contains a “.” character such as ( IIF( SomeTable.SomeField, ThisExpression, ThatExpression ) ). It also causes anomalous behavior if the ControlSource is an integer and the textbox’s MaxLength property has been set. This is the new, improved code for the textbox’s SetInputMask() mathod that takes all of these situations into account and deals with them appropriately. It should replace all of the existing code in that method:

LOCAL lcAlias, lcField, lcType, laFields[1], ;
lnElement, lnRow, lcIntegerPart, lcDecimalPart
IF ! EMPTY( .ControlSource ) AND ( '.' $ .ControlSource )
IF EMPTY( .InputMask )
*** Check here to see if the control is actually bound to a field in an
*** alias. The controlSource could be an IIF expression and this would
*** cause some problems
IF USED( JUSTSTEM( .ControlSource ) )
*** Only set the inputmask for numeric and character fields
*** and check the data type of the underlying field so we
*** can set it appropriately
lcType = TYPE( This.ControlSource )
IF INLIST( lcType, 'C', 'N', 'I', 'Y' )
*** Parse the alias and the field name from the ControlSource
lcAlias = JUSTSTEM( .ControlSource )
lcField = JUSTEXT ( .ControlSource )
*** format the field if it is character
IF lcType = 'C'
.InputMask = REPLICATE( 'X', FSIZE( lcField, lcAlias ) )
AFIELDS(laFields, lcAlias)
lnElement = ASCAN(laFields, UPPER(lcField))
IF lnElement > 0
lnRow = ASUBSCRIPT(laFields, lnElement, 1)
IF laFields[lnRow,2] = 'N'
lcIntegerPart = REPLICATE('9', laFields[lnRow, 3] - laFields[lnRow, 4] - 1)
lcDecimalPart = REPLICATE('9', laFields[lnRow, 4])
.InputMask = lcIntegerPart + '.' + lcDecimalPart
IF laFields[lnRow,2] = 'I'
*** Check here to see if the control's MaxLength property is set
*** and use it if it is
IF .MaxLength > 0
.InputMask = REPLICATE( '9', .MaxLength )
*** Max integer value is 2147483647...make sure we don't overflow
.InputMask = '999,999,999'
IF laFields[lnRow,2] = 'Y'
*** Max currency value is 922337203685477.5807...make sure we don't overflow
.InputMask = '99,999,999,999,999.9999'
ENDIF && IF laFields[lnRow,2] = 'Y'
ENDIF && IF laFields[lnRow,2] = 'I'
ENDIF && IF laFields[lnRow,2] = 'N'
ENDIF && IF lnElement > 0
ENDIF && IF lcType = 'C'
ENDIF && IF INLIST( lcType, 'C', 'N', 'I', 'Y' )
ENDIF && IF USED( JUSTEM( .controlSource ) )
ENDIF && IF EMPTY( .InputMask )
ENDIF && IF ! EMPTY( .ControlSource )

[6] Chapter 4 (Page 97)

The code as written in the AddSeparators() method of the Numeric Textbox control has a problem when using a decimal point character other than a period. When you tab out of the control any characters which have been entered to the right of that decimal point disappear from view - although they ARE retained in the value. The solution to this problem is to replace the current code:

*** Find the length of the decimal portion of the number
IF AT( .cPoint, .cInputMask ) > 0
    lnDecLen = LEN( SUBSTR( .cInputMask, AT( .cPoint, .cInputMask ) + 1 ) )
   lnDecLen = 0

with the following:

*** Find the length of the decimal portion of the number
IF AT( ‘.’, .cInputMask ) > 0
lnDecLen = LEN( SUBSTR( .cInputMask, AT( ‘.’, .cInputMask ) + 1 ) )
lnDecLen = 0

[21] Chapter 4 

txtSearch exhibited anomalies when the backspace key was pressed after two characters were entered. When this happened, the backspace key deleted both of the characters instead of just the second one. Many thanks to John Hosier, our tech editor, for pointing this out when he began to use the class in a production application. An updated version of CH04.vcx contains the modified class

[22] Chapter 4 

txtNumeric did not behave well as an unbound control. Many thanks to Pamela Thalaker for pointing this out to us. We also discovered that attempting to assign a numeric value to the textbox resulted in a data type mismatch error. The updated class is included in the current version of CH04.vcx.

[23] Chapter 4 


Unfortunately, our last update to the numeric text box class introduced a bug that caused it to behave as if it were ReadOnly. This update fixes the problem introduced by the last update. Many thanks to Geoff Franklin for pointing out the problems with this update. Sorry about that, folks...

The numeric text box has also been updated to ensure that other controls behave correctly if they cause the numeric text box to be refreshed. The Refresh() method of the numeric text box class calls its custom FormatValue() method and FormatValue() issues the command KEYBOARD '{END}' to allow calculator style entry from right to left. Obviously, this can and does cause problems when it executes if the numeric text box does not have focus. The FormatValue() method has been amended in the updated class library as follows:

IF .lChangingFocus
.lChangingFocus = .F.
*** If this control does not have focus,
*** we do not want to keyboard an 'End"
*** because of the effect it may have if this control
*** if being refreshed from another control
IF TYPE( 'Thisform.ActiveControl' ) = 'O' AND Thisform.ActiveControl = This

[11] Chapter 5 (Page 151)

Many thanks to Marc Leeds who e-mailed us when he discovered some strange behavior in our sample form MultiSelect.scx. This form illustrates the use of the lstChkBox class that uses check boxes like the one displayed by Visual FoxPro when “View Toolbars” is selected from the menu. To reproduce this behavior, run MultiSelect.scx and follow these steps:
· Select three items in the listbox
· Click the “Display Selections” button.
· Click on a new item in the listbox after clicking the “OK” button in the message box.
As expected, the last item that was selected in the listbox is still highlighted. However, when you continue and click on a different list item, the last selected item in the listbox is de-selected.
Typically, when you give a listbox focus by clicking on it, all the selections are cleared before the item that you just clicked on is selected. If you look at the GotFocus() method of the lstChkBox class, you will notice some code in there that retains the checks when the listbox gets focus. Without this code, all the checkboxes become unchecked even though the aSelected array has the correct value for each item in the list. Even if you just issue a This.RefreshList() in the GotFocus without the NODEFAULT at the end, the checkboxes still become de-selected even though the picture property of the items has been set correctly!
To get around this problem, add this as the first line of code in the GotFocus of the lstChkBox class:

This.ListIndex = 0

Of course, when the user clicks on the listBox from another form object, he cannot select or de-select anything because clicking on it merely gives it focus. But it does not de-select the wrong item. In the example in the book, since there is only the list and two command buttons, adding the line:
This.Parent.lstCountries.SetFocus() as the last line of code in the OnClick() method of the "Display Selections" button takes care of the problem. Of course, if you have a bunch of other objects on the form, it still is not a perfect solution.

[7] Chapter 6 (Page 174)

The code in the "How do I display the last full page of a grid" section is perfectly correct but since the original release we have found a better way of doing it. This is code should replace the code that we used in the form’s Init() method and is more efficient and less verbose than the version in the book: LOCAL lnKey, lnMaxRows, lcKeyField
*** Display the last page of the grid when the form instantiates
   Thisform.LockScreen = .T.
   WITH Thisform.GrdCustomer
     *** Calculate the maximum number of rows per grid page
     lnMaxRows = INT( ( .Height - .HeaderHeight - ;
     IIF( INLIST( .ScrollBars, 1, 3 ), SYSMETRIC( 8 ), 0 ) ) / .RowHeight )
     GO BOTTOM IN ( .RecordSource )
     *** Go to the record that should be in the first row of the last page
     SKIP -( lnMaxRows - 1 ) IN ( .RecordSource )
     *** Make sure it is the first relative row
     DO WHILE .RelativeRow # 1
       .DoScroll( 1 )
   Thisform.LockScreen = .F.

[23] Chapter 6 

grdDataEntry did not check to see if the grid’s RecordSource was at EOF() when setting its nRec2Validate property, causing an error when it was. Many thanks to Andrew Coates for bringing this to our attention. Actually, the entire process of validating the current row in VFP 6 was somewhat clumsy and kludged. Thanks to the new RowColChange property in VFP 7, we were able to modify the class, removing several properties and great chunks of code. Now, all we have to do is determine if we are attempting to move off the current row in the grid’s BeforeRowColChange() method. If so, we call the grid’s ValidateCurrentRow() method and issue a NODEFAULT if the validation fails. This is much cleaner than jumping back and forth between records as we had to in previous versions. The upated class is included in the current version of CH06.vcx

[12] Chapter 7 (Page 200)

A reference is made to the structure listing program ‘GetStru2’. This program, as written, will give incorrect results for the backlink when a table is defined as allowing NULLS. This is because an additional 22 bytes in the header is used when NULLS are permitted and so the location of the start of the backlink information is shifted. The original version of the code failed to take this into account properly and the code in the “TableLst” function needs to be amended as follows:
*** It's a table after all!
lcCDX = IIF( lnNextByte%2 = 0, 'No Structural CDX', lcAlias + '.CDX')
lcMem = IIF( lnNextByte%4 < 2, 'No Memo File', lcAlias + '.FPT')
*** So now Read DBC Backlink
llInDBC = .F.
lcDBC = "Not Part of a DBC"
llInDBC = .T.
*** Check to see if table allows nulls
*** Need to read a different location

[8] Chapter 7 (Page 214)

The NewID() function as defined on this page will fail if a transaction is in porogress and an attempt is made to add a record to a table which uses this function when SYSTABLE.DBF is not already open. The offending code is: *** Save Settings and Open Systable if not already open
lnOldRepro = SET('REPROCESS')
IF ! USED('systable')
  USE systable IN 0
  *** Make sure that the table is not buffered
  =CURSORSETPROP( 'Buffering', 1, 'systable' )

This will fail when Systable is NOT already open and a transaction is in force because you cannot change the state of a table inside a transaction (see "Using Transactions" in Chapter 8 for more details). In practice this should be a rare occurrence but that is no reason not to handle it. The basic correction is simple, just check for the presence of a transaction before changing the buffering status.

*** Save Settings and Open Systable if not already open
lnOldRepro = SET('REPROCESS')
IF ! USED('systable')
   USE systable IN 0
   IF TXNLEVEL() = 0 AND CURSORGETPROP( 'Buffering' ) > 1
     *** Make sure that the table is not buffered
     =CURSORSETPROP( 'Buffering', 1, 'systable' )

Chapter 8 (2002/8/12)

One version of the code for the form root class (xFrm in rootclas.vcx) included a transaction in the DataRevert() method. This was entirely erroneous and should not have been there. The TableRevert() function cannot be used inside a transaction and, when reverting changes, there is no point in using a transaction anyway. The correct listing for the DataRevert() method is: LPARAMETERS tcTables
LOCAL llRetVal, lnCnt, lcToDo, lnBuffMode

WITH ThisForm
*** Initialise Return Value to .T.
llRetVal = .T.
*** Loop through all tables
lnCnt = 0
*** Retrieve table name
lnCnt = lnCnt + 1
lcToDo = .GetItem( tcTables, lnCnt )
*** NULL return indicates end of the string
*** Check buffer mode of each
lnBuffMode = .GetBufferMode( lcToDo )
*** Issue the correct revert command and "AND" the result
*** Remember that TableRevert returns the NUMBER of rows reverted
CASE INLIST( lnBuffMode, 2, 3 )
*** We are Row Buffered
llRetVal = llRetVal AND (TABLEREVERT( .F., lcToDo ) >= 0)
CASE INLIST( lnBuffMode, 4, 5 )
*** We are Table Buffered
llRetVal = llRetVal AND (TABLEREVERT( .T., lcToDo ) >= 0)
*** No Buffering at all - so do nothing
*** Return Status

Chapter 8 (Page 219)

Regarding the indexing of a buffered table, GETFIELDSTATE() should be replaced by GETFLDSTATE().

Also, the CURSORSETPROP() used later in the same procedure has a space between the begin quote and the B in Buffering that caused an error.

Chapter 8 (2003/7/3) Page 267

Thanks to Randy Roberts of SRI Homes for spotting this one and reporting it. His solution of specifying the source table name in the call to RECNO() is both effective and good practice and would have avoided the error completely had I done it his way in the first place.

The issue is that the existing code does not take account of the fact that VFP temporarily switches work areas when it reaches the insert statement and so the value retrieved by the RECNO() call is from the wrong cursor. Since we want the record number as a character string anyway, the solution I have actually adopted is to ensure that the record number from the source table is retrieved, converted and stored to a local variable before the INSERT statement is executed. The INSERT statement has been modified to use this variable. The correct code follows:

WITH ThisForm
*** Evaluate the record number of the source table
*** Before the INSERT statement causes a change
*** in the work area.
lcRec = .ExpToStr( RECNO())
*** VFP switches work areas without warning right here
! INSERT INTO curcflix ;
( cfxRecNum, ;
cfxFldNam, ;
cfxOldVal, ;
cfxCurVal, ;
cfxUsrVal, ;
cfxForcit ) ;
( lcRec, ;
lcFldName, ;
.ExpToStr(luOldVal), ;
.ExpToStr(luCurVal), ;
.ExpToStr(luUsrVal), ;
2 )
*** And switches back to the correct work area here!

[13] Chapter 9 (Page 279)

In this section on Views we recommended using “GenDBC” to extract View Definitions into a program file for ease of editing and maintenance. As we pointed out in the chapter, though, even a simple view generates a large number of lines of code – most of which are simply repeated for each field in the view. Barbara Peisch told us that she uses a loop for generating these field level settings and she even sent us an example of how she does it for one of her applications. Here is her example:
Actually, here's a real life example of some code from one of my apps. (Note: not all the DBSetProp commands are needed, but I like to use them anyway.)

***************** View setup for AcctView ***************
SELECT AcctMast.*, Division.Division ;
from Acctmast left outer join Division on AcctMast.DivId = Division.DivId
* If the view is parameterized, you must initilize the view parameter here.
use AcctView
dimension FldArry[1]
DBSetProp('AcctView', 'View', 'UpdateType', 1)
DBSetProp('AcctView', 'View', 'WhereType', 3)
DBSetProp('AcctView', 'View', 'FetchMemo', .T.)
DBSetProp('AcctView', 'View', 'SendUpdates', .T.)
DBSetProp('AcctView', 'View', 'UseMemoSize', 255)
DBSetProp('AcctView', 'View', 'FetchSize', 100)
DBSetProp('AcctView', 'View', 'MaxRecords', -1)
DBSetProp('AcctView', 'View', 'Tables', 'AcctMast')
DBSetProp('AcctView', 'View', 'Prepared', .F.)
DBSetProp('AcctView', 'View', 'CompareMemo', .T.)
DBSetProp('AcctView', 'View', 'FetchAsNeeded', .F.)
DBSetProp('AcctView', 'View', 'FetchSize', 100)
DBSetProp('AcctView', 'View', 'Comment', "")
DBSetProp('AcctView', 'View', 'BatchUpdateCount', 1)
DBSetProp('AcctView', 'View', 'ShareConnection', .F.)
for I = 1 to alen(FldArry,1)
if I = 1
dbsetprop('AcctView.'+FldArry[I,1], 'Field', 'KeyField', .T.)
dbsetprop('AcctView.'+FldArry[I,1], 'Field', 'KeyField', .F.)
if upper(FldArry[I,1]) = 'DIVISION'
dbsetprop('AcctView.'+FldArry[I,1], 'Field', 'Updatable', .F.)
dbsetprop('AcctView.'+FldArry[I,1], 'Field', 'Updatable', .T.)
dbsetprop('AcctView.'+FldArry[I,1], 'Field', 'UpdateName', 'Acctmast.'+FldArry[I,1])
dbsetprop('AcctView.'+FldArry[I,1], 'Field', 'DataType', ;
use in AcctMast
use in Division

While this is a little shorter than the program generated by GenDBC it is still all hard–coded. Each view would, therefore, still require manual intervention. One possible solution might be to create a table to hold all of these settings, and then write a little function to extract the values and plug them into a generic form of the code that Barbara is using – but then you have another table to be maintained as well as the view itself.

[14] Chapter 9 (Page 301)

In the first paragraph on this page we have stated that:
“Any SQL engine will interpret "WHERE 1=1" as TRUE and "WHERE 0=1" as FALSE”
However the example to which we are referring actually uses ‘1=1’ in an “ON” clause, rather than a “WHERE”. The presence of the keyword is, of course, irrelevant and we should have simply said that:
“Any SQL engine will interpret ‘1=1’ as TRUE and ‘0=1’ as FALSE”

[15] Chapter 10 (Page 326)

The comment at line 13 has a typing error and should read:

*** Now release the Client Table Object

[16] Chapter 10 (Page 327)

The last two rows of “Table 10.2 Cursor Properties that determine the location of source data” are printed incorrectly. The word ‘empty’ which appears in column one, should in fact be in column three as shown:
Table Type and Location Database CursorSource
Bound Table, DBC on current drive Relative Path and File Name of DBC The name of the table in the DBC
Bound Table, DBC on different drive Absolute Path and File Name of DBC The name of the table in the DBC
Free Table on current drive Relative Path and File Name of DBF Empty
Free Table on different drive Absolute Path and File Name of DBF Empty

[17] Chapter 10 (Page 331)

The code for the Data Path Manager presented here is inconsistent in its approach. We have included an ASSERT so that a message would be displayed if the object is called with a reference that does not evaluate to a DataEnvironment. This is perfectly reasonable, however, we have NOT included similar ASSERTS if the reference does not even evaluate to an object (the ELSE condition) or if the reference table cannot be found.
Clearly it would be good practice to include ASSERT statements in all three of these error conditions and it might even be prudent to add one if no cursors are found – even though this is not really an error. (A form which is using a DE and which has had this object added to the BeforeOpenTables() method, but does NOT use any cursors would certainly be peculiar, if not actually wrong!)

[18] Chapter 10 (Page 341)

Thanks to Brad Jones for finding a couple of errors in the single instance handling section of the DoForm() method presented on this page.
First, when forcing a minimised form to restore itself, using a call to its Activate() method as we have done does not automatically give the form focus. If you want to restore the form, and give it focus in one step, then amend the code shown here so that the Show() method of the form is called instead of the Activate().
*** Force to top
.AlwaysOnTop = .T.
*** Activate the form
*** Cancel Force To Top
.AlwaysOnTop = .F.
May be amended to read:
*** Activate the form and give it focus

Second there is an error in the referencing for handling the Toolbars:
*** Sort out Toolbars, pass the name of the required toolbar (if any)
.SetToolBar(.aFmList[.nFmIndex, 4])

Should be amended to read:
This.SetToolBar( This.aFmList[This.nFmIndex, 4])
(Actually, this one looks like a classic case of “Find and Replace”. We probably added the WITH/ENDWITH construct after testing the code and simply replaced every occurrence of “THIS.” with “.” and omitted to re-test afterwards! This line of code does not refer to loFmRef, but to the Form Manager itself)!

[19] Chapter 10 (Page 342)

Brad Jones also pointed out that if you use the form manager to try and instantiate a modal form as an SCX file it fails. This is because the modal form does not return automatically after its Init() method, so the form manager cannot try to register the form until after it has actually been released (which will fail). The solution is very simple, add the NOSHOW clause to the DO FORM command by replacing the code:
*** Instantiate the form
IF tlIsClass
*** Create as a class
loFmRef = CREATEOBJECT( tcFmName, oParams )
*** Run as a Form using NAME and LINKED clauses
DO FORM (tcFmName) NAME loFmRef WITH oParams LINKED

With this form of the command
*** Run as a Form using NAME and LINKED clauses

Apologies for this error, the NOSHOW should have been there from the start, because the DOFORM method explicitly calls the form’s SHOW method later in the process anyway. No excuses, we just missed it out and obviously we never tested with a modal form – thanks to Brad for catching all these for us.

[9] Chapter 12 (Page 400)

The code for UpdFormProps() method of the Form Inspector has a bug which manifests itself when the target form has a property which is a reference to an object that is not actually a child object of the target form. In other words, an object reference property which is populated using either CREATEOBJECT() or NEWOBJECT(). No excuse for this one, we just missed it! The code shown on page 400 is:

*** Otherwise get the current Value
lcPVal = TRANSFORM( EVAL("ThisForm.oCallingForm."+laObj[lnCnt,1]) )
INSERT INTO curFormProp VALUES (laObj[lnCnt,1], lcPVal)

This needs to be wrapped in a test for any such remote object as follows:

*** Otherwise get the current Value
IF TYPE( "ThisForm.oCallingForm."+laObj[lnCnt,1] ) # "O"
   lcPVal = TRANSFORM( EVAL("ThisForm.oCallingForm."+laObj[lnCnt,1]) )
   INSERT INTO curFormProp VALUES (laObj[lnCnt,1], lcPVal)
   INSERT INTO curFormProp VALUES (laObj[lnCnt,1], 'Object' )

Chapter 14 (Page 466) (2002/8/12)

This version information can be extracted via the new native function AGETFILEVERSION (not AFILEVERSION as noted in the text). In VFP 6, AGETFILEVERSION function (not AFILEVERSION as noted in the text) returns a zero if the file specified in the second parameter is not found, or there is no version information in the executable. If the file is found and no version information is included then the array is only one element (with the value .F.). This behavior changed in VFP 7. No array elements are created if there is no version information in the executable. If the file is found and version information is found, the array is created with 15 elements.

Chapter 14 (Page 467) (2002/8/12)

The AGETFILEVERSION function (not AFILEVERSION as noted in the text) can be used to determine the version details in more than just VFP executables; it can also be used to get version specifics on other Windows executables.

Chapter 15 (Page 480) (2002/8/12)

Table 15.1 is a list of events (not methods) that are intrinsic to the VFP projecthook class.

Chapter 15 (Page 491) (2002/8/12)

Table 15.4 describes the custom methods on the development projecthook (CPhkBase::phkDevelopment). This table has two entries for the ProjectBuilderToolbarInit

Chapter 15 (Page 492) (2002/8/12)

The development projecthook (CPhkBase::phkDevelopment) has a custom property cSetExclusive that was not included in Table 15.5. This property is the setting of SET EXCLUSIVE that is set in the initialization of the projecthook. Inadvertantly, the cPathDirectories has duplicate entries in the table.

[20] Chapter 16 (Page 517)

In the "How to create flexible control breaks" section, the EOFBreakIt.prg is missing from the Source Code downloads. This problem has been corrected. Please download the updated source code for chapter 16 or copy the code from the E-book if you need the program.

Chapter 16 (Page 517) (2002/8/12)

The EOFBreakIt.prg was inadvertently left out of the chapter downloads. It is now available in an updated chapter download separate from the rest of the source code.

Chapter 17 (Page 532) (2002/8/12)

In the discussion of the printing of Page ‘X’ of ‘Y’ on a report we noted printing to the NUL printer device might be an alternative to just printing the report solely with the NOCONSOLE clause. It has been pointed out to us by several developers that this is the preferred way to accomplish this tip. Using the NOCONSOLE without the TO NUL occasionally reports an incorrect number of pages.

Chapter 17 (Page 539) (2002/8/12)

The form class frmPreview that we use as an example to demonstrate changing the title of the Print Preview window is in the Ch17.vcx class library, not in the CH16.vcx class library as misprinted.

Chapter 17 (Page 540) (2002/8/12)

The form class frmPreviewSDI that we use as an example to demonstrate displaying a report in Print Preview in a Top-level window is in the Ch17.vcx class library, not in the CH16.vcx class library as misprinted.

Chapter 17 (Page 544) (2002/8/12)

We stated: “Another method is to create a "View" menu pad named _msm_view. On this menu add a VFP bar. The bar number to be added is the _mvi_toolb. Having this menu option in your application will make the toolbars available”. We are sorry to report that creating the toolbar manually does not work, but the initial tip of creating a clean resource file to generate the default toolbars will do the trick.