VFP Rookie Mistakes(Updated: 2010.01.18 09:25:56 AM)
|
| Item | Rookie Mistake |
|---|---|
In some instances, especially at valuable junctures, CREATEOBJECT() andNEWOBJECT() are evil. |
The problem with CREATEOBJECT() and NEWOBJECT() is, to the extent they are sprinkled all over in code, they hard-code the birth and also, therefore, the subsequent configuration of class instances, all over the place.Compare these two lines, they have the same effect, yet The line with oFactory.Create() is incalculably more flexible and potent than CREATEOBJECT().x= CREATEOBJECT(“KeyCustomer”) && hard-coded direct y= oFactory.Create(“KeyCustomer”) && delegation, parameter is just a locator. Using the factory gives us the ability to do many things, including but not limited to:
One thing you have to watch out for is the data session your object is created in when using a factory. This is especially problematic if the object is going to need to read data from a VFP cursor. Of course you could design your factory to switch its current data session to a passed in session id prior to creation. Another possiblity is to use the factory to provide the concrete class name based on the token hence code like...
x = CREATEOBJECT( oFactory.GetClassName("KeyCustomer") )
This still allows for object creation in the current datasession while allowing the factory to determine the concrete class name. -- Bob Archer Yup, good point. In those circumstances you could always say:
oTempFactory = CREATEOBJECT("Factory")
x= oTempFactory.Create(“KeyCustomer”)
... which I think is better since the factory could/should not only encapsulate the creation of the object, but also its configuration, etc. So it's not always a matter of just delegating the hard-coding of the class name, but other services as well. -- Steven Black (Perhaps an additional possible rookie mistake would be creating objects in the wrong data session.) |
| Date data type |
I'd like to float this as a potential huge rookie mistake for applications that may someday upsize to a backend database. Why? Because the Date data type is a VFP idiom, whereas backends typically store Datetime data types. Therefore if all your code assumes Date data types, then upsizing is a much more horrendous task than it would be otherwise. Thoughts?-- Steven Black
I've upsized a few fairly large projects to SQL and mapping the data in the date type to a datetime type was never a problem. I think the larger problem is knowing how to deal with a datetime type in SQL when you only want to use the date portion. I don't see a reason to make a system more complex when it uses native tables just [BE]cause you might upsize it someday. In addition, SQL Server 2005 adds a date datatype. I have no knowledge of Oracle. -- Bob Archer Take note: The problem isn't in the type mapping, it's with empty values, which in SQL server are either NULL, or an idiomatic long-ago date, neither of which will work in your legacy Fox app without significant work.-- Steven Black Given your qualifier that the app may go to another backend in the future, then I would agree. For similar reasons, I have taken up the habit of coding logicals as numeric (1,0), which is common across the popular backends. The decision is really "pay now or pay later." The decision to use datetime against a VFP backend when you only need the date portion adds only a slight complication to date handling/date math. -- Guy Pardoe One idea is to always support the concept of NULL in your VFP applications. One of the problems is not necessarily the DATE data type itself but the fact that VFP supports a blank date value which is a foreign concept to most backend database servers. Rod Paddock
|
| Include Files |
An include file contains compiler directives and since VFP compiles every program, class, and form separately, it is necessary to specify the include file for every program. In the case of Forms and Classes, there is an option on the system menu (under Form for forms and Class for classes) to specify the Include file, using this menu option prevents the need to specify the include file in every method. Note: The file included through Form, Include File is included with absolute reference. I had a problem with this appraoch since we create a new project for each new year. When I copied forms to a new folder, all forms continued to refer to the old include file. On the other hand, I can use "Include\MyIncl.H" in form methods, and easily copy forms and modify the "Include\MyIncl.H" in the new folder. -- Ravi |
| SET EXCLUSIVE |
The default value for SET EXCLUSIVE is ON. Thus when you create an EXE, if you don't set this explicitly yourself, this is what you get. The default is only ON for the global datasession. The default is OFF for private datasessions. |
| Minimize the code you put in Refresh methods |
Don't put excessive code in Refresh methods. Especially, don't put SET FILTER TO or REQUERY() in a Refresh method. Refresh methods get called very often and excessive code will severely slow down the performance of your application. As George Goley said, "Speed up your applications by removing the slow parts", and lots of code in a Refresh method is a slow part. Don't call Refresh excessively. Realize that calling Refresh for a container calls the Refresh for each of its contained objects implicitly. In addition, having a DODEFAULT() in the refresh of a container will cause contained objects to refresh twice for every refresh of the container, even if followed by a NODEFAULT. So, it's best to stay away from behavior that must be inherited within a container's refresh method. You can get around this by having the refresh of a container's superclass call a separate method for any custom refresh behavior that you want to inherit and possible supplement in application classes. |
| SET DELETED |
Take the time to understand the effect of SET DELETED ON/OFF |
| Porting / Converting |
Don't port FoxBase or FoxPro code! It sucks. Don't do it. It's okay to say no. Get a job at a Microage store instead, if need be. The code that is transported from FoxBase or FoxPro is often convoluted and difficult to maintain and understand. It is virtually NEVER the best way to accomplish any tasks and is therefore a very poor tool for learning VFP. It is much better to study the sample code included with VFP and/or available here and at other sites. |
| Grids |
Avoid data entry in a grid. Place buttons near the grid that give the user the ability to add/edit/delete. If add/edit, fire up a form and do all the data entry stuff in the form. When finished, make the form go away and refresh the grid to show the changes. Grids have MANY weirdnesses and quirks and this method gets around a lot of them. Grids can be used for data entry, however, as a rookie you will need to deal with the user's requests for more and more customization over time and when grids begin to use controls other than the default textbox they can become complex beasts that are difficult to understand and maintain. It will be better for you in the long run to wait until you have more thorough understanding of grids and their quirks before you begin to use them for data entry. If you need to set the data source of a grid to a cursor that gets re-created via code (sql select, or whatever), save the Record Source property, set the Record Source property to space(0), execute the code to re-create the cursor, then set the Record Source back to the saved value. This prevents all the columns from disappearing when the cursor momentarily disappears. You can also save the individual controlsource properties of each column and then restore them. e.g. WITH THIS.grdDtlLines
LOCAL laControlSources[.ColumnCount],lnColumn
FOR lnColumn=1 TO .COLUMNCOUNT
laControlSources[lnColumn]=.COLUMNS(lnColumn).CONTROLSOURCE
.COLUMNS(lnColumn).CONTROLSOURCE=[]
ENDFOR
ENDWITH
.
< do the refresh data stuff >
.
WITH THIS.grdDtlLines
FOR lnColumn=1 TO .COLUMNCOUNT
.COLUMNS(lnColumn).CONTROLSOURCE=laControlSources[lnColumn]
ENDFOR
ENDWITH
Alternatively, use a Grid Safe Select; see also Category Grid Basics. |
| EventsShould Always CallMethods |
Don't place behavior code in the User Interface Layer. The issue here is to organize your code in the methods so it is most easily leveraged. If you find a line of code that says, Thisform.Someotherobject.Click(), you should probably create a new method for the form and move the code from Someotherobject.click() to the new method and then call that method from both the someotherobject.click() and the line that calls the click. |
| Get the Hackers Guide |
Get the Hackers Guide and put its CHM file on your hard drive, then place a shortcut to it in your Windows start button. Some developers even SET HELP TO HACKFOX.CHM or add it to their VFP menu at startup. The Hacker's Guide is available from www.hentzenwerke.com |
| FoxProAdvisor, Fox Talk, and CODE journals |
Subscribe to FoxPro Advisor, Fox Talk, and CODE magazines. After doing this scour the websites of Advisor Publications and Pinnacle Publishing and bookmark their indexes of past articles. While there, join our crusade to get these publishers (especially Advisor Publications) to put content online so you can get the documentary help you need within minutes of needing it. Also, I've found that the Fox Talk CDs have superior search capabilities than the Advisor CDs. I buy the previous year and add to my harddrive. |
| There's almost always an easier solution |
You just have to ask someone who's been there. There are a lot of resources available for little or no cost. (see Where To Find Downloadable VFPSamples) After you have reviewed what is already available, if you still feel you need to do things differently don't be afraid to experiment. |
| Consider using Views |
Study and experiment with the advantages and disadvantages of using views, particularly using views for data entry into grids, parameterized views instead of SET FILTER and parameterized views for data entry into child tables. Don't start your app working directly with tables only later to discover the advantages of views. |
| Don't reinvent the wheel |
The best VFP developers download or purchase far more code than they write themselves. Know Where To Find Downloadable VFPSamples and scour Category Frameworks. |
|
Don't try creating a Frame Work as your first endeavor |
A framework is a complex system and is not something to use to learn the language. You will probably throw out your first VFP application. See the "don't reinvent the wheel" item above. |
| Frame Works |
Study a framework. It will show you how things can be done, as well as how to avoid the pitfalls of VFP. Visual Maxframe has a shareware version on their web site that is a worthwhile study. Also, the original Codebook is freely available and worth a look. You may decide that using a framework is worthwhile. It usually is, except for the simplest of projects. Don't expect to be successful with any framework until you've first learned a considerable amount about OOP, data buffering, the database container and the VFP designers. With or without a framework you need to have a fair amount of VFP knowledge before you can be successful. Hold the chainsaw by the correct end. Frameworks took years to design, build and refine, and will take time to master. Use the framework as it was intended. On your early outings, do things the way the framework expects you to, or you'll be treading uncharted territory where side-effects can make the framework your adversary. As you learn the intricacies of the tool, you can start to cautiously experiment with going outside the lines. See Category Frameworks. |
| Pay attention to good data design |
When building tables, avoid Repeating Fields. When you see fields named Contact1, contact2, ... it is a really good sign that you are creating Repeating Fields. |
| Persistent Relationships in the DBC |
Defining a persistent relationship between two tables in a dbc is not the same as SET RELATION TO |
| Avoid using spaces in table names | You'll have to deal with it in numerous places in your application and VFP will not use the table name as the alias which is the default behavior under normal circumstances. |
| Don't hard-code drive letters or paths |
Don't hard-code drive letters (or network) paths in application code or SET PATH command. This locks the application into only being usable in one directory structure (or network structure). |
| Avoid using Key/Reserved words |
Don't use key/reserved words for memory variables. eg - If you have a PACK of a dozen don't write PACK = 'DZ' You will execute the PACK command. I agree one shouldn't use keywords for memory variables. However examples should make sense, no? I wonder in which version it is true that "If you have a PACK of a dozen don't write PACK = 'DZ' You will execute the PACK command.". I disagree with it. In all versions I know you will end with a memory variable named 'pack'. - Cetin Basoz Also avoid using key/reserved words as field names in a table. You don't want to see a SQL SELECT like: SELECT * FROM cTable GROUP BY group ORDER BY order INTO CURSOR Cursor |
| Screen Resolutions |
Determine the resolution of the screens that will run the application. Set the form designer to use this resolution (tools/options/forms/maximum design area). How to Find the Current Video Resolution |
| Screen Resolutions |
Good idea is to also set YOUR screen to the >>minimum<< possible resolution your clients will be using (which is, these days, rapidly changing from 640x480 to 800x600 but if you're wanting to play it safe....) that way, you're sure it looks good (albeit small on the higher resolutions) on every screen resolution.
Actually, there are still a lot of people who run 640 x 480 even on 17" monitors. They simply can't read text on the screen at higher resolutions--and this includes many people who don't have "official" visual impairments, but whose eyes are just going through middle-age-related changes. ALWAYS ensure that your application can run in 640 x 480, and if you have the ability, eschew the use of operating systems that don't support 640 x 480. -- Ken Dibble Remember that "640x480" is the whole screen, you might only have 640x400 available for a form. The Windows status bar, the VFP title bar and a menu all make the screen shorter. -- Geoff Franklin We had the reverse problem, forms designed to work at 640x480 but too small for users with new 14" 1440x1080 laptops.
It would be even better to design your applications with accessibility in mind (font choices, colors, etc.). Couple that with some resize code (or use anchors in VFP9) and you're on your way. -- Tod Mckenna |
| Tables with no records |
Test your application for ability to withstand tables with no records, record pointers at end-of-file, large numbers of records. |
| Conditional Branching |
Whenever you use an IF..ENDIF statement, consider the need for ELSE handling. For DO CASE constructs, consider the need for an OTHERWISE. |
| FOUND() |
Whenever you Seek or Locate, test for FOUND() before assuming that the sought-after record actually exists. You may want to consider using the SEEK() function and checking its return value. |
| Valid Events |
Don't rely exclusively on the VALID method for enforcing data integrity. The valid will only fire if the user enters the control. So, if you enforce something like the field can't be blank, and code that in the valid of the textbox, it is possible that that valid code is never run, and the record is saved. Better yet, you should develop your applications with an nTier architecture and put business logic (data validation) where it belongs: in the middle (or business) tier. |
| Toolbars and Menus never get focus |
Therefore the valid and lostfocus methods of the active control will not fire when the user navigates to a toolbar or menu. There are techniques for handling this but they are beyond the scope of this page. |
|
Closing Forms |
Be aware of all the ways a user can close a form and defend for them. |
|
Closing Tables |
Avoid this practice:
IF !USED('sometable')
USE sometable IN 0
ENDIF
... do some work
USE IN sometable
A program or method should leave things the way they were found (unless it is some sort of cleanup program I suppose). You can do something like this instead:
LOCAL lopened
IF !USED('sometable')
lopened = .t.
USE sometable IN 0
ENDIF
... do some work
IF lopened
USE IN sometable
ENDIF
-- Tod Mckenna In addition to what Tod has suggested I would add that one needs to remember that SomeTable may be open with a different alias and protect against the table is in use error. Todd's code would be altered to ...
LOCAL lopened
IF !USED('sometable')
lopened = .t.
USE sometable IN 0 AGAIN ALIAS SomeTable
ENDIF
... do some work
IF lopened
USE IN sometable
ENDIF
-- Jim Booth ![]() |
|
Reports |
Place the name of your report in one of the bottom corners of the report. It will make it easier to find it again if you need to modify it later. Also this makes it so that when a user calls and says "This report dosn't work right", you can say "What does it say in the bottom left corner?" instead of "Oh, which of the four hundred reports in the system are you talking about?" and being told, "You know, the one I run on Tuesdays for the boss". Saves a lot of time. |
|
IntelliSense |
(VFP 7 and beyond) To avoid having IntelliSense add the "TO" keyword to SET commands (leading to things like SET DATABASE TO TO), go to Tools | IntelliSense Manager | Advanced | Custom Properties. Change the lEnableFullSetDisplay property to F. The default is T. But a better choice is to live with and get used to it. For a while, you'll double up the TO's, but it won't take long before you get your fingers retrained. -- Tamar Granor |
|
REPLACE conflicting with ORDER/INDEX |
If you ever find that a "REPLACE foobar WITH foobar ALL" command is not performing as expected -- specificially that the all records currently FILTERed are not updated!!!
Confirm that the foobar field being REPLACEd is NOT involved in the currently active ORDER of the table (i.e. INDEX ON foobar+CODE TAG primaryIndex.) Resolution: use sOldOrder = ORDER() and SET ORDER TO / SET ORDER TO &sOldOrder to store/restore your ordering bracketing your REPLACE statement. -- Rick Hathaway |
|
TableUpdate() takes 3 or 4 arguments. Use them. |
TableUpdate has more than one argument. The arguments are:
|
|
Always check the return value from TableUpdate() |
There are many situations that can cause a TableUpdate to fail and only ONE of them is an update conflict. Among the other possible causes of failure are server crash, broken network cable, failure of a field or table validity check, failure of referential integrity checks, etc. etc.. Using the second argument as .T. (force past update conflict) does NOT guarantee that your TableUpdate will succeed. Your code should always check the return value and handle update failures. |
|
Add RESOURCE=OFF in Config.fpw |
Unless your application specifically takes advantage of the foxpro resource file (Fox User) add this to your config.fpw settings to prevent it from being created at startup. One less file to open and worry about screwing things up. Note: if in doubt whether you need this, you probably don't - if you use a commercial framework, check with the vendor on whether they rely on Fox User dbf at runtime. This is kind of a "legacy" feature that was useful in the DOS days for managing colors and printer drivers by user or workstation. It also stores browse window settings, etc. (which may be useful in development, but not in deployment) -- Randy Jean |
|
Defend against no printer being installed |
Before you send any output to the printer (including with REPORT FORM), keep in mind that the user will see a run-time error if no printer has been installed. You can test for this by calling APRINTERS(). Mike Lewis This is even a bigger issue than Mike states as sending report output to a file or via a Report Listener in VFP9 may result in a no-op or error if there are no printer drivers installed. John Koziol |
|
Create a clean up macro |
Assign a clean up macro to F4 or some other function key. See: Clean Up Code Carole Shaw
*OR* (another viewpoint) Just put the following in the Macro, and use code in the the program to do the cleanup:
{CTRL+F2}{CTRL+END}DO X:\PATH\CLEANUP{ENTER}
-- Peter Crabtree
Added non-macro version to Clean Up Code topic. ?cs |
| Checking for optional parameters with PCOUNT()/PARAMETERS() | I've seen this sometimes in functions where it is ok. But with objects and inheritance relying on the number of params leads to errors. You misdiagnosed the source of the problem. PCOUNT() is correct way and only way to check for optional parameters. The problem is in incorrect passing parameters through. See corrected version below. I disagree. The case solution is ugly, adds too much code, and depends on knowledge about foo's implementation in the baseclass (which is real bad design). And you can't add a parameter to myBaseclass::foo without breaking your code (or checking with VARTYPE/TYPE). -- Kurt
obj = create("theAncestor")
obj.foo("hello")
define class myBaseclass as Custom
procedure foo
lparameters param1, param2
if pcount() < 2
param2 = "World"
endif
? param1 + " " + param2
endproc
enddefine
define class theAncestor as myBaseclass
procedure foo
lparameters param1, param2
? "I said:"
***dodefault(param1, param2) && pcount() in myBaseclass::foo PCOUNT() will always be 2!
DO CASE
CASE PCOUNT() = 1
dodefault(param1)
CASE PCOUNT() = 2
dodefault(param1, param2)
OTHERWISE
dodefault()
ENDCASE
endproc
enddefine
It's better to check with VARTYPE(param2). If the expected type of param2 is boolean you have to ensure that .F. is the default. -- Kurt Bad idea. You cannot tell based on that check if optional parameter isn't passed or it has been passed with value .F. If you cannot tell if the optional parameter isn't passed, and the default behaviour is to behave as though .F. has been passed, then it doesn't matter. Using PCount() like this excessively clutters code, and means subclassing a function with 5 parameters to perform one additional line of behaviour requires at least 15 lines of code. Furthermore, you cannot add additional optional parameters. In short, if you need to have three behaviours from a boolean value (not passed, passed as .T., passed as .F.), then you shouldn't be using a boolean value. -- Peter Crabtree Thanks Peter. I wasn't clear enough. -- Kurt If a subclass' method is overridden and includes a DODEFAULT that passes all possible parameters, the parent class' method will be unable to react to the value of PCOUNT in the overridden method. It's not necessarily a rookie mistake to use pcount in the parent class, although it may be a mistake to be unaware of this issue. There is a very simple way to preserve PCOUNT up the class hierarchy, without a complex DO CASE, allowing PCOUNT to be used in the parent classes. LPARAMETERS m.tcParm1, m.tcParm2
*Here's a subclass' overridden method.
LOCAL m.lcParmList
m.lcParmList = PassParms("m.tcParm1, m.tcParm2",PCOUNT())
RETURN DODEFAULT(&lcParmlist.)Now when the parent class' method fires, it will have as many parameters as were passed. I created something like this for Visual Max Frame Professional - x5PItems.PRG with more parameter checking. This is enough to demonstrate the concept: *PassParms.PRG
LPARAMETERS m.tcParmlist,m.tnItems
RETURN LEFT(m.tcParmlist+",",AT(",",m.tcParmlist,m.tnItems)-1) -- Mike YearwoodThis looks better than the CASE-thing, but it's still overhead and doesn't solve the problem, that you can't add additional optional parameters. -- Kurt Can't really argue there. I think you can add additional parameters in the lower classes. One alternative is to pass a parameter object. The parent class method would have to know what parameters to expect. The subclass could request a parameter object from another method or factory and you could check for parameter names. -- Mike Yearwood There's repeated mention about the side-effect of the approaches that try to maintain PCOUNT()'s value when calling superclasses being the inability to add additional parameters in the superclass in the future... This inability also results from the PARAMETERS (or LPARAMETERS) statement itself! If a superclass' method has a new parameter added, all subclasses that want to support that parameter must have their parameter statement adjusted anyway. -- ?wgcs |
| Program opens and closes upon start |
What you see is VFP opening and closing the program. In cases, when the program closes it does not release the memory because it did not run the code to do it and/or a reference in memory is still taken (garbage collection not done). A Foxpro program needs a wait state. This is provided by VFP itself when you run in development environment (IDE). In an external (EXE) program you need to supply a READ EVENTS statement. This statement makes the program wait for user input, menu options, command buttons, etc. and react accordingly. Usually, even when your main program resides in a form, you must supply a startring prg that contains the instantiation code for menus and forms, then issues READ EVENTS. The EXIT option (in a menu or a form's Button) issues a CLEAR EVENTS and program execution returns to the PRG after the line READ EVENTS where you proceed to do your cleanup. See also ON SHUTDOWN in help. * main.prg do menu mymenu do form myform READ EVENTS * cleanup code here return && or quit See also: Forms Not Being Released From Memory App Shut Down One Form Vfp App PRB: Menu Appears, Then Application Exits to Operating System PRB:Visual FoxPro App's .EXE Quits & Returns to Command Window PRB: Application Appears Then Exits to Operating System Another cause of a form initiating and then disappearing is when .F. is returned from the INIT() of the form. -- Ben Creighton Or LOAD(), both preventing the object from instantiating. |
on Feb 5, 2002