CATEGORII DOCUMENTE |
Bulgara | Ceha slovaca | Croata | Engleza | Estona | Finlandeza | Franceza |
Germana | Italiana | Letona | Lituaniana | Maghiara | Olandeza | Poloneza |
Sarba | Slovena | Spaniola | Suedeza | Turca | Ucraineana |
Any craftsmans output can be improved by having good tools at hand. The Visual FoxPro development environment includes some very good tools. There are times, however, when the developer needs something more than what comes in the box. When that time comes, you can draw upon many of the third-party products available, or even whip up your own tool on the spot. This chapter looks at the tools that have helped make me a better, more productive developer.
Before FoxPro 2.0, developer tools were almost entirely third-party tools. Code generators and documentation utilities for the various Xbase variants were common. The only alternatives were those that individual developers created for themselves. The most common were some variant of a screen or report 'painter' that would allow the developer to lay out data-entry screens or reports, then automatically generate the code to reproduce it. FoxPro 2.0 introduced the idea of storing the specifications for screens, menus, reports, labels and the entire project in FoxPro tables. Along with that idea was introduced a set of tools for visually designing and managing program elements by interacting with these meta tablesthe Project Manager, Report Writer, Screen Builder, and Menu Builder. These tools had become, for the most part, ones that most of us could use as our primary development environment.
Visual FoxPro 3.0, with some minor name changes, continued to make these tools available. However, the native VFP 3.0 tools lacked some important capabilities, most notably in the Form Designer. As a result, many developers and third-party tool vendors immediately cobbled up some tools to speed form design and layout in particular. VFP 3.0 also provided two new tools: the Class Designer, to allow visual creation of the object classes introduced with VFP 3.0; and the Class Browser, which provided some additional class library management capabilities.
Visual FoxPro 5.0 corrected many of the omissions of VFP 3.0. Features that made it easier to lay out form elements, and to simultaneously set the properties of multiple controls, pretty much eliminated the need for home-grown and third-party tools that had been used to provide these capabilities in VFP 3.0. The VFP 5.0 code editor introduced some significant features, including syntax coloring and the ability to execute a block of code from within the editor. Visual FoxPro 5.0 also introduced a wonderful set of debugging tools that well examine in detail later in this chapter.
Visual FoxPro 6.0 expands on the Class Browser to include functionality that Microsoft terms a 'Component Gallery.' This functionality frees the developer from categorizing components only by where they are stored, and allows us instead to group them by function. VFP 6.0 also introduces a new debugging tool, the Coverage Profiler.
Anyone moderately experienced with Visual FoxPro is usually fairly comfortable with the native Visual FoxPro design tools, so I shant waste a lot of time and space to recap their use. However, I would like to touch on a few techniques that Ive either been slow to adopt, and wondered afterward, 'Why havent I been doing this all along?' or techniques Ive discovered on my own that you may find useful.
The next section discusses creating your own development tools, and the chapter closes with a discussion of commercial third-party tools.
Most components of a project are not represented by a single file. Because the project 'metadata' is stored as Visual FoxPro tables, each table has an accompanying memo file. The actual data, too, consists of multiple files on disk for each entity listed under the data heading in the Project Manager. Thus, when you select a program element you are selecting all files that 'contribute' to that element. For instance, when selecting a form, you are selecting both the .SCX and the .SCT memo file. If you select a database container, youre selecting the .DBC, .DCT and .DCX files that together make up the database container. If you right-click on an item listed in the Project Manager and select 'Rename,' you will rename all files associated with that entity. Contrast this with doing the same thing in Windows Explorer, where you rename each file individually. You then have to deal with the error that occurs when you try to open the project and it cant find the original file. Similarly, clicking the 'Remove' button in the Project Manager acts on all files associated with the selected item. If we respond to the 'Remove from project or delete from disk?' message box by requesting the 'Delete' option, all related files are deleted, and the reference to the deleted element is removed from the Project Manager.
There is a potential trap in renaming a class library. When adding a class instance to a form, subclassing a class, or including a class in a composite class, the class library that contains the class is included along with the class name. Consequently, renaming the class could make a class or form that uses a class contained in the renamed class library inoperable. Fortunately, the Project Manager issues a warning to this effect when renaming a class library. Unfortunately, neither the Project Manager nor the Class Browser is so helpful when renaming a class, so you must exercise due care when renaming classes. In fact, you can hose yourself up very nicely by renaming object classes. The Class Browser seems to attempt to resolve references to a class within the same class library, but in the current VFP 6.0 build Im working with, it seems to be broken. It successfully changes the class name in the reference, but has a problem with the classlib reference. Dont despair if you get into trouble when changing class names, or the location or names of class libraries. A utility discussed later in this chapter (in the section on creating your own developer tools) will save the day.
Along with 'Menus' and 'Text Files,' there is an 'Other files' group listed under the 'Other' tab in the Project Manager. This is often used for .BMP or .ICO files that you want to include in the project. However, you can also list other files that you want to access easily from within the VFP IDE. These might include a Visio class diagram, a 'to-do' list in a Word .DOC file, or a vendors documentation in an HTML Help (.CHM) file (.HLP-style help files apparently cant be opened via the Project Manager).
You can use the Class Browser (discussed later) to move or copy a class from one class library to another. However, I only just recently realized that dragging and dropping a class from one classlib to another in the project manager will copy the class. This is handy when you want to modify a class but dont want to experiment with your production class. You can copy it to another classlib to play around with it.
Layout tricks
One Form Designer trick was completely unknown to me until a 'newbie'whod only been using the Form Designer for a few monthsshowed me something hed discovered. (Probably through something arcane, like reading the documentation! I dont know about you, but the longer I use a tool, the less inclined I am to dig into the help file and online docs. Big mistake.) The Form Layout toolbar has three buttons that size the selected controls to the same height or width or both. These buttons use the largest control as the 'reference' in this operation. As a result, I relied on the menu options, which offer the choice of using the largest or smallest as the reference control, until this fellow showed me that pressing the Ctrl key when clicking the toolbar button would size all of the selected controls to the smallest control in the group. Clicking on any of the alignment buttons to align top, bottom, left or right edges will align the controls using the topmost, bottommost, leftmost or rightmost control, respectively, as a reference. However, the Ctrl key again will alter the behavior of these buttons. Holding the Ctrl key and clicking on the 'Align Right Edges' button will align the right edges but use the leftmost object as a reference.
Another formatting trick is to rely on the Form Layout toolbar buttons that align the control centers. I can never keep straight the two menu prompts for aligning centers ('Align horizontal centers' and 'Align vertical centers'). No sooner do I think Ive got it straight, than I select the wrong one. The trouble is that the menu prompts are just plain wrong. They should be either reversed from their actual actions, or read 'Align horizontal centers vertically' and 'Align vertical centers horizontally.' The good news is that the pictures on the Form Layout toolbar buttons are hard to screw up. A frustrating behavior of these layout commands is that the controls will both be moved to a common center. I often want to align the center of Control A with the center of Control B, leaving Control B in its current position. The trick to getting this behavior is to align the controls edges first, and then align their centers.
The formatting options of 'Bring to front' and 'Send to back' are commonly used to determine which control is visually 'in front of' another control. This permits placing a shape on a form on top of a label, then using the 'Send to back' to move the shape behind the label so the label is visible. However, what you may not realize is that this formatting option works by changing the instantiation order of the controls. If you examine the forms .SCX file, youll see that the order of the controls manipulated with 'Bring to front' or 'Send to back' is actually moved from one record to another to accomplish this. Normally the order in which controls are added to a form determines instantiation order. We can manipulate the tab order of the controls independently by using the TabIndex property, or interactively using the tab-ordering tools. However, this has no effect on the instantiation order of the controls.
This seems like a piece of trivial knowledge until you have two controls that need to be instantiated in a particular order. Youll find that using 'Bring to front' and 'Send to back' will allow you to fiddle with the instantiation order without being forced to remove and re-add the controls.
My ever-vigilant technical editor raised an interesting question with regard to instantiation order. If you have a situation in which the instantiation order of controls is critical, doesnt this imply very tight coupling between the controls? As discussed in the chapter on forms, tight coupling is usually something to be avoided.
The thing to remember is that the issue of tight coupling is best viewed in the context of reusability. A loosely coupled object is more self-sufficient and less sensitive to the environment in which it is used. This is of concern when we are creating a reusable object class. Once we are working with a form instance, the issue of reusability is no longer of concern. We know the environment in which we are operating, and can write code that is customized to that environment. This is true also in composite reusable classes. Each object can be tightly coupled with other objects within the composite class, because their encapsulation within a single container object ensures that whatever environment we require is being provided by the composite class.
Instantiation order can also be critical to actually implementing loosely coupled objects. Consider an object that 'registers' itself with a form when it is instantiated, storing a reference to itself to a form property. An object might do this to make it easier for it to receive messages from the form or other objects, which can reference the form property rather than relying on the name of the first object. Another object may need to communicate with the first, and needs to do so when its Init() code executes. The second class might be very loosely coupled, being able to perform some default behavior in the event that the first object does not exist. However, to realize the full benefit of the cooperation between the two objects, Object A must instantiate (and register itself) before Object B, so that Object B can send a message to Object A when it instantiates.
Bottom line is that the 'loose coupling' issue is indeed important with reusable objects, but tight coupling is expected in form and object instances and in the design of composite objects.
There is a bug that has existed in the Form Designer ever since Visual FoxPro 3.0. Folks at Microsoft have observed this bug, so it isnt entirely in the imagination of those of us who have encountered it in the field. The reason it hasnt been corrected is that no one can consistently reproduce it. Dont you hate those? Anyway, this bug has bitten me a half-dozen times over the years. It wasnt until I had a co-worker who encountered this one on a regular basis that I stumbled on what appears to be a fix.
The behavior in question is the actual swapping of method code. The code you placed in method A will suddenly appear in method B, and vice-versa. The most common place this seems to occur is between the BeforeOpenTables and AfterCloseTables event methods of the Data Environment. However, Ive seen one case in which the entire set of form-level methods was scrambled. Although it takes a while to figure out why your form is blowing up in your face, it isnt too big a deal to move stuff around and put it back where it belongs. The frustrating thing is that the next time you edit and run the form, itll move the method code around again! If you examine the .SCX table, youll see that the form source code is all stored in a single Methods memo field. When the Form Designer closes this file, the source code in the Methods field is compiled to pseudo-code and placed in the ObjCode memo field. This problem appears to involve something that actually takes the code from between one PROCEDUREENDPROC and moves it to another. How can this be?
The answer (I think) is that, contrary to our expectations, it is not a one-way relationship between the Methods field and the ObjCode field. To illustrate this, create a form, place some code in one of the methods, then close and save the form. Open the .SCX table, and examine the record representing the form (usually the third recordyoull see 'form' in the Baseclass field). Youll see the code you just placed into the form method. Examine the Objcode field, and youll see some gobbledygook that represents the compiled version of the code. While youre looking at ObjCode field, erase itselect the entire contents of the memo and hit the <Delete> key! Now, close the table and reopen the form in the Form Designer and examine the method into which you placed some code. Its empty, even though you didnt touch the contents of the Methods field. To verify that you didnt erase the wrong field, reopen the .SCX table and look at the Methods field. The code is still there, isnt it?
This leads to the fix Ive discovered for the swapping-methods behavior. Open the forms .SCX file as a table, REPLACE ALL ObjCode WITH '', close the table, and then issue the command COMPILE FORM <formname>. This will remove all the object code and force Visual FoxPro to recompile all of the code in the Methods field. So far, this seems to do the trick. By the way, this trick also seems to be a generic solution for forms that seem to be 'corrupted.' Ive had forms that the Form Designer refused to open. Purging the ObjCode fields and recompiling has on several occasions restored these forms to a usable condition.
The Class Browser brings an important dimension to managing object classes and class libraries. The Project Manager displays a list of all classes contained in any class library included in the project. The class hierarchy to which a class belongs, and where the class fits into this hierarchy, is an important piece of information missing from this display. When confronted with a complex set of class libraries, whether your own or those for a framework that may be unfamiliar to you, the Class Browser is indispensable for coming to grips with the class structure.
If you examine a class library using the Class Browser, and one of the classes is subclassed from a class contained in another class library, this is indicated by the chevron character (). If you open the class library containing the parent class using the View Additional File button, the Class Browser inserts the class into the class hierarchy. Figures 1 and 2 illustrate this.
Figure 1. The Class Browser showing the cEnvironment class in the CENVIRON.VCX class library. This class is subclassed from the cContainer class contained in the CCONTRLS.VCX class library. Note the View Additional File button.
Figure 2. The Class Browser after adding the CCONTRLS.VCX class library. The chevron character no longer appears next to the cEnvironment class. Instead, the cEnvironment class is shown as subclassed from cContainer.
Another important feature of the Class Browser is allowing redefinition of a class or form. Occasionally Ill spend considerable time creating a class or form subclassed from class B and decide later that it should be subclassed instead from class A. The Class Browser allows me to change the parent class, provided that its descended from the same baseclass objectyou cant redefine an object subclassed from a custom control as a text box. Should this kind of redefinition be necessary, Ive found that it can be accomplished using the HackVCX.APP tool discussed later.
The VFP documentation describes using the Class Browser to copy classes from one class library to another. The documentations instructions as of this writing are incorrect. If you select a class in the Class Browser, drag the class icon that appears just to the left of the Class Type combo box and drop it on a second instance of the Class Browser, displaying a different class library will move the class from one classlib to another. To copy a class, press the Ctrl key before clicking on the class icon. This feature of the Class Browser is super for effectively organizing your class libraries.
You can rename methods in both the Class Browser and in the new Property/Method dialog. However, beware of doing so. If you have a subclass that uses this method, the method code in the subclass gets 'lost' in the process of renaming the class.
Some of the things you can do in the Class Browser, such as renaming a class, or moving a class from one classlib to another, can cause a problem with forms that use those classes. The Class Browser can open form (.SCX) files as well as class libraries. If you have a class that you want to rename or move, load the forms that use that class into the Class Browser along with the classlib containing the class. When the class is renamed or moved, the Class Browser will update the references to that class in all the loaded forms.
The Report Designer is a fairly straightforward tool, and hasnt changed significantly since FoxPro 2.5. Ive never considered myself much of a report-writing whiz, but I have discovered something that is probably worth passing along.
If you create an application that involves report printing (and who doesnt?), you probably give the user some means of selecting a printer to use to produce the report. One of the easiest ways is to prompt the user for a destination printer using the GETPRINTER() function, then place the users selection into a SET PRINTER TO NAME <printer name> prior to running the report. An alternative that provides more control over printing options is to use the SYS(1037) function that invokes the Printer Setup dialog. Either approach will work fine in your development environment. You can redirect a report to any printer installed on your workstation, both local and networked.
The problems start when your client or user tries to direct a report to a printer installed on their workstation. Often, no matter what they do, itll end up going to whatever printer they have installed as their default printer. What the heck is going on?
As you might be aware, the Report Designer stores information in the report file about your currently selected printer when you create a report. This information is found in the first record of the .FRX table, in the Expr, Tag1 and Tag2 fields. An example is shown in Figure 3.
Figure 3. The contents of the first-record Expr, Tag1 and Tag2 fields in a reports .FRX file. Note all of the information about the currently selected printer that is stored in these fields.
Visual FoxPro will indeed respect the users selection of a report destination made using SET PRINTER TO NAME or SYS(1037), but only if the printer specified in the report file is installed on the users system. If the specified printer is not installed, output is always directed to the default printer. The way around this problem is to remove all information contained in these three fields before the reports are compiled into your application. Once this is done, the user is free to direct output to any installed printer.
As usual, there is more than one way to skin a cat. Some developers make a practice of installing report files as free-standing tables rather than compiling them into the application. Developers who follow this practice will often directly manipulate the contents of the EXPR field to change the selected printer, the number of copies, output trays, and so on. This is often accomplished through the printer control options of the COMMDLG control. If you require this level of control but still want to distribute canned reports compiled into the application, there is a middle ground. Any table (including power-tool meta tables like the form .SCX files and the report .FRX files) that are compiled into an application cant be modified, but can be USEd, and COPY TOd. Thus you can USE an .FRX file, COPY TO <temporary path and filename.FRX>, manipulate the contents of the Expr field, issue the command REPORT FORM <temporary path and filename.FRX> and then erase the temporary file.
This report and printer behavior is observed with VFP 3.0, 5.0 and 6.0 under Windows 3.1, 3.11 and Windows 95 and NT 4.0. I havent yet tested this under Windows 98, but would expect to see the same problem.
Over the years Ive often watched developers struggle with reports that involve complex relations between parent and child tables. Ive found that the easiest way to create reports is to run a query or open a view that creates a single, highly denormalized table. Dont be concerned that the customer name and address appears on all 246 records when you only want it to appear in the header. If you place the customer name in the header or footer band, thats the only place it will appear. All the grouping and aggregating functions of the Report Designer work just fine, and in fact, youll have better luck getting consistent results and behavior by creating a single result set on which to base your reports.
The reason this works so well is that you can separate the creation of the result set from its visual representation. You are able to concentrate on getting a set of records that contain all the information you want to appear on the report, and in the order you want them to appear, and then concentrate on layout. With a good result set in hand, you can then concentrate on aggregating, grouping and laying out the information within the Report Designer.
With VFP 5.0 we finally got some cool editor features. I wont belabor them herejust right-clicking in the Editor window and playing with each of the options is all you really need to do. Beautify works quite well (although it hoses up my carefully formatted SQL statements), and is nice in a multi-programmer shop where half the developers prefer tabs and the other half prefer spaces for indenting. The Beautify option allows you to switch code blocks back and forth between the two indenting methods. Another new feature introduced in VFP 6.0 is the ability to set a breakpoint directly in the editor via the Context menu.
I think that many developers overlook the power of the syntax coloring options in the VFP editor. Figure 4 is a screen shot of a code snippet in my editor. Even in black-and-white, you can easily distinguish the various elements of this code, although the background for the literals and strings look to be the same. Table 1 shows the settings I use.
Table 1. My syntax color settings.
Element |
Background |
Foreground |
Style |
Comments |
yellow |
black |
bold italic |
Keywords |
white |
blue |
bold |
Literals |
cyan |
black |
normal |
Operators |
white |
red |
bold |
Strings |
light gray |
black |
normal |
Variables |
white |
black |
normal |
The bright yellow contrasting background, together with the black bold italic foreground on comments makes them jump right off the screen. The contrasting background on the literals and strings makes them easy to spot, too. This also has the added benefit of helping to spot mismatched string delimiters. Try these settings (or something similar) for a session or two, and I think you might be surprised how much more effective you are, particularly in complex chunks of code. You might want to change some of the colorsmany dislike the cyan, but the specific colors are less important than the contrast they lend to different code elements. If you can come up with a color scheme that allows you to stand too far from the monitor to be able to read it, but still identify the operators, strings, literals, keywords, variables and comments, youve got a good color scheme.
Figure 4. Be bold in getting some contrast into your codeuse contrasting
background colors so you can identify code elements from across the room.
Being able to decide what information you need and the ability to collect that information is the lifeblood of effective debugging. Toward this end, Visual FoxPro 5.0 finally gave us some serious debugging tools. The equivalent to the Watch window and the Trace window are all we had in VFP 3.0 and earlier. Both tools are much improved in VFP 5.0 and later, and are joined by some new tools and features that make debugging easier.
The debugging tools are available in two environments, selectable from the Tools|Options|Debug dialog. One is a 'Debug Frame,' in which the entire suite of debugging windows are made available using a top-level form that exists outside the Visual FoxPro desktop. The other is the 'FoxPro Frame,' in which individual debugging windows can be opened as needed and kept visible on the Visual FoxPro desktop while you run your code, do stuff at the Command window, or interact with a form. For what its worth, I personally have no use for the 'Debug Frame' option. I seldom need all of the windows at the same time, and if I cant watch or interact with my app and watch the debugging tools at the same time without Alt-Tabbing, then Im not happy. There is one capability that comes along with the Debug Frame, and that is the ability to save and reload a configurationa set of breakpoints and watch values. This is available only from the Debugging menu, which is part of the Debug Frame. Should I ever need this capability, I will likely temporarily switch to a Debug Frame, load or save the configuration, and then switch back to the FoxPro Frame.
Enter a value and watch it change. Set a breakpoint on its change. What could be simpler? Dont forget that you can change the value of a memvar or field in the Watch window. Also remember that you can drag and drop expressions from the Trace window or a code window into the Watch window. I always have a reference to _SCREEN.ActiveForm in the Watch window.
There is nothing so illuminating as watching your code execute line-by-line. How many times have you had some piece of code that continues to misbehave, even after several cycles of tweak-run-tweak-run? Many times Ive finally traced the code and immediately seen the error! Visual FoxPros Trace window has some wonderful features, including the ability to place the mouse pointer over a variable or field and some simple expressions, and see the value. No need to place the variable or field name in the Watch window. Its a big time-saver to be able to execute, without actually stepping through a user-defined function or another procedure called from the routine we are tracing, by selecting 'Step Over' from the Debugger toolbar or the Context menu. Likewise, it saves time to be able to 'Step Out' of a procedure or function that we are reasonably sure is working as expected, so we can 'cut to the chase' and get back to the calling routine.
One of my favorite execution options in the Trace window is the 'Run to Cursor' option. You can scroll the Trace window and set a breakpoint, but you can also simply click the mouse pointer to place the blinking cursor on a line of code, then select the 'Run to Cursor' option. Execution will continue until the indicated line is encountered, just like setting a breakpoint. This is useful for zipping through a FOR/NEXT or WHILE loop when youre only interested in the results of the loop rather than watching it run through each iteration of the loop.
It took me a long time to notice one feature of the Trace window. By right-clicking to bring up the Context menu, I can load a .PRG file and then use the execution options (Step Into, Run to Cursor, and so on) to immediately begin tracing the code. No need to set breakpoints. Unfortunately, you cant do the same with forms or objects.
This window lists all objects and variables that are scoped to the currently executing procedure. At first, given the ability to just wave the mouse pointer over a memvar in the Trace window, the Locals window doesnt seem to be particularly useful. However, whats local? Most experienced FoxPro developers are very conscious of variable scope, but you will occasionally forget to declare a memvar local, and thisll bite ya in the butt. Looking at the Locals window, youll see that a memvar has a value before it should, because its not local to the calling routine, but PRIVATEtherefore, its visible to the called routine.
The Locals window is also always there, so when you want to check a memvar or object that you forgot to place into the Watch window, you can just check the Locals window. When working at the Command window, too, particularly when creating objects and arrays, the Locals window is a real help.
As with the Watch window, you can change the value of a variable or field in the Locals window, but unlike the Watch window, you cant set a breakpoint.
Using the Trace, Locals and Call Stack windows together puts an incredible amount of power in the developers hands to examine every aspect of an executing program. The Call Stack window can help debug a complex situation, such as a form with methods that call other methods. Im sometimes puzzled as to why a certain method is being called under certain circumstances, or uncertain which of several possible calls to a particular method is the one that got me where I am.
Figures 5 and 6 show just how powerful these three debugging tools can be. I set a breakpoint on the Requery() method of the example Time Card form used in Chapter 5. If youll recall, this method was called through a cascade of events started with the change in a combo box. As you can see from Figure 5, the Call Stack window shows the entire chain of four methods that were followed to get to the Requery() method. The Locals window shows the currently scoped memvars (only the Time Card form), and the Trace window shows the currently executing code.
Figure 6 shows the same debugging session, but Ive selected the next method up the calling stack, the uKeyValue_Assign method (note the ► symbol to indicate the currently selected routine). The Trace window changes to show this code snippet and the Locals window changes to show the variables scoped to this piece of code. In this case there is only one memvar declared and in use, vNewVal. Note that the 'fly-over' value display works only in the currently executing routine. You must rely on the Locals window to check the values of memvars in routines other than the one currently executing.
Figure 5. Tracing the Requery() method of the Time Card form discussed in Chapter 5.
Note the position of the pointers in the Call Stack and Trace windows, and the
contents of the Locals window.
Figure 6. Tracing the Requery() method of the Time Card form
discussed in Chapter 5.
Those of us who are slow learners have used (for far too long) the old techniques of setting breakpoints in the Trace and Watch windows. This will automatically add the breakpoints in the Breakpoints dialog. However, the Breakpoints dialog is much more flexible and efficient. You can easily set a breakpoint on a combination of conditions. As shown in Figure 7, using the Time Card form again, I can set a breakpoint for the Requery() method, but specifically for the TimeCard.SCX object, so that another objects Requery() method doesnt trigger the breakpoint. I can also set the breakpoint so that the execution is suspended only when the uKeyValue property is 3. Setting a breakpoint on a particular line of code in the editor or the Trace window will trigger the break every time that line of code is executed. The Breakpoints dialog will allow you to specify how many times the line of code is executed before triggering the suspension of execution, by using the Pass Count setting. You might be able to achieve these results by setting a breakpoint on a properly constructed expression in the Watch window, but the Breakpoints dialog is much easier. The Breakpoints dialog also allows you to establish a breakpoint and then enable or disable it as needed.
Figure 7. The Breakpoints dialog, showing a complex breakpoint condition-
a combination of object, method and variable values.
For years Ive peppered my code with WAIT WINDOWs to identify when certain code segments are executing. The Output window and the DEBUGOUT command provide a little more flexibility when actually outputting variable, field or property values because you dont have to convert the values to the character data type as you do when using WAIT WINDOWs.
As Ive climbed the Visual FoxPro learning curve, Ive struggled with understanding the exact order in which events occur. Often events occur in an order that is contrary to what I might expect, and this misunderstanding can lead to some difficult debugging sessions. In Visual FoxPro 3.0, all we had to use was code in each event method that reported when it was being triggered. To address this problem I created a testbed form with special methods and properties for reporting on event firing.
VFP 5.0 introduced the Event Tracking feature. This tool reports the firing of selected events to the Output window. A little time spent with the Event Tracking dialog to confirm or clarify your understanding of the order in which events fire, or exactly when certain events fire, can save a lot of time when writing event-dependent code. When tracking a large number of events, it may be more appropriate to use the option of directing Event Tracking output to a file. Figure 8 shows the Event Tracking dialog with several events selected. The results from launching the Time Card form with these settings are shown in Figure 9.
Figure 8. The Event Tracking dialog. The BeforeOpenTables,
Load and Init events have been selected for tracking.
Figure 9. The Debug Output window showing the results of opening the
Time Card form with Event Tracking turned on.
If you ever have an unidentified performance bottleneck in an application, Coverage Logging may help you track it down. Coverage Logging allows you to specify a log file to which program execution information is written. You can specify the log file by issuing the SET COVERAGE TO command or by clicking on the 'Toggle Coverage Logging' button in the Debugger toolbar. With Coverage Logging turned on, information on execution of any program elements is output to the specified log file. The file stores this information in a comma-delimited text format. VFP 6.0 added an additional sixth column to this output, the 'Call Stack Level,' which was not included in the output in VFP 5.0. Table 2 shows the contents of a coverage log file, but abbreviates the filename to exclude the file path, which is normally included.
Table 2. Contents of a Coverage Log file.
Duration |
Class |
Procedure |
Line |
File |
Call Stack Level |
Cboemployees |
Cboemployees.init |
chap5.vct | |||
Cboprojects |
Cboprojects.init |
chap5.vct | |||
Cboprojects |
Cboprojects.requery |
chap5.vct | |||
Cboprojects |
Cboprojects.requery |
chap5.vct | |||
Cboprojects |
Cboprojects.requery |
chap5.vct | |||
Cboprojects |
Cboprojects.requery |
chap5.vct |
As you can see, cboProjects.Init() takes 80 milliseconds to execute, and line 12 of cboProjects.Requery() takes 10 milliseconds to execute. The other logged events take less than a millisecond to execute. This kind of information makes it easy to spot which lines of code are slowing things down.
VFP 6.0 introduces a coverage profiler, invoked by selecting 'Coverage Profiler' from the Tools menu. In order to use it, you must first produce a coverage log file. The Coverage Profiler takes this log file and does some very interesting analysis with it. Not only does it show the lines of code executed along with their execution times, but also how many times each line of code is executed. This is great for spotting the control that is being refreshed 1,746 times for every KeyPress. Figure 10 shows the Coverage Profiler in use.
Figure 10. The Coverage Profilers analysis of the log file created while running TimeCard.SCX.
Microsoft has given Visual FoxPro developers more debugging horsepower than weve ever had. For more ideas about how to put these tools to use see the Debugging appendix.
One final note on debugging tools doesnt relate to a tool at all, but is something to stand up and cheer about. Im sure weve all been faced with the Access Violation or Illegal Operation error that brings our app crashing down around our ears. Until VFP 6.0 you were forced to step through your code one line at a time until you could spot the line of code that was triggering the error. Ill discuss ways to attack this particular problem in Appendix Five. However, in VFP 6.0 Microsoft has given us a new error message box that is raised when an access violation or illegal operation occurs. This message box identifies the module and line number where the error was triggered. We cant trap this information in an error log file as we can other errors. However, if this information can be noted during development or in production, it can save a lot of work in tracking down and correcting the problem.
No matter how powerful the tools that come in the box, there are always times when we need or want something a little extra. Visual FoxPro stores much of the 'source code' we create in FoxPro tables. Since working with FoxPro tables is what we do all day long, this makes it very easy for us to whip up our own utilities. Some of these are quite impressive and have been turned into commercial products. However, a lot of us are hackers at heart and love making our own tools. My first published article was about a tool that took advantage of the fact that a .PJX file is simply a table. Written for FoxPro 2.0, it searched the project directory tree for files that werent found in the project, and gave the developer the opportunity to examine and archive or delete the extra junk that had accumulated in the projects space. (See Cleanup.SCX later in this section for a tool that could be modified to do this with VFP applications.) Another published article covered a much more sophisticated tool I created for doing a global search-and-replace within a project. I still run into folks today who are using it.
If you havent done so, Im hoping that the two simple examples here encourage you to build your own tools when needed. Youll often find tool creation to be one of the more challenging and educational activities in your career as a developer.
How often have you tried to open a form or report or some other power-tool meta table using the power tool, only to be faced with a cryptic error referencing some element that you thought youd removed? Sometimes the error refers to a specific line of the source-code file, making it easier to find the problem, but often youre not so lucky. Sometimes a piece of information is buried in a large application data file. It might be some 'dirty' data that is causing a problem. Other times it might be a value that you suspect is seldom, if ever, encountered, and you want to search the entire table for the value.
SearchAll (see the source code in Listing 14-1) loops through the entire table, including all the fields, looking for matches between a search string and the contents of any character and memo fields encountered. In addition to the first two arguments, specifying a search string and the table name, there is an optional third argument that allows SearchAll to report the contents of one of the table fields. This utility falls into the 'quick-and-dirty' category. Its just a programthere is no interface and no slick installation program.
Running SearchAll with the following line of code:
DO searchall WITH 'Expense','comboclass.scx'
Yields these results on the VFP desktop:
Record 4 Field PROPERTIES
Record 5 Field PROPERTIES
Record 5 Field METHODS
Record 5 Field OBJCODE
Running SearchAll with the optional third argument also reports the name of the object in which the search string was found:
DO searchall WITH 'Expense','comboclass.scx','objname'
Record 4 Field PROPERTIES - objname: Cbosystemcodes1
Record 5 Field PROPERTIES - objname: Ccommandbutton2
Record 5 Field METHODS - objname: Ccommandbutton2
Record 5 Field OBJCODE - objname: Ccommandbutton2
You could just as easily search a 500,000-record data file such as an 'orders' table that had some corrupted data somewhere in it. If you had determined that the corrupted data involved null characters (CHR(0)), you could search the entire table for the corrupted records by using:
DO searchall WITH CHR(0),'CustOrder','cOrderNo'
While SearchAll.PRG is written to search for a character string, you could easily modify this program to be a little more flexible, and allow you to search for logical, date, datetime, or numeric values.
Listing 14-1. SearchAll.PRGa utility program to locate a character string anywhere in a table.
LPARAMETERS tcString, tcTable, tcReportField
LOCAL lcString, lnFields
lcString = UPPER(tcString)
IF NOT USED(tcTable)
USE (tcTable)
ENDIF
SELECT (tcTable)
lnFields = FCOUNT()
LOCATE
SCAN
FOR i = 1 TO lnFields
IF TYPE('tcReportField') = 'C'
lcReport = 'Examining ' ;
+ ALLTRIM(TRANSFORM(EVALUATE(tcReportField),'')) ;
+ ': ' + FIELD(i)
ELSE
lcReport = FIELD(i)
ENDIF
WAIT lcReport WINDOW NOWAIT
IF (TYPE( FIELD(i) ) = 'C' OR TYPE( FIELD(i) )= 'M') ;
AND lcString $ UPPER(EVALUATE(FIELD(i)))
? 'Record ' + LTRIM(STR(RECNO())) + ' Field ' + FIELD(i)
IF TYPE('tcReportField') = 'C'
?? ' - ' + tcReportField + ': ' + EVALUATE(tcReportField)
ENDIF
ENDIF
ENDFOR &&* i = 1 to FCOUNT()
ENDSCAN
A big step up from SearchAll is Rick Schummers HackVCX.APP. Its still a very simple application in principle, but Rick has taken the time to put a nice interface on it. In fact, thats what this little babys all about. As I mentioned in the sections on the Project Manager and Class Browser, you can hose yourself up pretty thoroughly by renaming classes and class libraries. Sometimes just trying to change your directory structure or moving a project to a different drive can cause problems. One of the worst messes to try to clean up is when you take over a project that used VFP baseclasses to construct everything and you want to jack up each form and slip your foundation classes under it.
You can address these problems by opening each affected form or class library as a table, then browsing and hacking each object class and class library reference to get them all pointed to the right classes in the right places. Given the fact that much of this information is stored in memo fields makes this a bit of a pain in the ol butt-ola.
HackVCX to the rescue. Nothing fancy, but it makes fixing these kinds of problems a lot more bearable because it exposes all the memo fields in a .VCX or .SCX file in edit boxes. Rick originally wrote this as a demonstration of an SDI form, but I found that changing the ShowWindow property from '2 As Top-Level Form' to '0 In Screen' gave access to the Cut and Paste options on the System menu.
Note in Figure 11 the 'Compile SCX?' check box. When checked (the default), the Classlib or Form file is recompiled when the form is closed.
Figure 11. HackVCXa handy tool for editing
an .SCX or .VCX file.
Taking roll-your-own tools to another level yet is Jim Booths Cleanup.SCX. A single-form tool, its purpose is to accommodate those who must transport an entire project development directory from one machine to another, either via e-mail or a space-limited medium such as a SyQuest EZ-135 drive or an Iomega ZIP drive.
During development, many of the meta tables increase in size, simply as a result of your editing existing features. As modifications are made, records in the tables are deleted and new ones added. Someone at Microsoft wisely figured out that this is easier than editing an existing record. This is why the Database Designer has a 'Cleanup Database' option, and the Class Browser has a 'Clean up class library' toolbar button. Both of these operations pack the meta tables. Taking up additional disk space in a project directory tree are .BAK files for edited program files and modified .DBF files, .FXP files from compiled programs, .TBK files from modified tables with memo fields, and .MNP and .MXP files from generated and compiled menus. All this 'stuff' can make the difference between getting the entire project onto a single Zip disk or not.
Jims utility walks the directory tree starting from its startup directory, compiling a list of all folders it finds and whether or not a particular folder contains any Visual FoxPro files. This information is displayed in a TreeView control. As shown in Figure 12, you can then determine which directory to process, whether to process subdirectories, and which types of files to process.
Figure 12. Cleanup.SCX, a tool to recover disk space within
a project directory tree.
This utility has some wonderful example code. Take a look at the Init() and PopList() methods for an example of how to instantiate an ImageList control, set its properties to a list of images, instantiate a TreeView control, and populate its nodes, all programmatically. Note that youll need to have both the TreeView and ImageList controls installed on your machine in order to use this tool. If you didnt elect to install these ActiveX controls during your Visual FoxPro installation, you can do so later by restarting the VFP Setup program, selecting Add/Remove components, and then selecting these two controls from the list of ActiveX controls that ship with Visual FoxPro.
Cleanup.SCX also (both in its PopList() and CleanThem() methods) shows how to handle recursively called methods and walking a directory tree.
You can modify how this tool works or what it does. For instance, you could modify it to walk a directory tree from the current directory, rather than the directory in which Cleanup.SCX is installed. You could use Cleanup.SCX as the basis for an application that would examine a project file, and then walk the directory tree looking for files that are on disk but not included in the project. These files are candidates for archiving or removal from the disk to recover disk space.
The third-party tool and add-in market has always been fairly active for Xbase developers. With Visual FoxPros migration away from its Xbase heritage toward object orientation and a place within Microsofts Windows DNA architecture, and the demise or significant decline of competing products such as Visual dBase and CA Visual Objects, the market is not what it once was. However, there is still a wealth of significant products that can make you more productive or effective as a Visual FoxPro developer.
I want to make a distinction between developer tools and application add-ins. Some products are intended to be included in your applications and tend to provide some additional, usually commonly requested, functionality. Query building, report generation, data encryption, and internationalization are common examples of the kinds of functionality an add-in product provides. ActiveX controls are also included in this category. Add-ins do indeed speed the development process, but do so by substituting someone elses code for your own. The tools I examine here are two that have contributed greatly to my productivity in creating my own applications by enhancing the development process, rather than providing plug-in functionality.
Both products discussed below relate to the foundation of our development efforts, and indeed for the final applicationthe database. I use both products and have found that, while they both assist in database management and design, they have different spheres of operation and are highly complementary. While I do briefly describe their features, this is not an exhaustive review of either product. Both have been reviewed in FoxTalk and FoxPro Advisor.
Xcase, from RESolution, Inc., is a database-design CASE tool. The standard edition is designed to work solely with Visual FoxPro databases and tables, while the professional edition works with Visual FoxPro databases, the Visual Basic/Access Jet database and a number of other database servers including SQL Server and Oracle. It should be noted that Xcase will also work with FoxPro 2.x tables.
Xcase is a two-way tool. You can create a model in Xcase, and from that model create a VFP database. If you have a Visual FoxPro database, you can 'import' that database into an Xcase model, make changes to it, and then update the VFP database with the changes youve made in the Xcase model. If you make a change in a VFP database for which you have a Xcase model, you can then update the Xcase model with the changes made to the database.
The exception to this is views. The Xcase view designer is far superior to the VFP View Designer in creating and maintaining views. Changes to the database structure are automatically reflected in the views maintained in Xcase. Views automatically inherit the properties of the fields of the tables on which they are based, including captions, default values, and (unfortunately) rules. However, Id rather have all the features migrate rather than none. The folks at RESolution very wisely (in my opinion) decided that their time was better spent working on more important features than the challenging task of parsing SQL commands and winnowing out all of the view properties from a Visual FoxPro database. You can maintain VFP database views in Xcase, but you cannot create or make changes to views in the VFP database and then update the Xcase model with these views. The practical effect of this limitation is actually beneficial. Ive found it more convenient to maintain read-only views in a viewscript program. The reason is that its often far easier to write, debug and test a view while in the Visual FoxPro development environment, than it is to create the view in Xcase, update the VFP database from the Xcase model, and then test it back in VFP. Because of the limitation in migrating views from the VFP database to the Xcase model, Xcase in effect ignores views that are added to the database that dont exist in the model. You can freely add views to the database without fear that theyll be overwritten when you update the database from the model.
Xcase allows you to establish a standard of using a surrogate key for all tables. When establishing a relation by visually connecting two tables, the primary key of the parent table is automatically migrated as the foreign key of the child table. Primary and foreign keys are automatically indexed. You can select any entity and display a list of incoming or outgoing relations. Relations can be named. The conventional Cascade/Delete/Restrict/Ignore Referential Integrity rules can be established for all relations, and Xcase can generate Xbase-style RI code, similar to that created by the VFP RI builder. Xcase RI code also provides for two additional options for RI enforcement, one which assigns a default foreign key value to orphaned child records, and another that assigns the primary key value of a default parent record to the foreign key fields of orphaned recordssort of an 'automatic adoption' behavior.
One of the most powerful features of Xcase is the concept of 'domains.' In Xcase, a domain is a user-defined data type. For instance, if I decide that my key fields will be system-generated, six-character fields, and define the domain of these fields as 'Key,' I can create a table. Instead of specifying the field type and length as C(6), I can simply specify the type as 'Key' and be done with it. I can also associate a whole host of default values for a defined domain, including caption, rule, default value, comment, rule text and about 20 others. To see the beauty of this system, consider being halfway through designing a database of 80 tables, using this Key domain all along to define primary and foreign key fields. If you decide that this type of key wont work for some reason, without Xcase youre doomed to spending several hours going back and redefining all primary and foreign key fields. With Xcase you simply redefine the key domain and this change cascades throughout the model.
Visual FoxPro provides its own database design tool, called (not surprisingly) the Database Designer. This tool works fine for a small database, but quickly reaches its limits to visually represent a database schema as the database grows in size and complexity. To graphically illustrate this point, Figure 13 shows a database that I am currently involved with. Dont be dismayed if you cant make any sense of this illustration. This is a screen shot from Xcase that shows the entire database. Zoomed out as it is, nothing is legible, but you can get some feel for the complexity of the database from the numerous lines representing the persistent relations between the tables. Note that this picture would be even more illegible if it included the overloaded codes table that is related to virtually every other table in the system, with as many as 50 relations to a single table.
Figure 13. A complex database as it appears in Xcase
If Xcase couldnt improve on this mess, it wouldnt be worth much. Figure 14 shows an example of Xcases zoom window, which displays a magnified view of the area under the mouse pointer.
Figure 14. Xcases zoom window acts like a magnifying glass that coincides with the mouse pointer. Note the mouse pointer over the Services table in the lower left-hand corner.
Even better, you can define and name multiple displays, each of which can zero in on a subset of the tables in the database, as shown in Figure 15.
Figure 15. A named display within Xcase showing one segment of the entire database.
The Xcase displays are conventional entity-relationship diagrams. You have complete control over the cardinality of the relation types, such as one-to-one, one-to-none-or-many, or many-to-many. In this display, the table names, field names, field types and sizes and relations are all displayed. There are several options for how the entities are displayed. You can include only the entity names or their descriptions. You can display only primary and foreign key fields. You can choose to display field types and sizes. You can set colors for foregrounds, backgrounds and text styles. In Figure 15, if any child tables related to the AttendRate and SvcRate tables were included, the color of the relation lines would match the background colors of the parent tables. Field descriptions can be displayed instead of their actual names in the tables. This is useful during the initial design stages when youre working with a client or end user. You dont have to explain your naming convention for fields because everything is displayed in terms understandable to the client.
Xcase provides full support for all features of Visual FoxPro, including default values, captions, DisplayClass and DisplayLibrary properties, rules, triggers and comments.
Another feature of Xcase that I particularly like is the ability to 'browse' the tables that store the entity, field, index, relation and view information. This makes it easy to ensure that all address fields are consistently named cAddress, and that I havent used cZipCode in some tables and cZip_Code in others. Figure 16 shows one of these 'browsers.' (I placed this word in quotes because Xcase uses the term browser to refer to a different, customizable form that displays information about database elements.)
Figure 16. An Xcase browser showing the fields for a multiple tables.
Each of the browsers can be sorted according to several different fields. Its even possible (if none of the predefined indexes do the job and within certain limitations) to define your own index expressions.
The professional edition of Xcase is helpful when migrating a local-data application to a client/server architecture. Even if you have no immediate need to design a client/server application, the professional edition allows you to view the actual data in the database using ODBC.
There are a couple of reasons that I find a CASE tool like Xcase so valuable.
Software development is an iterative process. CASE tools help you stand back and look at the 'big picture' and do a much better job of getting things right on the first pass. With Xcase, you can fiddle and tweak and move and rename and change the way things are laid out, and get a very good feel for how the entire database fits together. But this isnt the only strength of this kind of tool. I find it hard to imagine any database that is 'cast in stone' once the coding starts. During development, various problems or shortcomings of the database design will become evident. Often the time and effort involved in making a change prevents the developers from doing it. Xcase wont make changing the code any easier, but it will certainly make modifying the database much easier. As an example, consider the use of domains mentioned above. This kind of capability allows sweeping changes to be made in a matter of minutes instead of days, and therefore encourages developers to make changes to the database design as soon as the need becomes apparent, rather than saying 'Were too far along now to make such a change.'
Many development efforts are a little short on documentation. Im as guilty as anyone else of not adequately documenting systems Ive created. Xcase allows you to document the database in the process of creating it. When the project is finished, you can spend a little time adding some graphical and explanatory elements to the displays, and print out an entire book of diagrams and reports describing every aspect of the database.
For a database like the one used in the samples for this book, Xcase would be a bit of overkill, although it does a very nice job of documenting even a simple database. But it becomes indispensable when working with a database like the one in Figure 13, with more than 60 tables and hundreds of relations. Without Xcase, Im afraid my coworkers would have long ago found me under my desk, drooling and babbling and playing with my fingers.
If Xcases strong suit is database design, SDTs strong suit is database maintenance. As I near completion of a development project, database modifications become less frequent, and I can concentrate on the maintenance issues that need to be addressed when the application is in production.
SDT 5.1 works with both VFP 5.0 and VFP 6.0, and can be used as a much superior replacement for the VFP Database Designer. It provides outstanding support for views, migrating table field properties to the view fields and keeping the views 'in sync' with the tables when the table structures are changed. You can set a flag on those views that must be maintained in code, so that you dont inadvertently try to load a complex view into the View Designer. Unlike Xcase, SDT wisely excludes field-level rules from the table properties that are migrated to views.
One of the thorny problems involved with maintaining a database is updating a clients or users database when its necessary to make structural changes. You code and test against the 'master' database, but then you need a way to make the clients database look like yours. SDT includes a utility program to update a clients database based on changes youve made to the 'master' database, simplifying the process of updating the client site.
SDT has some important maintenance capabilities that begin to blur the line between a developers tool and an add-in. The updating feature is designed to be included in your application to facilitate changes to the database structure. SDT also includes distributable object classes that allow packing and re-indexing tables more reliably than the native PACK and REINDEX commands.
The structure of each database must match the structure as described in the database container. Sometimes during development, in the process of modifying table structures and moving things around, these two structures can get 'out of sync.' Attempting to open a table under these circumstances will trigger an error to the effect that the database container and the table structure dont match, and VFP wont open the table. The VALIDATE DATABASE RECOVER command is intended to fix this situation, but can do so only by removing the tables information from the database container and removing the back-link from the table to the DBC. This destroys all default values, validation rules, relations, triggers, and so on, that may have existed in the DBC. This is not an optimum solution, and even if it were, the VALIDATE DATABASE RECOVER command cannot be used in the runtime environment. SDT can successfully resolve this problem without destroying any information in the process.
SDT does its magic by storing information about the databases structure in a set of 'metadata' tables through a technology known as DBCX II. This technology begins by describing the database structure, picking up where the database container leaves off, most notably by storing complete index and index expression informationsomething sorely lacking in the database container. DBCX II also provides an opportunity to create any kind of extended property for both tables and individual fields. For instance, if you want to create a two-line heading for use on reports, one that is different from the Caption property stored in the database container, you can create two text-type properties for your table fields, called cReptHead1 and cReptHead2. Once youve defined these as field properties using SDT, you can then establish the report headings for each field in the database. Ive used SDT for this purpose, as well as setting flags on tables that cant be packed, or setting security properties that determine what fields can be seen by which users.
SDT can also automatically produce documentation, completely describing your tables as well as their structures, indexes, and extended properties.
Historically, many people have been critical of the .DBF file structure, calling it 'fragile' and therefore unreliable. In my opinion, improvements to FoxPro and Visual FoxPro over the years make Visual FoxPro files as stable and reliable as a file-based database system can be. Using a UPS (Un-interruptable Power Supply on all file servers and regular routine maintenance, which includes re-indexing all tables on a regular basis, I have never personally seen a single case of file, header or memo pointer corruption in a FoxPro or Visual FoxPro database. However, there is another layer of complexity once you introduce the database container. While in my experience the database container is not at all fragile, in a production environment the DBC and its features are prone to getting snarled up in the development environment because of the unspeakable things we do to them in the process of writing applications. I spend a great deal less time struggling with this kind of problem by using SDT to maintain my database during development.
Whenever a friend or acquaintance gets married, I always try to take the groom aside and explain to him an unwritten clause in the marriage contract: For every 'Honey-Do' project that The Wife comes up with, The Husband gets to buy a new tool. Hang curtain rods? Sure! But the job cant be done without that nifty new 15-amp hydraulic curtain-rod installation tool with the power plaster-dust collection system.
Some of us are tool freaks, and that seems to carry over into our work as developers. Some developers get as much or more satisfaction from creating development tools as we do actually creating applications. Some enjoy it so much that they begin producing tools for others, either as freeware, shareware or commercial products.
If you havent already, Id like to encourage you to try your hand at creating some tools of your own. Youll find that doing so often extends your knowledge of Visual FoxPro more than working on an application, and will make you a better developer in the process.
If youre not inclined to build your own tools, be sure to check the file libraries in CompuServes VFOX and FOXUSERS forums, and on the Universal Thread. Youre likely to find some wonderful tools that other VFP developers have created and which you might find useful.
Give some consideration to the many tools advertised in FoxPro Advisor and found in the Resource Guide that came with your copy of Visual FoxPro. Post a message in the VFOX forum or the Universal Thread and ask others if theyve used these tools and whether they find them useful. Be advised that you should be careful when evaluating folks responses, since their way of working may be very different from yours. A recommendation from any one person may or may not be meaningful to you if your philosophies of application development are different.
The important thing is, whether you make, borrow or buy your tools, that they can be important to your productivity and the quality of your product, and deserve as much attention as you devote to any other aspect of your professional development.
Politica de confidentialitate | Termeni si conditii de utilizare |
Vizualizari: 1625
Importanta:
Termeni si conditii de utilizare | Contact
© SCRIGROUP 2024 . All rights reserved