Finally! @ Say...Get
By: George Burt, President of TrueShot Produce Systems, Inc.
PERHAPS the most painful loss required of dBASE 7.x developers is the surrender of the @ Say ... Get commands.   Many have screamed that dBASE is not dBASE without them.  Yeah, they are not OOP.  Yeah, they lack flexibility.  Yeah, progress marches on and if you are going to make an omelet, you have to break a few eggs...but if dBASE 7.x is so powerful, why is it depriving us of useful tools?  How is that supposed to make our life easier?

The numerous answers to that question have been discussed in such depth in the Newsgroups, that I will not spend much time on why.

The basic problem in the @ Say ... Get structure is the Read command.  In procedural programming, it is common to build in stages where the program flow is waiting for the user to enter data so it may proceed to the next step where it usually processes the data just entered.  With a Read statement, there is usually one intended exit point, that is the step right after the Read command.  In the object oriented way of a dBASE form, there can, and often are, many exit paths.  The brilliance of GUI interface combined with OOP is that many, many program flow directions are available.   Whether it is a mouse click, a change in focus, a mouse movement, a timer event, a keyboard event or whatever, the impetus for the code to run next can come from many sources.  That is what “event-driven” is all about.

The Read statement is not just a “have nothing to do” state, which is the normal resting state of an OOP application.  It is more like the msgBox object in dBASE 7.x.  The message box says, “hold your horses! There is something we need to tell the user, and we need to stop everything until they say its OK to go on.”  After the user responds to the msgBox, the program flow picks up on the line immediately following the call to the msgBox. This behavior is very similar to what the Read statement does.  Since the msgBox is a type of modal form, I use a model form as the platform for the @Say...Get...Read process.

Consider the following:

MuserName = space(30)
Mage = 0
@ 10, 10 say "Enter Your Name: " get MuserName
@ 11, 10 say "Enter Your Age: " get Mage
read
msgbox(rtrim(MuserName)+" you are about: "+;
      ltrim(str(Mage*52))+" weeks old.","Wow. You are old!")

The simplicity is appealing.  Many of us can write this kind of code with our eyes closed.  To do it in a form is not exactly hard, but we have to keep our eyes open and we have to worry about all the events that can happen.  The nice thing about a Read is that we know exactly what is going to happen next. You can “get on with your life” so to speak after you have written the code above because there is just not much that can go wrong with it.  The worse that can happen is that the user can fail to enter anything, but that is predictable and easily handled.

The only problem with the code cited above, is that it won't work in dBASE 7.x.  You need a little help from a program I wrote called Terpret.  It is short for interpreter, which is what it is.  To actually run the code above, you need to save it to a text file.  By convention, I use “.scp” as my extension.  This is short for script.  You could just as easily write it to “.prg” file.

Suppose you wrote the code above to “test.scp”

You would run it by:

Set procedure to Terpret.prg additive
Terpret("test.scp")

Terpret creates a new form, lays out the text and entryfields according to @ Say and Get commands, and then opens it readmodal.  Opening a form ReadModal suspends all other processing until the form closes.   In this way, you have not violated the OOP concept.

Remember that the variables used in a Terpret script are the same as that for any program.  That is, when the Terpret.prg file concludes, only the variables that have references outside of Terpret exist.

Terpret("test1.scp")

Where test1.scp contains:

_app.MuserName = space(30)
_app.Mage = 0
@ 10, 10 say "Enter Your Name: " get _app.MuserName
@ 11, 10 say "Enter Your Age: " get _app.Mage
read
msgbox(rtrim(_app.MuserName)+" you are about: "+ltrim(str(_app.Mage*52))+" weeks old.","Wow. You are old!")

If you were to run this script, then both MuserName and Mage would still exist attached to the _app frame.  This is a brute force way of saving variables and should generally be avoided.  The smarter way to do it is to pass a reference to the object which called Terpret.  This can be done like:

Terpret("test.scp",form)
or
Terpret("test.scp",this)

For technical reasons, “form” and “this” will not work inside of a Terpret script.  Instead you have to use “fform” and “tthis

Suppose you issue the following command linked to a pushbutton:

Terpret("test2.scp",form.entryfield1)

And test2.scp contains:

@ 10, 10 say "You realize, of course, that you have"
@ 11, 10 say "entered the following information: "
@ 12, 10 say "Please fix it and try not to be so stupid!"
@ 14, 30 get tthis.value
read
fform.text ="Don't be soooo stupid!"

This will result in a form popping up and demanding that the information be fixed.  When the user fixes the value, it will also fix the value of the form's entryfield1.  Also it will change the text of the form's title bar to encourage the user to do better in the future.

You can build in more logic too.

