Exploiting the Power of the Visual dBASE 7 Language

This paper highlights many language features, some introduced in Visual dBASE 7 and some older ones, that should make your dBASE programming easier and more robust. It also contains some examples of "power programming" which demonstrate some moderately advanced programming concepts.


Basic changes from earlier versions

This section covers basic syntax changes in the dBASE language.

No more dots

Well, that's hardly accurate; your programs probably have more dots than ever in all those object references. But the dots around the logical (or boolean) operators are now optional, and the boolean constants true and false can now be spelled out.

Old way

New way

.and.

and

.or.

or

.not.

not

.t.

true

.f.

false

As always, the new keywords are not case-sensitive, and the old keywords may still be used.

Hexadecimal and octal numbers

You may now specify hexadecimal (base-16) and octal (base-8) numbers as numeric literals. Hexadecimal numbers are used primarily when using the Windows API or some other lower-level operation. You can now specify the number directly instead of having to use the HTOI( ) function to convert a string into a number. Hexadecimal numbers are preceded by "0x" or "0X".

Old way

New way

htoi( "FFFF" )

0xFFFF

Octal numbers are used for Compuserve account numbers, but not much else nowadays. Regardless, many languages like Java and C still support octal numbers. Any number that starts with a zero "0" that does not have a decimal point ".", "8", "9", or "x" (or "X") after it is considered to be an octal number. For example:

If you use

it's the number

0

0 (zero is always the same with all bases)

07

7

08

8

010

8

011

9 (1 * 8 + 1)

076

62 (7 * 8 + 6)

096

96

0.76

0.76

0x76

118 (7 * 16 + 6)

If you don't use octal numbers, you should be aware of this is if you were in the habit of preceding all your whole numbers with zero. Now, unless the first digit happens to be an 8 or 9, the number will be interpreted as an octal number, which is probably not what you want.

Functions and arguments

In DOS, there was a distinct difference between code that was declared as a FUCTION and code that was a PROCEDURE. You could not DO a FUNCTION; you had to call it with parentheses. A FUNCTION was required to return a value. In the Windows 5.x versions, most of that went away. The parentheses were formalized as call operators, so you call a FUNCTION or PROCEDURE, or DO either one. A FUNCTION did not have to return a value.

While version 5.x favored PROCEDURE (that is what was generated by the Form Designer), v7 favors FUNCTION. There is almost no difference. (One minor difference: you cannot use RETURN TO from a FUNCTION; but who does a RETURN TO anymore, especially with the new event-driven paradigm?)

Also in v7, you may now use empty parentheses in the FUNCTION line when the function takes no parameters. Before, if you had parentheses, they could not be empty. This change is especially helpful if you program in other languages that allow empty parentheses, and always place them in function definitions by habit.

