Straight Talk about Built-In Files

It's back-to-basics time, VFP folks.

As I posted in reference to the Sedna Beta files, the "swapcopy" code we've put into _ReportListener and leveraged in gfxNoRender has to be used very carefully, and we are tightening it up as much as possible so that it serves as a good example of handling a new-in-SP2 product feature.  (If you're not up-to-speed on this already, that new feature is read-write ReportListener.CommandClauses.File value in the LoadReport event.) 

The thing we're trying to provide an example of is a very-specific-to-reporting feature.  The basic problem-to-solve, however, as described in the "Backstory" section of that post, is not. That basic problem is how to handle files built into APP/EXEs and how to use them as resources in Xbase code. 

I am constantly amazed by the number of people who find a bug in the way something of this nature is handled and don't know how to correct it, as a result of which they conclude that the whole thing is impossible to resolve.

Basic principle #1: SYS(2000) and FILE() are different.  Use both, and use each appropriately.

Use SYS(2000) when you want to absolutely, positively ensure that you have a copy on disk  — for example, you need to edit the file. 

Use FILE() when your code needs to "see" a file but it may be a readonly resource built into an APP or EXE. If FILE() can "see" it you can work with it, for example to create a copy of it on disk that you can further manipulate.

Every time you need to check that a file exists, you should evaluate: which function should I use here?

In what situation might a file exist and be available to your application but not be available to your Xbase code? In other words, in what situations is FILE() a required check? If you don't know the answer, see principle #4 below.

In FFC reportlistener code, you will see both functions used all over the place. For what it's worth, I didn't use the wrong function in the new swapcopy code, I was just so intent on a specialized version check (was SP2 functionality available?) that I forgot to do an additional FILE() check before attempting to USE the FRX in two new places. 

You often have to use both functions in one operation.  For example, use the FILE() check to see if you can make a copy of the file and then, afterwards, use SYS(2000) to make sure that the copy on disk exists before proceeding.

Basic principle #2: Use FULLPATH() and understand the results.

You won't see a use of FULLPATH() illustrated in the reporting example under discussion in our swapcopy code, because ReportListener.CommandClauses.File is already fullpath'd.  The important thing to realize is that the result of FULLPATH() — and also the path you see in ReportListener.CommandClauses.File — may not actually exist if the file is built into the APP/EXE file. But that is perfectly okay. 

What you're seeing is an indication of how the APP/EXE "thinks" about that file within the structure of the compiled version, to distinguish between it and other potential files with the same filename that are also built in.  It's more like an file ID or a virtual path than a physically-pathed filename. It doesn't matter that the path looks like nonsense on the deployment machine; if FILE() sees the file, then your other code working on that file will also see it.

In other words, it's really not a bug, it really is a feature. And it's misunderstanding this feature that causes a lot of people not to be able to use built-in files successfully.

Basic principle #3: Most code will work with files that FILE() can "see" — but some won't.

In scenarios where you've chosen to use FILE() rather than SYS(2000) to validate the availability of a file, verify the availability of the syntax with which you manipulate that file.

When you work with a built-in file, it's read-only.  That much is obvious. For example, you can't USE a built-in lookup DBF and append a record to it.

But some things are not quite so obvious, so it's always a good idea to check any operation you think should be usable against a built-in file.  For example, you will see this code in the FFC's XMLDisplayListener class, where it is going to publish an image to your stipulated external files location as part of the support files for an XML version of the report output:

IF EMPTY(SYS(2000,m.lcFile))
   * used to be:
   * COPY FILE (cContentsToBeRendered) TO (lcFile)
   * to handle files built into an app

   STRTOFILE(FILETOSTR(m.cContentsToBeRendered),m.lcFile)
ENDIF

.. the nested STRTOFILE(FILETOSTR()) simply worked better than COPY FILE to reproduce the image files faithfully, and I switched.

Basic principle #4: Xbase code using FILE() and other built-in file-access can't travel up code modules in the calling chain.

One of the little-known "deep" changes that had to be made for _REPORTOUTPUT and a separate REPORTOUTPUT.APP to work at all was in the base class reportlistener code. If you're using REPORTOUTPUT.APP to get your reportlistener and you've built in your FRXs and/or images referenced in your FRXs to your APP/EXE, the base class has to be able to see those images to render the report. Our Xbase code also gets to make an extra "hop" in this unique situation, to a certain extent. 

It's not infinitely extensible behavior. If you modularize your application and (say) have a master EXE that hands listener references to a reporting APP/EXE, you're going to run into some limitations, no matter what your design.

As I mentioned in that previous post, this is a fact of Xbase life and not specific to reporting at all, but making Xbase code such an integral, seamless part of the base reporting process really pushes this fact into the forefront of our collective Xbase development consciousness. 

You can bring reporting back into alignment with the rest of your Xbase code, in this area, by simply removing REPORTOUTPUT.APP from the equation. I cannot stress enough the fact that REPORTOUTPUT.APP was just a leg up to bridge the two syntaxes (SET REPORTBEHAVIOR 80 and object-assisted 90).  Please do remove the extra hop and bring the coordination of APP/EXEs under your direct code by eliminating REPORTOUTPUT.APP; you can get the same functionality by simply pointing _REPORTOUTPUT at frxoutput.PRG instead.  The FFC _ReportListener.VCX library is identical to the files used in REPORTOUTPUT.APP, by design. The discussion in the docs that introduces this critical point is in the topic Including Report Files for Distribution.  The simple method required to build in the necessary components is identical for all three REPORT*.APPs and is covered in the topic How To: Specify and Distribute ReportBuilder.App.