Mcompname = space(20)
Maddress  = space(30)
Mcity     = space(30)
Mstate    = space(2)
Mzip      = space(10)
@ 10, 10 say "Enter Name:    " get McompName
@ 11, 10 say "Enter Address: " get Maddress
@ 12, 10 say "Enter City:    " get Mcity
@ 13, 10 say "Enter Zip:     " get Mzip
read

if .not. file("rolo.dbf")
   create table rolo (compName char(20), address char(30), city char(30), state char(2), zip char(10))
endif

if msgbox("Are you sure you want to save this?","Sure",36) = 6
   use rolo
   appe blank
   replace compname with McompName
   replace address  with Maddress
   replace city     with Mcity
   replace state    with Mstate
   replace zip      with Mzip
else
   return
endif

msgbox("Let's try it again.")

@ 10, 10 say "Enter Name:    " get McompName
@ 11, 10 say "Enter Address: " get Maddress
@ 12, 10 say "Enter City:    " get Mcity
@ 13, 10 say "Enter Zip:     " get Mzip
read

replace compname with McompName
replace address  with Maddress
replace city     with Mcity
replace state    with Mstate
replace zip      with Mzip

fform.McompName  = McompName
fform.Maddress   = Maddress
fform.Mcity      = Mcity
fform.Mzip       = Mzip

Terpret supports most dBASE flow structures.

It supports if/else/endif, do while/enddo, do case/otherwise/endcase, loop, exit, return and read.

“form” is supported, but you must use “fform
“this” is supported, but you must use “tthis

Macro substitution is also supported, but you have to code it differently:
dBASE does it like this:

Mvalue = &Mtemp.

Terpret does it like this:

Mvalue = :::Mtemp;;;

The following are among the flow commands not supported:

FOR/NEXT, ELSEIF, TRY/CATCH, DO/UNTIL, THROW, WITH/ENDWITH

Most of these can easily be replaced using flow commands that are supported by Terpret.

Perhaps the biggest limitation is that Terpret cannot create procedures.  For instance, this won't work:

Mtemp = getTemp()

Procedure getTemp
  ?"does something"
return

In other words, you cannot put multiple procedures in a terpret script.

You can call subprograms using the Terpret command.

** TEST3.SCP
Mtemp = Terpret("aGetTempFile.scp")

** AGETTEMPFILE.SCP
return("something")

Passing an array to Terpret

If you are uncomfortable with a text file sitting out there that could be edited by the user, you can pass the script to Terpret via an array.

Mj = new array()
Mj.add("Mid_no='ACME '")
Mj.add("Mid_name = 'Acme Produce Company'")
Mj.add("@10,10 say 'Enter I.D. #:' get Mid_no")
Mj.add("@10,10 say 'Enter I.D. #:' get Mid_no")
Mj.add("@11,10 say 'Enter Name  :' get Mid_name")
Mj.add("read")
Mj.add("form.Mid_no = Mid_no")
Mj.add("form.Mid_name = Mid_name")

Terpret(Mj)

Debugging a Terpret script

Debugging a Terpret script is a challenge because the error message does not refer to a line that dBASE has compiled and found to be OK.  Everything in a Terpret script happens via dBASE's powerful macro substitution.  This makes it difficult for a developer to know exactly where in the script the error happened.  Terpret implements a debug function that helps to pinpoint the error.  The forth parameter that is passed to Terpret tells it to open a debug window and then tell you which script line is causing the problem.

*** Test5.scp
Mcompname = space(20)
Maddress  = space(30)
Mcity     = space(30)
Mstate    = space(2)
Mzip      = space(10)

When you run test5.scp, it has an error.  This is because there is a missing parenthesis on the third line. If you issue the command:

Terpret("test5.scp","","",.t.)

A debug window will appear showing the commands involved.  The error box will reference the line with the problem.

Conclusion

Terpret and its support of @ say ... get is a splendid example of the awesome power of dBASE's “late binding” technology.  Unlike most languages, dBASE doesn't presume that all the logic in an application has to be known in advance.  In the case of Terpret, dBASE allows entire programs to be created after the compiler has long finished its job.  The author has created terpret scripts that are several thousand lines long and which add significant functionality to non-customized “.exe” files.  Since these customer's customized code reside in a script, they can receive many revisions of the “.exe” file without it affecting their customized application.  The new “.exe” can add standard functionality without destroying customized features.

To download the Terpret utility,  click here
(it's a 37Kb zipped executable file)


About the Author: George Burt is President of TrueShot Produce Systems, Inc. which provides a free accounting and information system specifically designed for the produce industry. TrueShot Accountant is designed to be completely customizable while remaining fully upgradable. TrueShot's web site is http://www.TrueShot.com