A related feature is the previously undocumented ARGVECTOR( ) function, which lets you handle parameters to functions without having to declare them. It is usually used in conjuction with the ARGCOUNT( ) function (or its older equivalent, the PCOUNT( ) function). This is useful when there are either a lot of parameters (it's no fun typing a few hundred parameter names by hand), or you don't know how many parameters there will be. For example, suppose you are maintaining a list of words, and have a function that adds words to the list. You can make the function more flexible by allowing a single function call to add multiple words.

function add()  // No parameters declared, but that's OK
  local nArg
  for nArg = 1 to argcount()  // Get the count
    // Convert each one to uppercase and store in AssocArray
    this.wordList[ upper( argvector( nArg ) ) ] = null
  endfor

In this example, the wordList object is an AssocArray object. This lets you store any arbitrary string and use the isKey( ) method to see if it is in the list, without having to worry about SET EXACT OFF; which is a problem if you used a plain Array object and the scan( ) method. Note that nothing gets associated with the string, because the list is only being used to search for keys.

Assignment and comparison

The equals sign (=) serves double duty, as both an assignment and a comparison operator:

x = 5    // Assign value
? x = 7  // Compare values

This ambiguity can lead to simple, but subtle problems. For example, if you forgot the question mark at the beginning of that last statement, the statement is perfectly legal, but you would assign a value instead of doing a comparison.

dBASE 5 (both DOS and Windows) introduced the double-equal operator (==), which acts as a comparison only. Not only that, but when it compares strings, it always compares them as if SET EXACT is ON, which is the way people expect it to work. So when doing a comparison, unless you specifically want to take advantage of the "begins with" behavior of the single equals sign (and have SET EXACT OFF all the time), you should use the double-equal operator.

Visual dBASE 7 introduces the colon-equal (:=) as the assignment-only operator. Remember that with the plain single equals sign, if the variable or property that you are assigning to doesn't exist, it is created. Again, this can lead to some simple problems. For example, suppose you want to print a report starting at the second page, so you use:

r = new SomeReport()
r.beginPage = 2  // Start on second page
r.render()

But it doesn't work. Why? It turns out that the name of the property is startPage, not beginPage; there is no such property. But because you used the plain equal operator, dBASE obediently created one for you. It just doesn't do anything. If you had use the assignment-only operator:

r.beginPage := 2  // Start on second page

You would have gotten the runtime error, "Variable undefined". This would either jog your memory so that remember the correct property name, or force you to look up the property in the online help. The assignment-only operator is good not only for when you get the name completely wrong, but when you accidentally mistype the name of a property or variable. As long as you don't happen to mistype it as something else that actually does exist, you will get an error, which is better than having your code appear to run without problems, but either not work right immediately or, worse yet, not work right somewhere down the line after you have already deployed the application everywhere.

So to summarize:

Note that declaring a variable with LOCAL or PRIVATE does not create it, so you must use a single equals sign the first time you assign a value to it.

Field names in the OODML

In the OODML (Object-Oriented Data Manipulation Language, as opposed to the old Xbase DML), fields are accessed through the rowset's fields array, and all the field names are strings. For example:

? someQuery.rowset.fields[ "LastName" ].value   // Don't forget the .value, a common mistake

So if you need to generate some field names, in a loop for example, you don't have to use any macro tricks:

for n = 1 to 10
  ? someQuery.rowset.fields[ "Category" + n ].value
endfor

This code takes advantage of automatic type conversion, another new feature described later

Top


C-inspired language features

Over the years, dBASE has adopted many features common with other languages to become a full-fledged programming language. In particular, Visual dBASE 7 has a number of features popularized by the C programming language.

Comments

Visual dBASE 7 now supports both the C-style block comments inside /* and */, and the C++-style end-of-line comments with a double-slash (//). The double slash is easier to type, and doesn't look quite as odd as the double-ampersand (&&). Block comments are good for commenting out entire blocks of code without having to comment out each line—although the new program editor will do this for you, under Edit|Comment Line(s) and Edit|Uncomment Line(s). Block comments are also useful when you want to comment something in the middle of a statement. For example, you can comment the arguments for a function call:

someObj.someMethod( true /* a comment about true */, 23 /* why 23 */ )

Compound assignment operators

Compund assignment operators combine an operation, like multiplication, with an assignment. For example, instead of:

x := x * 2

you can use:

x *= 2

These compound operators become really useful when changing a deeply nested property like:

this.form.dataModRef1.ref.someQuery.rowset.fields["SomeField"].value *= 2

which is clearly better than having to type that property name twice (and get it right). In addition to multiplication, you can do division (/=), addition (+=), subtraction (-=), and modulo (%=).

Increment/decrement operators

The increment (++) and decrement (--) operators are like special cases of the compound assignment operators, in that you can add or subtract one from the value, which is a very common operation in computing. The value can be either a number or a date; if it's a number, it doesn't have to be an integer, although it usually is.

Unlike the compound assignment operator, which forms a statement, the increment/decrement operators are used in an expression. For example, you cannot say:

if x += 1 > 10

but you can use:

if x++ > 10

This gets the value of x, adds one to the variable, and then compares then compares the old value to 10. In other words, it increments after evaluating the variable, a post-increment operation. You can also place the increment/decrement operator before the variable or property:

if ++x > 10

This increments the variable first, then gets its value and compares it to 10. This is called a pre-increment operation.

Full language preprocessor

There was partial preprocessor support starting with the dBASE IV Compiler, but full C-style preprocessor support has been in all the Windows versions of dBASE. So this is not a new feature, but one worth pointing out.

Top


Automatic type conversion

One of the appeals of dBASE is that it has always been easier than other popular programming languages. For example, you do not have to declare a variable or its data type before you use it. To make things even easier, Visual dBASE 7 now features automatic type conversion in three ways: during concatenation, comparison, and assignment to the value of a Field object.

Concatenation

By now, you're familiar with using the + or - operator to "add" two strings together, a process referred to as concatenation. (Didn't know that you could use the - operator? It adds two strings together, combining all the trailing spaces at the end.) Before, when you wanted to add a number or date to a string, you had to use functions like STR( ) or DTOC( ). Now that's done for you automatically; to wit:

The capability is used to good effect in by the report engine, where text is defined in an expression codeblock. For example the Text object that displays the date might have the following text property:

{|| "Date: " + date()}

The date returned by the DATE( ) function is automatically converted into a string before it is concatenated.

Beware that the value null plus anything is null.

Comparison

It used to be that if you wanted to use a comparison operator, the two values had to be the same data type. Now, if necessary the operands may be converted, in accordance with the following rules:

All comparisons between a number and an invalid number result in false.

This means that you should never get a data type mismatch error when comparing two values. If the types are not compatible, you simply get false.

This is a convenience feature; most of the time when you compare two values, they are the same data type. But, for example, if you are reading data from a text file, you don't necessarily have to do a conversion to perform a comparison. Note that if you are comparing numbers, the numeric string will treated as a genuine number, but if you are comparing dates, the date string will be compared as a string, so if the SET DATE format is MDY or DMY, the dates will not sort correctly. So when checking if a date in a string comes before or after another date, you should use CTOD( ) to convert the string to a genuine date.

Assigning to the value of a Field object

You can assign strings to the value property of a numeric, date, or boolean field object. The values will be converted automatically using VAL( ), CTOD( ), and for booleans, whether the first character is the letter "T" or "t". For example:

form.rowset.fields[ "SomeNumber"  ].value := "3.4"
form.rowset.fields[ "SomeDate"    ].value := "08/12/98"
form.rowset.fields[ "SomeBoolean" ].value := "true"

(Of course, in real code you would be assigning other properties or variables, not literal strings.) Again, this is simply a convenience feature.

Type still matters

Even with automatic type conversion, data types still matter in many places. In particular, the data types for function arguments must be correct. For example, the SQRT( ) function returns the square root of a number. You cannot pass a string containing a number; dBASE does not perform automatic type conversion here, and you would get a "Data type mismatch. Expecting: Numeric" error.

Top


Exception handling

An exception is an interruption in the normal flow of your code. Errors are the main kind of exception that you will encounter, but there are others, as you will see later. The idea behind exception handling is to provide a structured, localized, and flexible way of dealing with exceptions; not just things that you hope never happen (like errors), but also to deal with situations that are likely to happen, and even to use exceptions to your advantage to control the execution of your code.

For example, suppose you want to open a table exclusively. The usual technique is to actually try it, and then determine if it worked. This is a bit more complicated than it used to be.

To digress for a moment: before Visual dBASE 7, a USE <table> EXCLUSIVE would fail and generate an error if the table was already open elsewhere. But now the USE will work, but the table will be opened normally in shared mode, not exclusively, without any error. In most cases you can work around this—which really is intended as an interactive convenience feature—by then attempting to delete a non-existent index tag. This action requires exclusive use, and you can then check the error: is it the "Tag not found" error, which indicates the table is opened exclusively; or the "Operation requires exclusive use of table" error, which indicates that the exclusive USE failed? (A third possibility is that the table is not indexed at all, in which case you will always get a "Table is not indexed" error, regardless of whether the USE EXCLUSIVE worked. This example covers the vast majority of cases where the table has at least one index.) So, even with an older version of dBASE, the idea was to try something that might cause an error; but because you didn't really want the error to handled like an unexpected and usually fatal error, you need to temporary change the way errors are handled.

Now back to the real topic: without structured exception handling, you would need to save your global ON ERROR handler, set a temporary local ON ERROR, attempt to delete the non-existent tag, and then restore the ON ERROR handler. This old-style code would look something like this:

#define ERR_TAG_NOT_FOUND      53
#define ERR_REQUIRES_EXCL_USE 110
PROCEDURE UseExcl( cTable )
  private cOnError, nErrCode
  cOnError = set( "ON ERROR" )  && Get the global ON ERROR handler
  nErrCode = 0                  && No error to start with
  use (cTable) exclusive
  on error nErrCode = error()   && Assign the error code to the variable to be tested
  delete tag X__Y__Z__          && Attempt to delete non-existent tag
  on error &cOnError            && Restore global ON ERROR
  return ( nErrCode == ERR_TAG_NOT_FOUND )

If the error is the expected "Tag not found" error, that means the appropriate table was found and opened for exclusive use; the function would return true. Anything else would return false, indicating something is wrong. But notice how cluttered the code is with the saving, setting, and resetting of the ON ERROR handler. The code is much simpler with structured exception handling:

function UseExcl( cTable )
  use (cTable) exclusive
  TRY
    delete tag X__Y__Z__        // Attempt to delete non-existent tag
  CATCH ( Exception e )
    return ( e.code == ERR_TAG_NOT_FOUND )
  ENDTRY

The keywords TRY, CATCH, and ENDTRY are the building blocks of the exception handling structure. The TRY and ENDTRY mark the beginning and end, just like IF and ENDIF mark the beginning and end of a conditional structure. After the TRY statement is any code that may, or definitely will, cause an exception. A CATCH marks the end of that code. If an exception occurs, execution jumps to that CATCH. In the parlance of exception handling, dBASE will "try" the code. Any code that fails, "throws" an exception. The exception is "caught" by the CATCH.

Catching exceptions

In keeping with Visual dBASE's object-oriented architecture, an exception is represented by an object. The simplest kind of exception is an instance of the class Exception. There is also a stock DbException class, which is a subclass of the Exception class specifically for data access errors. You can also subclass the Exception class to create your own exception classes, which you will see later.

Each CATCH statement declares a class name and a variable name inside parentheses. The class name is not case-sensitive. You may use any variable name you want; the variable is automatically considered to be local to the function. (If you use the name of a local variable that already exists in the function, the variable will be overwritten if that CATCH is executed.) When an exception occurs, the class in each CATCH statement is checked against the class of the exception that occurred.

The declared class name matches the exception object by either being the exact same class as the object, or by being a superclass of the exception. Because the class Exception is the base class for all exception classes, it will match all exception objects. Most exception handling structures are designed to handle plain dBASE errors, which are represented by Exception objects. Therefore, there is often only one CATCH, with the class Exception.

If the declared class matches the class of the exception, then the exception object is assigned to the declared variable name, and the statements in that CATCH block are executed.

Handling exceptions

Once inside the CATCH block, you can do whatever you want, including nothing (if you have no executable statements between the CATCH and the ENDTRY). In that case, the exception is simply ignored, as if it did not happen. In any case, after the CATCH block is complete, execution continues with the statement after the ENDTRY.

A CATCH block is not a subroutine. You cannot go back to the line that caused the error, or the line after the error, as you can with RETRY and RETURN with ON ERROR handling. Execution jumps to the CATCH block, and continues from there.

When taking an action in a CATCH block, it often involves some aspects of the exception, which are reflected in its properties. The two main properties of interest in an Exception object are the code and message properties. These correspond to the error code and message that you would get with the ERROR( ) and MESSAGE( ) functions in the old-style error handling system. There are also lineNo and filename properties, which contain the line number and name of the file where the exception occurred. All four properties are set by Visual dBASE when an error occurs. If you create your own exception objects, these properties are zero and blank by default.

In the example, the exception's code property is examined to see which error occured. Note that the way the example is structured, it assumes that the DELETE TAG line will always cause an error; otherwise, the function simply ends, and does not RETURN a value. Also, the USE statement is before the TRY. If you specify a bad file name for example, an error will occur outside the exception handling structure, which would cause the standard error dialog to appear.

Nesting TRY blocks

What happens if there is an error while executing the CATCH? That error generates another exception, but it does not execute the same CATCH recursively. No, the code in the CATCH is outside the code that is being "tried", so the current CATCH does not apply.

What happens if there is no CATCH statement that declares a matching exception class? (Or what if there's no CATCH at all, which you will see later?) In this case, what you end up with an "uncaught" exception.

There are two mechanisms for handling these problems. The primary one is nesting TRY blocks. Whenever an exception occurs, it goes "up" the hierarchy of nested TRYs to find a suitable CATCH. For example, you can nest TRYs in the same routine:

try
  try
    // Some code that might throw an exception
  catch ( Exception e )
    // Catch exceptions here
    // If there are exceptions in this CATCH
  endtry
catch ( Exception e )
  // they get caught here
endtry

But if you're concerned about an exception occurring in your CATCH, you could handle the exception inside its own TRY block:

try
  // Some code that might throw an exception
catch ( Exception e )
  try
    // Handle the exception here
    // If this code fails, then
  catch ( Exception e )
    // It gets caught here and goes no further
    // (But if you get an error here, you will have an uncaught exception)
  endtry
endtry

TRY blocks also nest into separate routines, because the subroutine you are executing is being "tried". Examine how exceptions would be handled in the following code:

try
  someFunction()  // Tries calling another routine
catch ( Exception e )
  // Any uncaught exceptions in someFunction() come here
endtry

function someFunction
  // An exception here will go the CATCH in the caller
  try
    // If an exception occurs here
  catch ( Exception e )
    // it gets caught here.
    // But if an exception occurs during this CATCH,
    // it causes its own exception, which will go back
    // to the CATCH in the caller
  endtry
  // Outside the TRY again, an exception here will go to
  // the CATCH in the caller
  nestedFunction()  // Call another nested routine
  return

function nestedFunction
  // An exception here will go all the way back to the first CATCH
  return

ON ERROR and exceptions

So far, you've seen exception handling as a replacement for the global ON ERROR mechanism. But ON ERROR can still serve a purpose.

If an exception occurs that is not caught by a CATCH, it eventually causes a system error: "No CATCH for exception," with the class name of the exception, and the exception object's message property. Try it:

try
  ? xyz   // No such variable
catch ( DbException e )
  // Exception class does not match
  // Exception is not caught
endtry

Running this code in a program produces the standard error dialog:

You can use ON ERROR as a "global" catch, a final catch-all for exceptions that are not properly handled. As with most errors handled by a global ON ERROR handler, this is probably a fatal error in your application, something you want to handle more gracefully than with the standard error dialog. The following stripped-down example is just slightly less abrupt:

// In your application startup, assign ON ERROR handler
on error do GlobalErrorHandler with program(), line()

// Code that fails
try
  ? xyz   // No such variable
catch ( DbException e )
  // Exception class does not match
  // Exception is not caught
endtry

// Handler in procedure or LIBRARY file
#define ERR_NO_CATCH    22
function GlobalErrorHandler
  if error() == ERR_NO_CATCH
    // Log error here
    msgbox( "A serious problem has occurred", "Bye!", 16 )
    quit
  endif

(Warning: in v7.01, dBASE may crash when the ON ERROR handler is finished handling the "No CATCH for exception" error, which makes the explicit QUIT all the more necessary.)

Throwing exceptions

Nested TRY blocks let you localize and focus your exception handling as needed. But what if you want to handle certain exceptions differently? Remember that an exception is considered a failure in the code that is tried, and there is no way to go back. Suppose you want to ignore or handle some expected exceptions, but cancel the process and generate an error message for the rest. You can do this with the THROW command.

THROW will generate an exception at that statement. Most often you will THROW exceptions that you have caught with CATCH. For example:

try
  // A multi-step process
  try
    // Step 1
  catch ( Exception e )
    if e.code # ERR_THATS_OK     // Is it the error you want to ignore?
      throw e                    // If not, re-throw exception up a level
    endif                        // Otherwise, exception is ignored and 
  endtry                         // execution continues
  try
    // Step 2
  catch ( Exception e )
    if e.code == ERR_YOU_EXPECT  // Is it an error you can handle?
      // Handle error (and then execution continues)
    else
      throw e                    // If not, re-throw exception up a level
    endif
  endtry
  // etc
catch ( Exception e )            // THROWn execeptions caught here
  msgbox( e.message, "Process failed", 16 )
endtry

Note that if you THROW an exception in the CATCH for Step 1, it does not get caught by the CATCH in Step 2, because that CATCH is for the code inside Step 2's TRY. By re-throwing all unhandled exceptions, they can be caught and handled by the same CATCH at a higher level, streamlining your code.

Simulating exceptions

You can also use THROW to simulate exceptions to test your exception handling code. For example:

fakeException = new Exception()
fakeException.code := 5000    // Code for whatever error you want to test
fakeException.message := "Test error"

try
  // Normal code 
  throw fakeException     // Statement inserted to simulate failure
  // More normal code
catch ( Exception e )
  if e.code == 5000
    // Do whatever
  endif
endtry

Using FINALLY

Many types of operations require some kind of cleanup, even if the operation fails. For example, if you have a process that creates temp files, you always want to delete those files, even if an error prevents the process from completing. Visual dBASE 7 provides a very easy and powerful way of implementing this type of behavior: with FINALLY.

FINALLY is another keyword like CATCH. It must be inside a TRY...ENDTRY block. Every TRY...ENDTRY must have at least one CATCH, or a FINALLY; or it can have both. Although a TRY can have more than one CATCH, it can only have one FINALLY. When a TRY has both a CATCH and a FINALLY, the FINALLY is usually placed at the end.

The code in the FINALLY block is always executed, whether the code in the TRY completes successfully or not. For example:

try
  fTemp = new File()
  fTemp.create( "TEMP.$$$" )
  // Do some processing, which might fail
  msgbox( "Finished at " + time(), "Process complete", 64 )
catch ( Exception e )
  msgbox( e.message, "Process failed", 16 )
finally
  fTemp.close()
  erase TEMP.$$$
endtry

What can happen with this code?

    1. The "Process complete" message is displayed. The code in the TRY block is done.
    2. The CATCH block is skipped, because no exeception occurred.
    3. The FINALLY block is executed.
    1. Execution jumps to the CATCH. The declared class matches the exception object, so
    2. The "Process failed" message is displayed. The CATCH is done, so
    3. The FINALLY block is executed.

Now, although the FINALLY block clearly indicates to anyone reading the code that the file cleanup is an important final step in the process, in this case, the code would have worked just as well if you had put the file close and delete after the ENDTRY, without a FINALLY at all. If there was an exception, it would be handled by the CATCH, and execution would continue after the ENDTRY regardless. But suppose you want to return a value to indicate whether the process completed successfully. With FINALLY, you can do this:

try
  fTemp = new File()
  fTemp.create( "TEMP.$$$" )
  // Do some processing, which might fail
  msgbox( "Finished at " + time(), "Process complete", 64 )
catch ( Exception e )
  msgbox( e.message, "Process failed", 16 )
  return false     // Didn't work
finally
  fTemp.close()
  erase TEMP.$$$
endtry
return true        // All done, finished successfully

Here, the CATCH has a RETURN statement, which would normally cause execution to return back to the caller. But a FINALLY guarantees that the code will be executed. So before this code returns, the FINALLY is executed, closing and deleting the file. In fact, you can even do this:

try
  fTemp = new File()
  fTemp.create( "TEMP.$$$" )
  // Do some processing, which might fail
  msgbox( "Finished at " + time(), "Process complete", 64 )
  return true      // All done, finished successfully
catch ( Exception e )
  msgbox( e.message, "Process failed", 16 )
  return false     // Didn't work
finally
  fTemp.close()
  erase TEMP.$$$
endtry

What happens is:

    1. The "Process complete" message is displayed.
    2. The function attempts to RETURN the value true to its caller.
    3. But because the RETURN is inside a TRY that has a FINALLY, the FINALLY block is executed.
    4. Inside the FINALLY block, the file is closed and deleted.
    5. The function returns true.
    1. Execution jumps to the CATCH. The declared class matches the exception object, so
    2. The "Process failed" message is displayed.
    3. The function attempts to RETURN the value false to its caller.
    4. But because the RETURN is inside a CATCH for a TRY that has a FINALLY, the FINALLY block is executed.
    5. Inside the FINALLY block, the file is closed and deleted.
    6. The function returns false.

With FINALLY, you avoid having to create a temporary variable to store the return value while you do the cleanup. The code is easier to write and easier to read, and clearly indicates the intent of the program flow.

If there is a RETURN statement inside the FINALLY block, then it takes precedence. If it returns a value, that value supercedes the value (if any) returned by the original RETURN that caused the FINALLY to be executed. The RETURN is acted upon immediately, skipping any other statements in that FINALLY block. Execution will return to the caller, unless of course that TRY structure is nested inside another TRY with its own FINALLY.

Using FINALLY with loops

FINALLY also works for code deviations inside loops; namely the EXIT and LOOP commands. For example, suppose you have a deeply nested set of IF...ENDIF structures inside a loop. The loop uses an associative array, so it must increment the key value using the nextKey( ) method. If you want to get out of a deeply nested IF to skip that particular element, you can put the LOOP command inside a TRY, and in the FINALLY, increment the key value:

cKey = aAssoc.firstKey
do while not cKey == false
  try
    if someCondition
      // Some processing
      if someOtherCondition
        // More stuff
        if etc
          // And so on
                            // At some point, you decide to skip the rest
                            loop  // Go to next element
        endif
        // If you want to skip the rest, you want to bypass this
      endif
      // and this
    endif
  finally
    // but always execute this, difficult to do without TRY/FINALLY
    cKey := aAssoc.nextKey( cKey )
  endtry
enddo

Using FINALLY with no CATCH

In addition to being an example of FINALLY with the LOOP command, that was also an example of a TRY that has a FINALLY, but no CATCH. If there is no CATCH, then the FINALLY marks the end of the code that is "tried".

Another example of when you might have this is a variation on the file processing code above. Suppose it was part of a larger operation, using nested TRY structures to manage any errors:

// Main operation
try
  processFile( someFile )  // Call file processing
  // Do something else that might cause an exception
catch ( Exception e )  // All errors come here
  msgbox( e.message, "Operation failed", 16 )
endtry

function processFile( cFilename )
  try
    fTemp = new File()
    fTemp.create( "TEMP.$$$" )
    // Do some processing, which might fail
  finally
    fTemp.close()
    erase TEMP.$$$
  endtry

In this example, if the file processing fails, then the cleanup code in the FINALLY block executes. But because the exception was not caught, the exception "bubbles up" to the previous level, and gets caught by the CATCH in the main operation. The advantage with this kind of structure is that all the exception handling, in this case the display of a message, is handled in a single location.

Creating custom exception classes

You can create your own exception classes to take advantage of the execution flow control provided by exceptions, while differentiating your exceptions from genuine errors. Extending the file processing example, suppose you're translating a file from one format to another. There are many different places where the translation could fail. The file could be missing, it might not appear to be in the correct format, it may be in an older unsupported format, it might have invalid characters, it could fail internal integrity checks, and so on. If any of these conditions occur, all you can do is give up. Exceptions give you an easy and direct way to jump back all the way to a known point in your program.

The first task is to create your own exception class. One convenience you can include is to provide an easy way to assign information to the exception object when you create it, like setting its message property:

class FileProcException( cMsg ) of Exception

  this.message := cMsg

endclass

When creating your own subclasses, you can of course add your own properties. Suppose for an invalid character, you want to store that character as a separate property. You create an even more specific subclass:

class InvalidFileCharException( cChar ) of ;
      FileProcException( "Invalid character in file" )

  this.invalidChar = cChar   // New property

endclass

(Note that in defining this subclass, the parameter cChar is not passed to the subclass; instead, a constant string is passed. While this might seem strange, it is appropriate and perfectly legal. All InvalidFileCharException objects will have the same message.)

Now you have to create separate CATCH blocks for each exception class that you want to handle differently. If you have more than one CATCH statement inside a single TRY...ENDTRY, you must declare the classes from most specific to least specific, because superclasses are considered a match. Suppose you only want to differentiate between your custom exceptions and standard errors:

for n = 1 to fileList.size   // Process a list of files
  cFilename = fileList[ n ]  // Get the filename from the list
  try
    processFile( cFilename )
  catch ( FileProcException e )
    msgbox( e.message, "Failed to process: " + cFileName, 48 )
    // Display message and continue loop for next file
  catch ( Exception e )
    msgbox( e.message, "Error on line " + e.line, 16 )
    // Stop on system errors
    return
  endtry
endfor

In the file processing code, you would THROW the exception like this:

// Somewhere in some function, the version number has been 
// extracted from the file into the variable nVersion
if nVersion < 2
  throw new FileProcException( "Obsolete file version not supported" )
endif

As shown above, the processFile( ) function should do its work in a TRY with a FINALLY but no CATCH, so that if an exception occurs, the proper cleanup can be done, and the CATCH can be handled at a higher level, inside the main loop in this case.

For the more specific invalid character exception, the code might look like:

// Somewhere in some function, a character has been extracted into the
// variable cType that indicates the kind of information that follows.
if cType == INDICATOR_CHAR  // Some #DEFINEd character constant
  // Do whatever
else
  // But if that character is not recognized...
  throw new InvalidFileCharException( cType )
endif

As the main loop stands now, the InvalidFileCharException will be caught in the first CATCH as a FileProcException, which it is. But suppose you want to handle the specific exception differently, like counting how many of each bad character you encounter:

aBadChar = new AssocArray()  // For invalid characters encountered
for n = 1 to fileList.size   // Process a list of files
  cFilename = fileList[ n ]  // Get the filename from the list
  try
    processFile( cFilename )
  catch ( FileProcException e )
    if aBadChar.isKey( e.invalidChar )  // If the bad character has been found before
      aBadChar[ e.invalidChar ]++       // increment the count
    else
      aBadChar[ e.invalidChar ] = 1     // Otherwise, create a new array element
    endif
    // Same code as base class exception
    msgbox( e.message, "Failed to process: " + cFileName, 48 )
  catch ( FileProcException e )
    msgbox( e.message, "Failed to process: " + cFileName, 48 )
    // Display message and continue loop for next file
  catch ( Exception e )
    msgbox( e.message, "Error on line " + e.line, 16 )
    // Stop on system errors
    return
  endtry
endfor

Because the CATCH is for a specific class of exception, you can confidently access custom properties for that exception object. Also note that to duplicate the actions for the base class, the statements must be repeated explicitly. Like in a CASE structure, only one CATCH is executed.

Exceptions in event-driven programming

All this exception handling power has one major caveat: a TRY covers a single "thread" of execution. (The term "thread" is used loosely here because dBASE does not support true multi-threading as some languages do.) For example, if you do this:

try
  do SomeForm.wfm  // Open a form
catch ( Exception e )
  // Handle any exceptions
endtry

Any exceptions that occur while instantiating and opening the form will be caught. But once the form is open, the DO statement is done, which means the TRY block is done, and execution continues with the statement after the ENDTRY. This particular exception handling structure no longer applies. Any exceptions that occur while clicking a button on the form, or adding a row of data, or anything else on that form will not be caught here, because those actions occur in their own separate event.

Currently, there is no general solution for this problem. Ideally, there should be some way to setup exception handling in some form for a given event or set of events. But even without this capability, exception handling is a powerful and flexible way to manage the execution of your application.

Top


Power programming example 1: a multicast timer

When you have several processes that rely on a timer, there are basically two ways to go: each process can have its own timer, or you can create a single multicast timer that notifies each object on its list. The multicast timer has a few advantages. First, the events for all the processes are synchronized. Second, it's easier to manage a single timer. If you've ever had timer code go bad during development, you may have experienced how hard it can be sometimes to turn them off!

The multicast timer object

This is the source code for MulticastTimer.prg:

set procedure to program(1) additive

_app.timer = new MulticastTimer()

class MulticastTimer of Timer

  this.interval := 1

  this.listener = new Array()

  function onTimer()
    this.broadcast( {|obj|; obj.onMulticastTimer()} )

  function start()
    this.enabled := true
    this.broadcast( {|obj|; obj.resumeTimer()} )

  function suspend()
    this.enabled := false
    this.broadcast( {|obj|; obj.suspendTimer()} )

  function stop()
    this.enabled := false

  function broadcast( kMessage )
    local n, purgeList
    purgeList = new Array()
    for n = 1 to this.listener.size
      try
        if this.listener[ n ].isListeningToMulticastTimer()
          kMessage( this.listener[ n ] )
        endif
      catch ( Exception e )
        ? e.message
        purgeList.add( n )
      endtry
    endfor
    for n = purgeList.size to 1 step - 1
      this.listener.delete( purgeList[ n ] )
      this.listener.size--
    endfor

  function addListener( obj )
    if this.listener.scan( obj ) == 0
      this.listener.add( obj )
    endif
    return true

endclass

The MulticastTimer class subclasses the built-in Timer class. It sets the built-in interval property, and the event handler for the onTimer event is set implicitly by creating a function with the name of the event. All other properties and methods are new, created in this subclass.

When you run this program, it creates an instance of the MulticastTimer object as _app.timer. Objects can then "subscribe" to the timer service by calling _app.timer.addListener( ) with the object as the parameter. The code checks if the object is already on the listener list, and if not, adds it.

After every second of idle time, the timer goes off, and onTimer( ) calls the broadcast( ) method with a "message" to notify all the subscribing objects. Before discussing the content of the message, take a look at how the broadcast works.

Timer broadcast

The broadcast( ) method creates a purgeList array to keep track of any objects that will be removed from the listener list if it runs into any problems during the broadcast. It uses a FOR loop to go through the list and, in a TRY block, call each object's isListeningToMulticastTimer( ) method. There are two likely problems here:

In any case, the error is caught by the CATCH block, which adds the array element number of the object to the purgeList array. For demonstration and testing purposes, it also displays the error message.

If the object's isListeningToMulticastTimer( ) method returns true, then the message code is executed against the object.

After attempting to notify all the subscriber objects, another loop goes through the purgeList array, which contains the objects that failed notification. The loop goes through and deletes the listener array elements in reverse order, because deleting an item shifts all the remaining items forward, which would throw any element numbers that follow in the purgeList array out of sync.

So what exactly is in the message code?

Message code

The key statement in the broadcast( ) method is:

kMessage( this.listener[ n ] )

kMessage is the parameter to the method. (In dBASE-flavored Hungarian notation, "k" stands for code—"c" was already taken, and denotes character.) Because the parameter is called with the parentheses call operators, kMessage must be either a codeblock or a function pointer. The code is called with the listening object as the one and only parameter.

The onTimer event handler calls the broadcast( ) method with:

this.broadcast( {|obj|; obj.onMulticastTimer()} )

This means that the kMessage codeblock takes the first parameter as obj, and then assuming that the parameter is an object, calls the object's onMulticastTimer( ) method.

Using the multicast timer

An object that subscribes to the MulticastTimer must therefore implement the following four methods, as either functions or codeblocks:

A clock

The following object, in the file Time.cc, implements those four methods to display the current time:

class ClockText(f) of Text(f) custom

   with (this)
      height = 1.5
      width = 20
      colorNormal = "Highlight/BtnFace"
      fontSize = 18
      fontBold = true
      text = "" + ttime()
   endwith

  function onOpen()
    _app.timer.addListener( (this) )

  function isListeningToMulticastTimer()
    return this.hWnd # 0

  function onMulticastTimer()
    this.showTime()

  function showTime()
    this.text := "" + ttime()

  function suspendTimer()
    // Do nothing
    return

  function resumeTimer()
    // Do nothing
    return

endclass

The control subclasses the Text control, setting fairly large text (partly for demonstration purposes here). It initially sets its text property with the undocumented TTIME( ) function. This returns the current time in the undocumented Time data type. Visual dBASE displays the time using the time format specified in the Regional Settings of the Control Panel, which it reads at startup. By concatenating the Time value to an empty string, it is converted into its display representation, using automatic type conversion. If you assigned the TTIME( ) directly to the text property of a Text control, it displays the time using the standard TIME( ) format, which is a 24-hour clock.

In the onOpen event, the object calls the addListener( ) method of the multicast timer, which it assumes already exists as _app.timer, with itself as the parameter. Note that when passing this, it is enclosed in parentheses so that it is passed by value. Without the parentheses, it would be passed by reference, which means that the called method with consider itself—the timer—as the first parameter.

The isListeningToMulticastTimer( ) method checks the hWnd property of the object. The hWnd is zero if the form has not been opened yet, or has been closed. In that case, the method returns false; otherwise it returns true.

The onMulticastTimer event handler calls the showTime( ) method, which simply updates the text with the current time. Both the suspendTimer( ) and resumeTimer( ) methods do nothing; they are simply there to fully implement the multicast timer interface, because they will be called whenever the multicast timer is suspended or resumed.

A form with a clock

The following form, in FormWithClock.wfm, displays this ClockText control:

class FormWithClockForm of Form
   set procedure to time.cc additive
   with (this)
      height = 4
      width = 40
      text = "Clock form"
   endwith


   this.CLOCKTEXT1 = new CLOCKTEXT(this)
   with (this.CLOCKTEXT1)
      height = 1.5
      left = 3
      top = 1
      width = 20
   endwith

endclass

To run this form:

do MulticastTimer      // Instantiate the _app.timer object
do FormWithClock.wfm   // Open a form
do FormWithClock.wfm   // or two (reposition so you can see them both)
_app.timer.start()     // Activate timer

When you run these forms, you will see that the clocks are synchronized. When you close one of the forms while the timer is running, you will see the following error message displayed in the Command window:

Error:  Attempt to access released object.

The form was closed and released, thereby destroying the text control contained in it. When the timer attempted to contact the control it was gone. This expected exception was caught by the CATCH block in the broadcast( ) method, causing it to be removed from the listener list. The timer will no longer attempt to contact the object. When you close the second form, the same thing happens, leaving the listener list empty.

You could code the broadcast( ) method to stop( ) the timer if the list is empty, by adding the following code to the end:

    if this.listener.size == 0
      this.stop()
    endif

The drawback is that you would have to restart the timer a lot during development. Without that code, to stop the timer:

_app.timer.stop()

Displaying elapsed time

You can use the MulticastTimer to display elapsed times. It is important to note that the timer fires after every second of idle time. The computer is busy occasionally, so it does not fire exactly every second, and eventually it will "skip" a second. Therefore, when displaying elapsed time, you use the timer as a reminder to check the time only; you must keep more accurate track of the time separately.

Tracking elapsed time

The following base form class, in TimerForm.cfm, keeps track of elapsed time for the form.

class TimerCForm of Form custom

  protect baseTime, elapsedTimeSet
  this.baseTime       = 0       // These times in
  this.elapsedTimeSet = 0       // milliseconds

  if type( "_app.timer" ) == "O" and _app.timer.enabled
    this.resetBaseTime()
  endif

  function getElapsedTime()
    if _app.timer.enabled
      return floor( ( new Date().getTime() - this.baseTime ) / 1000 )
    else
      return floor( this.elapsedTimeSet / 1000 )
    endif

  function resetBaseTime()
    this.baseTime := new Date().getTime() - this.elapsedTimeSet

  function saveElapsedTime()
    this.elapsedTimeSet := new Date().getTime() - this.baseTime

  function suspendTimer()
    this.saveElapsedTime()

  function resumeTimer()
    this.resetBaseTime()

endclass

By keeping the elapsed time at the form level, all controls in the form have easy access to that time. To keep track of elapsed time, the form stores a baseTime; when necessary, it can be subtracted from the current time to get the elapsed time. The times are stored in milliseconds, which are returned by the Date object's getTime( ) method. (Note that the timer resolution is actually only about 55 milliseconds, or one clock tick. Each tick, the time alternates between 50 and 60 milliseconds more than the last time, e.g. 50, 110, 160, 220, etc. You could get more resolution through the Windows API or some other method.) Using a Date object avoids any problems with the clock rolling over at midnight.

When the form is created, it checks if _app.timer exists and is enabled. This check relies on the short-circuit evaluation rules for a boolean expression. If the TYPE( ) test fails, the object does not exist, so there is no need to check if it's enabled. In fact, attempting such a test would cause a "variable undefined" error. But because these two tests are joined by an and, if the first test fails, the second one is not even attempted, because false and anything is false. If the timer is enabled, the form calls resetBaseTime( ).

The resetBaseTime( ) method sets the baseTime property so that the elapsed time starting at that moment equals the desired elapsed time stored in the elapsedTimeSet property. This defaults to zero, so by default the form starts the elapsed time at zero. The saveElapsedTime( ) method does the reverse, storing the current elapsed time in the elapsedTimeSet property so that it can be restored later. saveElapsedTime( ) is called from the suspendTimer( ) method, which is intended to be called when the timer is suspended. Conversely, resumeTimer( ) calls resetBaseTime( ), for when the timer starts up or resumes.

However, the form itself is not designed to subscribe to the MulticastTimer object; for one thing, it lacks the isListeningToMulticastTimer( ) and onMulticastTimer( ) methods. Instead, an elapsed time control is the subscriber, and calls upon the TimerCForm to keep track of the time.

class ElapsedTimeText(f) of ClockText(f) custom

  with ( this )
    text := "00:00:00"
  endwith

  function showTime()
    this.text := convertIntToTime( form.getElapsedTime() )

  function suspendTimer()
    form.suspendTimer()

  function resumeTimer()
    form.resumeTimer()

endclass

This ElapsedTimeText control subclasses the ClockText control described earlier. That control already implemented all the methods to interface with the MulticastTimer, so this class simply overrides some methods to change the behavior (which of course is what inheritance in object-oriented programming is all about).

The showTime( ) method still updates the control's text property, but here it calls the form's getElapsedTime( ) method, and uses the convertIntToTime( ) function (a generic function that is not part of any class) to convert seconds into the standard time format. The code for convertIntToTime( ) is fairly straightforward:

function convertIntToTime( nInt )
  local hours, minutes, seconds
  hours   = int( nInt / 3600 )
  minutes = int( nInt % 3600 / 60 )
  seconds = int( nInt % 60 )
  return str( hours  , 2, 0, "0" ) + ":" + ;
         str( minutes, 2, 0, "0" ) + ":" + ;
         str( seconds, 2, 0, "0" )

The STR( ) function is used in lieu of automatic type conversion, so that it can add a leading zero if necessary.

The suspendTimer( ) and resumeTimer( ) methods call the same-named methods in the form, which save and restore the elapsed time. In the ClockForm class, these methods did nothing.

The following form, in ElapsedTimer.wfm, displays the ElapsedTimeText control:

class ElapsedTimerForm of TimerCForm from "TIMERFORM.CFM"
   set procedure to time.cc additive
   with (this)
      scaleFontBold = false
      height = 4
      width = 40
      text = "Elapsed time"
   endwith


   this.ELAPSEDTIMETEXT1 = new ELAPSEDTIMETEXT(this)
   with (this.ELAPSEDTIMETEXT1)
      height = 1.5
      left = 4
      top = 1
      width = 14
   endwith

endclass

If you start the multicast timer and run a few of these forms, you will see that each form keeps track of its own elapsed time.

Top


Power programming example 2: dynamic report layout

Now that Visual dBASE 7 has its own native report classes, you can direct the power of the language toward report generation. One useful feature is the ability for users to choose the columns that they want to print, in the order they want them.

As the developer, you are still responsible for formatting these columns; only you know the field names and how the data is stored. For example, an address can have many components, like the number, street name, city, state, and postal code; a person's full name may include a middle initial, which could be blank or null; phone numbers may be stored as digits only, so the desired formatting symbols must be inserted when printing.

The technique shown here relies on you to create a "template" report that contains the basic layout of the report (like the stream frame, the title, an a page number) but no data columns. You also compose, format, and size the columns' data and titles as you want them to appear, in a form. Then the code reads the columns you have created, and presents them to the user in a dialog box. The user choses their columns, and then the code creates those columns in the template report, scales them if necessary to fit, and renders the report.

Composing the data columns

This technique relies on the fact that the same Text objects are used to display text in a report and in a form. In a form, the text property of a Text object usually contains plain old text—a character string. Text objects in a report also use character strings, in the titles and column headings. But the column data itself, the data from the table, is displayed using a Text object that has a codeblock in the text property. This codeblock is evaluated whenever the Text object is rendered. It's usually a simple expression codeblock that references the value property of a Field object, for example:

{|| this.form.contacts1.rowset.fields["HomePhone"].value}

This codeblock uses the form reference to point to the root object, which is the report, and from there, the contacts1 query, which contains the rowset. So every time the Text object is rendered, which is for each row in the stream source's rowset, the value of the field is displayed. In essence, all the data in a report is displayed with a calculated field; it's just that most of the calculations are very simple.

The exact same codeblock works for a Text object in a form. If you change the data type of the text property to Codeblock, using the type drop-down (the button with an inverse "T") in the Inspector, and assign that codeblock, the field value from the first row in the rowset will be displayed. Unlike a report, the form evaluates that codeblock only once, because it only has to render the Text object once.

Creating the layout form

Why do the layout in a form? Two main reasons:

The layout form subclasses the ColumnsCForm custom form class in Columns.cfm. That base form class changes the form's metric to inches, so that it matches the report, and defines a few methods to read the layout. The following figure shows the two pages of ContactsColumns.wfm, the column layout form for the Contacts report, in design mode:

The data column is directly under its title, although that is not strictly necessary. The name of each control is set so that the title/data pair matches. For example, the title for the Home phone/fax is called "TITLEHOMEPHONE", and the field data is named "DATAHOMEPHONE" (a control's name property is always converted into uppercase). Also, the Text objects have their border property set to Single, so that you can see the bounds of the object. This border is not rendered in the report. Each title/data pair is sized to the desired width and height.

The titles use the HTML <H3> tag to increase their size and weight. An HTML tag will scale relative to the actual fontSize of the text, which may be shrunk or stretched to fit. The Home address title is also colored fuchsia, although more to show that it can be done than as something that should be. You can use the Formatting toolbar to apply this formatting, and others. The text property of the Home address title contains the character string:

<H3><font color="fuchsia">Home address</font></H3>

Defining and formatting the data fields

Some of the columns are easy, like the Email address, which is a single field. Its text property is the codeblock:

{|| this.form.contacts1.rowset.fields["Email"].value}

But most of the columns require some work. For example, all the phone numbers are stored in ten-character fields, as the digits only. The parentheses around the area code and the hyphen after the prefix is inserted with the TRANSFORM( ) function, as in the Home fax column:

{|| transform( this.form.contacts1.rowset.fields["WorkFax"].value, "@R (999)999-9999" )}

The Work phone combines both a phone number and a four-digit extension, which might be null. Remember that anything plus null is null. You can use this to your advantage, to place an "x" in front of the extension, if it exists. Here is the codeblock:

transform( this.form.contacts1.rowset.fields["WorkPhone"].value, "@R (999)999-9999" ) +
new String( " x" + this.form.contacts1.rowset.fields["WorkExtension"].value )

(Once the codeblocks get this long, or even before, you should use the codeblock builder dialog, which you get by clicking the tool button in the Inspector when the data type is Codeblock. Codeblocks are always saved out as one long line in the source code, but in the dialog, you edit them with a multi-line editor. The dialog automatically places the curly braces and pipes around the codeblock, which is why they're not shown above; that is how you will see the codeblock in the codeblock builder.)

Looking at the second half of the codeblock, the letter "x" (with a space in front of it) is concatenated with the WorkExtension field. If the WorkExtension field is empty—or more precisely, if it is null—then the "x" will disappear, because the result is null. If the field is not null, then you get the "x" in front, which is then concatenated with the phone number, which is formatted with TRANSFORM( ), like before.

What's with the new String()? Well, if the extension is null, you don't want to destroy the phone number. If you pass null to the String class constructor, you get an empty string, "", a string with nothing in it. Any string is returned unchanged. So using new String() "de-nulls" the resulting expression. Now, you could use a more direct IIF( ) expression, like:

iif( empty( this.form.contacts1.rowset.fields["WorkExtension"].value ), "",
    " x" + this.form.contacts1.rowset.fields["WorkExtension"].value )

but who wants to type that field object reference twice? Using new String() is less error-prone, and you will see it a lot when combining field values that might be null.

(To digress for a moment, you can avoid a lot of problems with null field values simply by defining a default value for the field. Many table formats, including DBF7, support this. You can say for example, that you want the field to default to spaces, just like with older DBF tables. But if you do this, you will probably still have to deal with empty fields by trimming spaces.)

The Name column concatenates the first name, middle initial, and last name:

new String(this.form.contacts1.rowset.fields["FirstName"].value).rightTrim() +
new String( " " + this.form.contacts1.rowset.fields["MiddleInitial"].value).rightTrim() +
" " + new String(this.form.contacts1.rowset.fields["LastName"].value)

Each field uses new String() to deal with nulls. The MiddleInitial uses the same technique as the phone extension to place a space in front of it, but only if it's not null. Both the FirstName and MiddleInitial trim their trailing spaces with the rightTrim( ) method. The TRIM( ) function would also work, but the method has the advantage of simply being tacked on to the end of the string with the dot operator. In contrast, the function requires that you place it correctly, balancing the parentheses. It's harder to see exactly what the function is enclosing, which makes it more difficult to read or edit the codeblock. Note that the rightTrim( ) method is applied to the result of the new String() operation, not directly on the field's value property, because if the value is null, it doesn't have a rightTrim( ) method.

The multi-line Home address combines a number of fields:

this.form.contacts1.rowset.fields["HomeAddress"].value.rightTrim() +
new String( "<BR> " + this.form.contacts1.rowset.fields["HomeCity"].value.rightTrim() +
    " " + this.form.contacts1.rowset.fields["HomeState"].value + " " +
    transform( this.form.contacts1.rowset.fields["HomeZip"].value, "@R 99999-9999" ) )

This codeblock assumes that the HomeAddress will never be null; it doesn't use new String(). The HTML <BR> tag causes the line break. Because of a minor bug, you must include a space after the <BR>; otherwise, the next line will break again after the first character. The contents of the HomeCity, HomeState, and HomeZip are all added together. If any of these are null, then the entire result, including the line break, will be nulled out, so that the column contains just the one address line by itself. You would want to have your data validation verify that those fields are filled in. If your data is missing data for some of those fields, then you would have to add more new String() operations to build the result correctly.

Choosing the columns

The columns to print are chosen using a variation of the standard two-listbox mover dialog, PrintColumns.wfm, shown in the following figure:

Most of the functionality in this dialog is directed towards managing the moving of columns from one list to another. There is also code to display the current width of the columns chosen (which is calculated elsewhere), and to display the hints regarding the Shrink/Stretch To Fit feature. Finally, when you click the OK button, the chosen columns are set in the report, and the report is rendered. All the interesting code is in the report itself.

Using the template report

The custom report class ColumnsCReport, in Columns.crp, is the base report class for all the choose-your-columns reports. Its metric is inches, to match the layout form; and it contains the code to read the layout form, return the list of columns available, calculate the width of a given set of columns, set the columns chosen, and shrink or stretch the columns to fit. Once the columns are set, the report can be rendered with the render( ) method, just like any other report.

The chooser form uses the ContactsReport class in Contacts.rep, which subclasses ColumnsCReport and contains the specific layout items for the Contacts report. There isn't much: a title, the date and page number, and a line in the detail band to separate each row of the report. There are no data fields; they will be generated by the report. The bindColumnReport( ) method in the chooser form assigns the specific report and layout forms that will be used to generate the report:

function bindColumnReport
  set procedure to contactscolumns.wfm additive
  set procedure to contacts.rep additive
  this.report = new ContactsReport()
  this.report.setColumnForm( new ContactsColumnsForm() )

The method opens the necessary procedure files, instantiates the report and assigns it as the chooser form's report property, and then instantiates the layout form and passes it to the report's setColumnForm( ) method. That method is defined in the base report class:

function setColumnForm( argForm )
    if this.colForm == null   // Can set once only
      this.colForm     := argForm
      this.streamFrame := this.firstPageTemplate.streamFrame1
      this.dataIndent  := this.streamFrame.left + ;
          this.streamFrame.marginHorizontal + this.getBandMarginLeft()
      this.frameWidth  := this.streamFrame.width - ;
          ( 2 * this.streamFrame.marginHorizontal ) - this.getBandMarginLeft()
      this.frameTop    := this.streamFrame.top
      this.loadColumns()
    endif 

The setColumnForm( ) method can be called only once. A reference to the form is stored in the report's colForm property, and a reference to the stream frame named streamFrame1 in the report's firstPageTemplate is stored as the report's streamFrame property because it will be accessed often. Then various sizes and offsets are calculated for the report, referencing that stream frame. This means that this code only works for reports with one page template, that have one stream frame with the default name. Luckily, this describes the vast majority of reports. Finally, the loadColumns( ) method is called:

function loadColumns
  // Get titles and data from columns form
  this.colForm.titlesAndData( this.titles, this.data )
  local cKey, n, t, d
  cKey = this.titles.firstKey
  for n = 1 to this.titles.count()
    // titles[] and data[] contain element numbers that are keyed by name
    t = this.colForm.elements[ this.titles[ cKey ] ]
    d = this.colForm.elements[ this.data[ cKey ] ]
    // Set column width to larger (using max() function) of title or data width
    this.cols[ cKey ] = new ReportColumn( max( t.width, d.width ), t.text, d.text, ;
        t.alignment, d.alignment, t.fontName, d.fontName, t.fontSize, d.fontSize )
    this.titleHeight := max( this.titleHeight, t.height )
    cKey := this.titles.nextKey( cKey )
  endfor

The layout form's titlesAndData( ) method, defined in the base form class ColumnsCForm, is called to populate the two associative arrays, titles[ ] and data[ ]. Each associative array contains the same set of keys, which are the names of the columns, like "EMAIL" and "HOMEPHONE". The titles[ ] array contains the form element numbers of the Text objects that represent the titles for each column, and the data[ ] array contains the element numbers of the Text objects for the column data.

A FOR loop goes through all the titles, getting object references to the two Text objects for each column. The important properties for both objects are then passed a new ReportColumn object, also defined in Columns.crp, which simply acts as a container for those properties. This new object is assigned to a third associative array, cols[ ]. After the FOR loop is finished, the cols[ ] array contains the pertinent information for all the columns:

The loop also tracks the height of the titles, and keeps the height of the largest one, which will be used for the titles, in the titleHeight property.

Calculating the total column width

Every time the user changes their selection of columns, the chooser form asks the report what the resulting total column width would be, by calling the report's getTotalWidth( ) method:

function getTotalWidth( aColumns )
  local w, n
  w = 0
  for n = 1 to aColumns.size
    w += this.cols[ aColumns[ n ] ].width
  endfor
  return w + this.getColumnSpacing() * ( aColumns.size - 1 )

This method is pretty straightforward: it takes the array of column names, gets the column's width from the cols[ ] array, and adds them all together. It then adds the spacing between all the columns. To get that spacing, the report calls its getColumnSpacing( ) method:

function getColumnSpacing()
  return 0.1

This simple method takes the place of an Inspect-able custom property. If you were to create a custom property, you could see it in the Inspector, but you can't edit it. There's no point in setting the value in the class constructor, because that will vanish when the designer restreams (rewrites) the constructor when it saves the report. Since you have to resort to touching the code anyway, a simple method works just as well, is not touched by the designer, and can be easily overridden in a subclass. In fact, there are two other "pseudo-properties" defined in the ColumnsCReport base class:

function getBandMarginLeft()
  return 0

function getBandMarginTop()
  return 0

The Contacts report overrides getBandMarginTop( ) to provide spacing between the line at the top of the band and the text inside it, simply by creating a method with the same name in Contacts.rep:

function getBandMarginTop()
  return 0.1

Generating the report

When the user clicks OK in the chooser form, it calls the form's printReport( ) method, which in turn calls one of the following three methods defined in the report, depending on whether they chose to shrink or stretch the text:

function setColumns( aColumns )
  this.setCols( aColumns )
  this.genCols()

function setColumnsShrink( aColumns )
  this.setCols( aColumns )
  if this.getTotalWidth( aColumns ) > this.getFrameWidth()
    this.scaleText()
  endif
  this.genCols()

function setColumnsStretch( aColumns )
  this.setCols( aColumns )
  if this.getTotalWidth( aColumns ) < this.getFrameWidth()
    this.scaleText()
  endif
  this.genCols()

Each method takes a list of columns to print, which is passed on to the setCols( ) method. That method simply makes a copy of the columns in an array called colIndex[ ]. Then, if they chose to shrink, and the chosen columns are wider than the frame; or they chose to stretch, and the columns are narrower than the frame; then the scaleText( ) method is called. scaleText( ) calculates the exact percentage required to make the columns fit exactly, and then adjusts the width of all the columns. It will also shrink the height of the titles stored in the titleHeight property. It never makes the titles taller, because there is no easy way to guarantee that there is enough space at the top of the report for really large titles.

Creating the report text

Finally, the genCols( ) method is called to create the report text:

function genCols
  local n, c, e, nTop, nLeft, nColSpace
  nTop      = this.getBandMarginTop()
  nLeft     = this.getBandMarginLeft()
  nColSpace = this.getColumnSpacing() * this.scaleFactor
  for n = 1 to this.colIndex.size
    c = this.cols[ this.colIndex[ n ] ]
    // Set title in PageTemplate
    e = new Text( this.firstPageTemplate )
    e.text      := c.titleText
    e.top       := this.frameTop - this.titleHeight
    e.left      := nLeft + this.dataIndent
    e.width     := c.width
    e.fontName  := c.titleFontName
    e.fontSize  := this.roundFontSize( c.titleFontSize * min( 1, this.scaleFactor ) )  // Shrink only
    e.alignment := c.titleAlign
    // Set data in detailBand
    e = new Text( this.streamFrame.streamSource.detailBand )
    e.text      := c.dataText
    e.top       := nTop
    e.left      := nLeft
    e.width     := c.width
    e.fontName  := c.dataFontName
    e.fontSize  := this.roundFontSize( c.dataFontSize * this.scaleFactor )
    e.alignment := c.dataAlign
    e.variableHeight := true
    // Update left for next column
    nLeft += c.width + nColSpace
  endfor

First, for convenience (less typing and slightly faster execution), the psuedo-properties are copied into variables. The top margin will be reused for every data item. The left margin is used as the starting position for the first column. The inter-column spacing is scaled according to the calculated scaling factor. Then using a FOR loop to go through all the columns in the colIndex[ ] array:

  1. The ReportColumn object describing the column is retrieved from the cols[ ] array.
  2. A new Text object is created directly on the page template for the title.
  3. The properties for the title are set.
  4. A new Text object is created for the data field inside the detailband of the stream frame's streamSource.
  5. The properties for the data field are set.
  6. The horizontal position is updated for the next column by adding the width of the current column and the scaled column spacing.

When finished, all the Text objects for all the columns will be laid out. The report is ready to render with the standard render( ) method, which is called from the chooser form's printReport( ) method.

Scaling fonts

A quick word about scaling fonts: you cannot simply multiply a font size by some scaling factor and expect the results to be exactly what you want. For example, if the font size is 10, and you figure that you must shrink everything to 89% of the original size, multiplying the two will give you 8.9 points. But if you ask for text that is 8.9 points, Windows or the printer may round that to 9 points, which would be too big. So the first thing you would do is round the point sizes down to the nearest full point or half-point.

To further complicate matters, decreasing the font size does not necessarily decrease the width of the resulting text by the same amount! You can see this by editing the form ContactsColumns.wfm with the designer. Select the data for the Home Phone/Fax, and change the fontSize property from 10 to 9. The text will noticeably shrink, but the space between the digits will increase, so that the width is almost unchanged. Playing with the fontSize shows real size changes only at the even sizes.

So how do you handle this? The roundFontSize( ) method defined in ColumnsCReport rounds down to the nearest even size, with a minimum size of one point (you cannot have a zero point size):

#define POINT_GRANULARITY 2        // Round to even point size
function roundFontSize( nPtSize )
  local nRet
  nRet = floor( nPtSize / POINT_GRANULARITY ) * POINT_GRANULARITY
  return max( 1, nRet )            // Minimum point size is 1

Another approach would be to make sure that you have enough extra space inside each Text object, in case the font doesn't shrink enough when it scales. In that case, you could change the POINT_GRANULARITY constant to 1 or perhaps 0.5 points, so that the text is as large as possible. Note that the printer and the display handle font scaling differently, so what may appear to be too large on the screen might fit on the printer.

Top


Power programming example 3: custom data objects

One of the advantages of using the OODML is that it surfaces most of the methods that dBASE itself uses to manipulate your data. For example, when you click the Next button in the default toolbar, it actually calls form.rowset.next( ). By default this is the stock next( ) method in the Rowset class. But you can override this method to provide your own functionality in addition to, or in lieu of, the built-in behavior. That capability is extended here to provide a feature that is not in Visual dBASE 7: the ability to use the old Xbase DML in reports.

Now at first glance, it might appear like it will work. The provided Flight.qbe, which uses the sample Fleet database, has a one-to-one lookup and a one-to-many relation with SET SKIP, and a filter to show only those parent records that have children:

close databases
open database FLEET
set database to FLEET
use FLIGHT
use SCHEDULE in 2 order :FLIGHT ID:
use AIRPORT in 3 order :AIRPORT ID:
select FLIGHT
set relation to :FROM ID: into AIRPORT, :FLIGHT ID: into SCHEDULE
set skip to SCHEDULE
set filter to found(2)

If you run this QBE (with the SET VIEW command) and create a new report in the Report designer, you will see the tables and fields in the Field palette. But if you drag one of those fields onto the report surface, nothing appears to happen. Actually, the Text objects for the title and data are created correctly. But the problem is that there is no rowset to drive the report; to the report engine, it's as if the rowset is empty, so nothing is generated. The report needs a valid rowset.

Creating a custom rowset

The solution lies in the four classes in X.dmd. The first one is a custom data module:

class XDM of DataModule

  private cmd, q
  local nWorkArea
  for nWorkArea = 1 to 225
    if not empty( alias( nWorkArea ))
      q = new XQuery( nWorkArea )
      q.parent := this
      cmd = "this." + q.alias + " = q"
      &cmd.
      if nWorkArea == workarea()
        this.rowset := q.rowset
      endif
    endif
  endfor

endclass

When the XDM data module is instantiated, it loops through all 225 available workareas, checking if there is a table open. If there is one, it creates an XQuery object to represent that workarea. It uses &macro substitution to create the statement that binds the query to the data module, using the alias name as the property name. While a normal data module would default to using the "name plus 1" rule to name its queries, like "fleet1" and "schedule1", this approach uses the actual alias names, like "fleet" and "schedule", which are already guaranteed to unique.

Finally, if the current query in the loop is for the currently selected workarea, then that query's rowset is assigned as the data module's primary rowset.

The XQuery class

The XQuery class subclasses the stock Query class to generate a rowset that access the workarea. Examining it method by method:

class XQuery( nWorkArea ) of Query

  if empty( nWorkArea )
    nWorkArea := workarea()
  endif
  this.workArea = nWorkArea
  this.alias    = alias( nWorkArea )
  this.skipList = new Array()

  this.session := null
  this.active  := true
  this.rowset.handle := this.workArea

The constructor expects the workarea number as a parameter. None of the setup code SELECTs the workarea; everything is done via the workarea/alias parameters in the various XDML functions. If no workarea is specified, the current workarea is used. Both the workarea number and the alias name are stored as properties. An empty array named skipList is used to store those workareas that are related via SET SKIP, which is necessary to implement bookmarks.

The session property is set to null to tell dBASE that this is not a BDE object. The query is activated, and the handle of the query's rowset is arbitrarily set to the workarea number. The rowset handle must be non-zero, or the report engine will assume that the rowset is invalid. The workarea number is used because it will be unique for each XQuery rowset, although that fact is not that important.

  function execute()
    this.buildFieldsArray()
    this.linkRowsetMethods()
    this.updateRowsetProperties()
    if not empty( set( "SKIP" ) )
      this.buildSkipList()
    endif

The query's execute( ) method is executed when the query is activated. Normally, this method does the work of executing the SQL statement in the sql property. It is overridden here to call the four methods that actually create the custom query instead.

  #define FIELD_NAME_DELIMITER ":"
  function buildFieldsArray()
    private code
    local nField, cFieldName, calcField
    nField = 1
    cFieldName = field( nField, this.workArea )
    do while not empty( cFieldName )
      // Does not work: code = "{|| " + this.alias + "->" + cFieldName + "}"
      code = "{; private w, r; w = workarea(); select " + this.workArea + ;
             "; r = " + cFieldname + "; select (w); return r}"
      if left( cFieldName, 1 ) == FIELD_NAME_DELIMITER and 
          right( cFieldName, 1 ) == FIELD_NAME_DELIMITER
        cFieldName := substr( cFieldName, 2, len( cFieldName ) - 2 )
      endif
      calcField = new Field()
      calcField.fieldName      := cFieldName
      calcField.length         := flength( nField, this.workArea )
      calcField.beforeGetValue := &code.
      this.rowset.fields.add( calcField )
      cFieldName := field( ++nField, this.workArea )
    enddo

All the fields in the workarea are represented by calculated fields. Whenever you ask for the value of a field, dBASE will execute the field object's beforeGetValue event handler, if one is defined; all calculated fields use the beforeGetValue event to return their values. Ideally, the calculation would simply refer to the field value with the alias operator (->). Unfortunately, when you do that, dBASE crashes.... However, manually SELECTing the workarea and returning the field value appears to work. In comparison, it's quite a bit more work, because values must be saved to variables, but overall it's not that complicated.

The codeblock that contains the code is created as a string. Then using the &macro operator, the codeblock is evaluated and assigned to the beforeGetValue event. This approach uses the &macro, which is a relatively time-consuming operation, only once for each field. In creating the codeblock, any colons (:) used as field name delimiters for fields with spaces in their name are needed, but they are stripped before the field name is assigned to the field's fieldName property. The length is copied from the Xbase FLENGTH( ) function.

After adding the new calculated field to the fields array, the next field is retrieved with the FIELD( ) function. Note that the field number is incremented with the pre-increment operator, because the current field number is needed for the FLENGTH( ) function.

  function linkRowsetMethods()
    with this.rowset
      next           := XRowset::next
      first          := XRowset::first
      last           := XRowset::last
      bookmark       := XRowset::bookmark
      bookmarksEqual := xRowset::bookmarksEqual
      goto           := XRowset::goto
      count          := XRowset::count
      atFirst        := XRowset::atFirst
      atLast         := XRowset::atLast
    endwith

The linkRowsetMethods( ) method does just that: assign methods defined in the XRowset class, described next, overriding the built-in methods. These methods are all that are needed by the report engine.

  function updateRowsetProperties()
    with this.rowset
      endOfSet := eof( parent.workArea ) or bof( parent.workArea )
    endwith

The updateRowsetProperties( ) method updates the rowset's endOfSet property. It's called when the query is activated to indicate whether there is any data in the rowset.

  function buildSkipList
    local n, t, s
    n = 1
    t = target( n, this.workArea )
    s = "," + set( "SKIP" ) + ","    // All aliases are surrounded by commas
    do while not empty( t )
      if "," + t + "," $ s
        this.skipList.add( t )
      endif
      t = target( ++n, this.workArea )
    enddo

endclass

The final method, buildSkipList( ), is called only if SET SKIP has been used. It gets all related tables with the TARGET( ) function and checks if they are on the SET SKIP list. If so, they are added to the query's skipList[ ] array. This array will be used in the XBookmark class to store bookmarks for those tables whenever a bookmark is retrived for the query. This is necessary because when you get the bookmark for a table that uses SET SKIP, you also need the bookmarks for the related tables; otherwise, when you go back to that bookmark, those related tables will not be on the correct record.

The XRowset class

The XRowset class contains all the interesting rowset emulation code. Note that it does not subclass the Rowset class. There is no reason to do this because XQuery will always create a stock Rowset object anyway. This class simply acts as a container for a set of methods. Again, method by method:

class XRowset

  function next( n )
    skip iif( argcount() == 0, 1, iif( bof(), n-1, n )) in (this.parent.workArea)
    this.refreshControls()
    this.endOfSet := eof( this.parent.workArea ) or bof( this.parent.workArea )
    return not this.endOfSet

The next( ) method takes a numeric parameter. If it is omitted, it defaults to 1. You SKIP the number of records specified. If you're starting at BOF( ), you must subtract one, because BOF( ) is on the first record. (In the OODML, endOfSet at the beginning of the rowset is before the first record, symmetrical to the end of the rowset. BOF( ) and EOF( ) in XDML are not symmetrical.) The controls are refreshed, and endOfSet is set. Finally, next( ) returns true if the navigation does not leave you at the endOfSet.

  function first
    go top in (this.parent.workArea) 
    this.refreshControls()
    this.endOfSet := bof( this.parent.workArea )
    return not this.endOfSet

  function last
    go bottom in (this.parent.workArea)
    this.refreshControls() 
    this.endOfSet := eof( this.parent.workArea )
    return not this.endOfSet

first( ) and last( ) are similar to next( ), using GO TOP and GO BOTTOM respectively to go to either end of the rowset.

  function bookmark
    return new XBookmark( this.parent.workArea, this.parent.skipList )

The bookmark( ) method instantiates and returns a new XBookmark object, described next. Although the bookmark( ) method is expected to return a value of type BookMark, the report engine does not complain when it gets an object instead, and dutifully passes this object back to the goto( ) method.

  function goto( b )
    go (b.bookmark) in (this.parent.workArea)
    this.endOfSet := false
    b.goRelated()
    return true

goto( ) takes the XBookmark object returned by bookmark( ) and goes to the real bookmark stored in its bookmark property. It then clears the rowset's endOfSet property, because you can only goto a valid row. Next, it calls the XBookmark object's goRelated( ) method to go to the correct record in any related workareas. Finally, assuming everything succeeds, it returns true.

  function count
    local w, b, c
    w = workarea()
    select ( this.parent.workArea )
    b = bookmark() 
    count to c 
    goto b
    select w
    return c

In the OODML, count( ) does not move the record pointer, so extra effort is needed to do the COUNT, which must be in the current workarea.

  function atFirst
    if eof( this.parent.workArea ) or bof( this.parent.workArea )
      return false
    else
      local b, ret
      b = bookmark()
      skip -1 in (this.parent.workArea)
      ret = bof( this.parent.workArea )
      goto b
      return ret
    endif

  function atLast
    if eof( this.parent.workArea ) or bof( this.parent.workArea )
      return false
    else
      local b, ret
      b = bookmark()
      skip in (this.parent.workArea)
      ret = eof( this.parent.workArea )
      goto b
      return ret
    endif

endclass

atFirst( ) and atLast( ) are similar to each other. They can immediately return false if the table is at EOF( ), past the last record. BOF( ) is different because if you're on BOF( ), you are also on the first row. But because of the OODML semantics for the endOfSet at the beginning of the rowset are different, you can't allow being at BOF( ) to be considered the same as being on the first row. If you did, the record count for next( ) would be off by one. (If you are atFirst( ), then next( ) should take you to the second row, but to emulate OODML semantics, next( ) from BOF( ) takes you to the first row.) So if you're at BOF( ), then you can't be atFirst( ), because paradoxically, you know you really are. Otherwise, to see if you are at the first or last row, you venture one record toward the respective end of the table, and if you hit the end, you know you were at that terminal record to begin with.

With these methods, all navigation through the OODML actually causes record pointer movement in the underlying workareas. Every time the field value read, the calculated field actually gets the value from the corresponding table. By doing these two things, the XDM data module emulates read-only access to your XDML tables through the OODML.

The XBookmark class

The XBookmark class is needed because of SET SKIP. With SET SKIP, navigation through a single table can actually cause navigation through many tables. If you want to bookmark a record, you really need to bookmark the locations in all those related tables.

class XBookmark( nWorkArea, aSkipList )

  this.bookmark = recno( nWorkArea )
  this.related  = new AssocArray()

  local n
  for n = 1 to aSkipList.size
    this.related[ aSkipList[ n ] ] = recno( aSkipList[ n ] )
  endfor

  function goRelated()
    local cKey
    cKey = this.related.firstKey
    do while not cKey == false
      go ( this.related[ cKey ] ) in ( cKey )
      cKey := this.related.nextKey( cKey )
    enddo

endclass

When instantiated, it expects a workarea number, and a list of related workareas to check. This object doesn't know about the other classes used by XDM. It stores a bookmark in the bookmark property, and creates an AssocArray of related work areas. Then the code runs through the list of workareas, and stores all those bookmarks, using the alias as a key.

After the XRowset's goto( ) method goes to the record stored in the bookmark property, it calls the XBookmark object's goRelated( ) method, which simply runs through the AssocArray and goes to the appropriate record in the related workareas.

Using the XDM data module

To use XDM in reports:

  1. Open your tables through the Xbase DML, using a .QBE or manual commands
  2. Make sure that you have the primary table (the root of any relations) SELECTed in the current workarea
  3. Open the Report designer to create a blank report.
  4. Drag the X.dmd from the Navigator onto the report surface.
  5. Create your report normally.

You may want to set the Report designer properties to display only one row to make it run faster.

A sample report that uses the Fleet.qbe, Schedule.rep, is included. Note that the report uses totals in the group header, which requires the use of bookmarks by the report engine, and that header is repeated in every stream frame. The report seems to contain the correct results.

One final word: XDM is beta code. It appears to work, but it hasn't been tested thoroughly. One known issue is that the SET SKIP support really only works down one level. Any comments can be directed to the dBASE2000 newsgroups at news://news.dbase2000.com.

Top