VFP 9.0 Reporting System Fundamentals Part 2

This article continues from Part One.

2. The Reporting System at Run Time

In this section, we are going to cover how the object-assisted reporting engine works during a report run.

Recap: Reporting in VFP 8.0 and earlier

The Reporting System in Visual FoxPro 8.0 and earlier was a bit of a black box. We had about three choices of places to send the report output, using clauses on the REPORT FORM command:

  • We could send it to the printer
  • We could send it to a preview window
  • We could send it to a file

There are a few other clauses, PROMPT, ASCII, but basically that was it.

Reporting in VFP 9.0

By default, the reporting system in VFP 9.0 processes reporting command syntax in exactly the same way as in previous versions of Visual FoxPro.  The same REPORT FORM commands will produce identical results as VFP 8.0 and earlier.

...Of course, that's not the end of the story. The report engine in VFP 9.0 has been enhanced significantly:

  • It allows numerous events during the report run to have VFP code attached to them in "object assisted mode";
  • It can use GDI+ rendering for greater accuracy and true WYSIWYG previewing;
  • It makes alternative output formats possible

OK, so how do we get to the new engine?

If the existing REPORT FORM syntax produces the same results as previous versions of FoxPro, then how do we get to the new features of the report engine? The answer is: Through a new Visual FoxPro base class, and some new command syntax.

The Reportlistener Base Class

If we take a look at the Help file topic for the ReportListener class, we can see a daunting array of properties and methods: LoadReport, BeforeBand, AdjustObjectSize, GetPageHeight, Render, OutputPage, gdiPlusGraphics... these look like the kinds of things that a reporting engine would need to implement. In fact, that's exactly what they are.

Properties and Methods

Let's take a look at one:

oX = NEWOBJECT("Reportlistener")

The report engine, formerly a "black box", has been extended and re-engineered to expose a good deal of its functionality as a FoxPro base class that we can subclass and attach our own custom behavior to, giving us a kind of an object-oriented report run.

New Syntax: REPORT FORM .. OBJECT x

The object-assisted mode of the report engine is enabled through some new command syntax:

oX = NEWOBJECT("Reportlistener")
REPORT FORM customer OBJECT m.oX

This plugs an instance of the Reportlistener class in to the report run:

  • Properties of the ReportListener control aspects of how the report run is processed;
  • Methods of the ReportListener are invoked as the report run proceeds

In this way, the new GDI+ report engine is enabled; custom report previews can be displayed, and almost unlimited output types are possible, depending on what variety of report listener class we use.

Properties of the ReportListener class

Let's look at some of the properties of a ReportListener in more detail:

The listenerType property

ListenerType controls how rendered report pages are treated by the reporting engine:

ListenerType
 value
OutputPage()
"style"
Report System Behavior
0 Event This is the closest to VFP 8.0 behavior: Each page is rendered and sent to the printer. After each page is rendered, the report listener's OutputPage() method is invoked. OutputPage() is treated as an event by the report engine.
2 Event This mode is similar to 0, except that the report engine does not send the rendered pages to the printer.
3 Method In this mode, the report engine renders the pages but caches the GDI instructions internally as an enhanced metafile (EMF). OutputPage() is not invoked during the report run.
1 Method This mode is the same as 3, with the addition that when the report run has completed, the listener will call the .Show() method of the object assigned to its PreviewContainer property.
-1 - The report engine runs the report but does not perform any page rendering. OutputPage() is not invoked. This is a fast way of pre-processing a report.

 

The OutputPage() method

According to the help file, OutputPage "Provides access to the current page or the full range of pages for a report run, according to the value of the ListenerType property."

Is OutputPage() an event or a method? It depends on how the ListenerType property is configured.

"Page at a time" mode (event style):
ListenerType
value
PageNo
(parameter 1)
OutputDevice
(parameter 2)
DeviceType
(parameter 3)
0 Page being rendered GDI+ handle to printer -1
2 0 -1

When listenerType is 0 or 2, the report engine sends the current page number after it has rendered a page. If we wished, we could write code in OutputPage() using DODEFAULT() to echo the page to an alternate output device.

When listenerType is 0, the report engine will pass the GDI+ graphics handle to the current printer in this parameter.

When listenerType is 2, this parameter will have the value 0.

"Spool and wait" mode (method style):
ListenerType
value
PageNo
(parameter 1)
OutputDevice
(parameter 2)
DeviceType
(parameter 3)
1 or 3 Rendered page to export GDI printer handle 0
GDI+ graphics handle 1
Object reference to a VFP Shape or Container 2
File name (EMF) 100
File name (TIFF) 101
: :

When listenerType is 1 or 3, we can  specify which page number we wish to direct to an output device using this parameter.

When listenerType is 1 or 3, we can invoke OutputPage() using this parameter to specify the destination device, indicating the type of device in the third parameter (iType).

Aside: OutputPage() also has additional parameters: 4 dimension parameters and 4 clip parameters. These are useful for advanced techniques such as described in my article, Techniques for an alternative Report Preview UI.

We'll look at examples of using OutputPage() in more detail later on.

The previewContainer property

This property is only used when listenerType = 1. The report listener expects it to contain an object reference to a FoxPro form class or similar. We'll talk more about this property later on.

What happens during a report run

During an object-assisted report run:

  • The report listener has a private data session available to it, the session Id of which is available in its FrxDataSession property. (Note that this is not the same data session that the listener was created in.) This session stays available for the entire report run.
  • A read-only copy of the report layout is open in this session with the alias "frx".
  • The listener's commandClauses property contains an object (base class Empty) that has attributes that indicate which clauses were used on the command that launched the report run.
  • Methods of the listener are invoked by the report engine as it traverses the data set and processes the report layout.

SET REPORTBEHAVIOR 80 | 90

Now that we have learned how to run reports in object-assisted mode, using the new reportlistener class and the new REPORT FORM.. OBJECT syntax, let's revisit a statement we made earlier:

"The reporting system in VFP 9.0 processes reporting command syntax in exactly the same way as in previous versions of Visual FoxPro.  The same REPORT FORM commands will produce identical results as VFP 8.0 and earlier."

There is a new SET command in Visual FoxPro 9.0 that affects the validity of that statement: SET REPORTBHAVIOR. According to the help file, this command "Configures how Visual FoxPro processes REPORT FORM and LABEL FORM commands."

By default, REPORTBEHAVIOR is set to 80 to ensure backward compatibility with existing applications that may assume that the reporting system and preview windows work a certain way. You can still use the new REPORT FORM .. OBJECT syntax to process reports in object-assisted mode, if you wish. Observe:

SET REPORTBEHAVIOR 80

Old-style:

REPORT FORM _samples + "\Solution\Reports\colors.frx" PREVIEW

New Style:

ox = NEWOBJECT("Reportlistener")
REPORT FORM _samples + "\Solution\Reports\colors.frx" OBJECT ox

But when REPORTBEHAVIOR is set to 90, the report engine will use new-style object-assisted mode when processing normal REPORT FORM syntax.

It can't process reports in object-assisted mode unless it has a reportlistener object. How does it get one when there is no OBJECT clause to give it one?

This brings us to another new system variable: _REPORTOUTPUT.

System Variable: _REPORTOUTPUT

From VFP9 Help file: "_REPORTOUTPUT Specifies the Visual FoxPro handler application providing ReportListener derived classes to be used with the REPORT FORM command, maintaining a registry of ReportListener derived classes for different output results."

Similar to _REPORTBUILDER, _REPORTOUTPUT specifies a program or application. The task of this application, however, is to instantiate and manage instances of the reportlistener class to be provided to the report engine in support of REPORTBEHAVIOR 90.

_REPORTOUTPUT specifies the Reportlistener Factory application.

Warning: If _REPORTOUTPUT is empty, then we will get an error to the effect of "Variable '_REPORTOUTPUT' is not found" which can be misleading. The variable exists, but the value is not appropriate.

The program must accept two parameters:

  • a integer value representing the type of output requested by the REPORT FORM command; and
  • a placeholder parameter that must be assigned a reference to a report listener.

The outputType property

An additional clause to the contract between report engine and listener factory is that the report listener handed back to the factory must have its OutputType property set to the same value as the type of output requested (i.e. the value of the first parameter).

outputType vs. listenerType

The report engine passes the requested output type to the listener factory program, but otherwise ignores this value. It is up to the listener object to decide how to interpret the output type, and set the listenerType property appropriately. As we saw earlier, the listenerType property will in turn affect how the report engine interacts with the report listener object during the report run.

A derived reportlistener class may choose to:

  • support multiple output types;
  • support multiple values for listenerType (i.e. multiple engine modes).

Example: A simple listener factory

Consider the following program, RL_FACTORY.PRG:

LPARAMETERS outputRequested, listenerRef
listenerRef = NEWOBJECT("Reportlistener")
listenerRef.OutputType = m.outputRequested
listenerRef.ListenerType = 0
RETURN

Note that this program returns a reportlistener with a listenerType of 0, irrespective of what output type the report engine requests.

Now, in the Command window, we can test this out:

SET REPORTBEHAVIOR 90
_REPORTOUTPUT = "rl_factory.prg"
REPORT FORM customer PREVIEW

Even though our code thinks it is asking for a report preview, the report is actually sent to the printer - because our factory program returned a base reportlistener class with a listenerType of 0.

Clearly, in practice, we'll need more sophisticated report listeners, and a more capable report listener factory. One like, say, ReportOutput.App.

Introducing ReportOutput.App

The default listener factory is provided in the form of an application, ReportOutput.App. In addition to the requirements of a listener factory described above, ReportOutput.App also provides the following:

  • Provides sophisticated user feedback during a report run
  • Provides error handling
  • Determines which FFC ReportListener class is instantiated for specific output types, using internal defaults
  • Allows the defaults to be overridden, using a registry table. (More on this later.)
  • Allows ReportListeners to be configured up-front, before they get used in response to  REPORT FORM commands
  • Caches references between REPORT FORM calls in the public collection variable _oReportOutput, using the OutputType as a key
  • Supports report chaining (the NOPAGEEJECT clause) for any output result
  • Allows you to override OutputType class defaults, or add to them, simply by adding to the collection.

ReportOutput.App's reportlistener cache

So we know that ReportOutput.App is going to create and return references to report listener classes. What is not so obvious is that it caches these instances in a PUBLIC collection. If our code performs two REPORT FORM TO PRINT commands in succession, the second command will re-use the listener instance created for the first command.

The name of this public collection is _oReportOutput. There is one listener instance for each requested output type, the collection key being the string representation of the output type for which the listener was created.

Let's see this in action. First, we initialize the environment:

CLEAR ALL
SET REPORTBEHAVIOR 90

Then we tell ReportOutput.App to initialize its collection with a "printing" listener, and configure the instance:

DO (_reportoutput) WITH 0
_oReportOutput['0'].PrintJobName = "My Report Print Job"

And now, print a report:

REPORT FORM customer.frx TO PRINT

We can interrogate the listener for interesting information:

? _oReportListener['0'].PageTotal
   4

When we print another report, the report engine will receive and use the same listener instance:

REPORT FORM fileexpr.frx TO PRINT

We should have seen the same print job name in the progress bar and it should also appear in the list of pending documents in the operating system's Printer Queue.

Do we need a ReportOutput.App?

The Visual FoxPro 9.0 report engine requires ReportOutput.App (or a suitable replacement) in order to support SET REPORTBEHAVIOR 90 and the familiar REPORT FORM commands. Our applications may not need it, if we use the REPORT FORM ... OBJECT syntax for our report run commands instead.

Remember, though, that if we wish to use the NOPAGEEJECT clause to chain report runs into a single print job, we will need to do something similar to what ReportOutput.App does with respect to caching and supplying a common reportlistener reference across multiple report runs.

A listener for every output: /FFC/_reportlistener.vcx

ReportOutput.App doesn't just serve up base reportlistener classes. It creates instances of sophisticated subclasses derived from Reportlistener, the same foundation classes that we can find in the ./FFC/_reportlistener.vcx class library.

These classes are documented in the VFP9 Help file topic ReportListener Foundation Classes.

These classes include built-in support for many features not available with a "vanilla" reportlistener:

  • support for dynamic effects
  • support for alternative output formats (HTMLListener, XMLListener)
  • user feedback during report runs (UpdateListener)
  • "chaining" of reportlisteners for multiple simultaneous output types
  • debugging report runs (DebugListener)

In examples to come, we will see how to use some of these listener classes in detail.

Aside: _reportlistener.vcx has changed in Service Pack 2. UpdateListener is deprecated, because the progress bar and user feedback is implemented differently, and is now common to all FFC listener classes derived from a common ancestor class, FxListener.

How ReportOutput.App maps "output type" to "listener class"

ReportOutput.App has an internal table that maps the requested output type (integer value) to a specified reportlistener class from the FFC/_reportlistener.vcx class library:

Output
Type
FFC Class Sets ListenerType to: Description
0 UpdateListener 0 Output is sent to printer, listener also handles user feedback during report run (progress bar, cancelling reports, etc).
1 UpdateListener 1 Output is cached for preview. Listener handles user feedback during report run.
2 reportlistener 2 base class, default behavior
3 reportlistener 3 base class, default behavior
4 XMLListener -1 No normal rendering, listener builds XML tree as the engine processes the report layout and traverses the data set.
5 HTMLListener -1 listener builds XML tree as above, then uses XSLT to create HTML output.
999 DebugListener 999 No normal rendering. Generates a report processing log for debugging.

For more information on ReportOutput.App

See the following help topic in the Visual FoxPro 9.0 help file: Understanding the Report Output application

New Syntax: REPORT FORM .. OBJECT TYPE

Now that we understand the role that _REPORTOUTPUT and ReportOutput.App play in the report engine's support of report command syntax, it is a good time to consider some more new syntax: REPORT FORM .. OBJECT TYPE n.

This syntax was introduced into the product because - once the derived reportlistener classes in the FFC made alternative output types such as HTML and XML possible - syntax of the form REPORT FORM TO HTML and REPORT FORM TO XML seemed to be required. This would have been shortsighted because there are many more output types that are possible (Word or Excel, anyone?). Instead, the addition of the TYPE n clause to the existing REPORT FORM OBJECT command was implementationally efficient, providing an integer value can be mapped to a given output type.

Consider the following command to generate HTML output from a report:

REPORT FORM customer.frx OBJECT TYPE 5

Here's how the command is processed by VFP9:

  • The report engine invokes _REPORTOUTPUT to ask for an instance of the listener class mapped to output type 5
  • ReportOutput.App looks in its cache (_oReportOutput) for an instance of HTMLListener, and instantiates it if necessary. It then sets its outputType property to 5 and returns it to the report engine.
  • The report engine runs the report in object-assisted mode, using the received listener object reference.

Previewing Reports in object-assisted reporting

Consider the following command to display a report preview:

SET REPORTBEHAVIOR 90
REPORT FORM customer.frx PREVIEW

Here's how the command is processed by VFP9:

  • The report engine invokes _REPORTOUTPUT to ask for an instance of the listener class mapped to output type 1
  • ReportOutput.App looks in its cache (_oReportOutput) for a listener instance assigned to output type 1 (an UpdateListener by default), and instantiates it if necessary. It then sets its outputType property to 1.
  • UpdateListener understands that in order to service an outputType of 1, it must configure its own listenerType to 1.
  • The report engine runs the report in object-assisted mode, using the returned reference to the UpdateListener instance, caching all rendered report pages as per a listenerType of 1.
  • When the report run is completed, the report engine checks to see if the report listener has a valid object reference in its PreviewContainer property. If this is the first time a report preview has been executed, it probably doesn't have one.

  • The report engine invokes _REPORTPREVIEW to ask for an instance of a preview container class.
  • The application specified by _REPORTPREVIEW (ReportPreview.App, by default) instantiates a Visual FoxPro class and returns the reference to the report engine.
  • The report engine assigns the object reference to the listener's PreviewContainer property.
  • The report engine invokes ReportListener.PreviewContainer.SetReport(), passing it a reference to the listener object itself. *
  • The report engine invokes ReportListener.PreviewContainer.Show(). **

* If the method exists. The preview container object doesn't have to have a SetReport() method.
** Or it might in fact call .Show(1) if in a program and the NOWAIT clause was not used.

This lengthy sequence of steps has introduced a new system variable: _REPORTPREVIEW.

System Variable: _REPORTPREVIEW

From the VFP9 Help file: "_REPORTPREVIEW Specifies the application used by Visual FoxPro to generate instances of a report preview user interface in order to satisfy requests from instances of the ReportListener class."

Similar to _REPORTBUILDER and _REPORTOUTPUT, _REPORTPREVIEW specifies a program or application.

The task of this application is to instantiate a class — a preview container — that can be used to display a preview of the report, using the OutputPage() method of a report listener, along with the cached rendered pages it manages.

_REPORTPREVIEW specifies the Report Preview UI Factory application.

Warning: If _REPORTPREVIEW is empty, then we will not encounter an error. A report preview window will simply not appear, as though there was no data to report on. This is just the way it works.

The program must accept a single parameter:

  • a placeholder parameter that must be assigned a reference to a Visual FoxPro class.

The Preview Container API

In order to function as a useful preview window, the object returned from _REPORTPREVIEW must support the Preview Container API. I've discussed this in more detail in another article.

Introducing ReportPreview.App

The default preview container factory is provided in the form of an application, ReportPreview.App. In addition to the core requirements of a preview container described above, the default preview container class returned by ReportPreview.App also features:

  • a multi-page display of report pages;
  • properties that control its appearance;
  • a comprehensive extension API for plugging in your own custom functionality.

How the default preview container uses the resource file

In previous versions of Visual FoxPro, the report preview window remembers position and layout settings in the resource file, if SET RESOURCE ON. The default preview container in VFP9.0 also remembers layout settings in the resource file. These records can be identified by the following column values:

Column Value
TYPE PREFW
ID 9REPPREVIEW
NAME <name of report file>

Note: In Service Pack 2, due to a slight change in how the DATA column is written, the ID for preview container preference records has been changed to "92REPREVIEW".

Example: Controlling the default Preview Container

Although the report engine will call _REPORTPREVIEW to obtain a default preview container automatically, there is nothing preventing us from giving a reportlistener a customized preview container up-front. The default preview container exposes a number of properties that we can tweak.

First, let's initialize the ReportOutput.App's listener collection, and set up a "preview" listener (that's output type 1):

CLEAR ALL
DO (_reportoutput) with 1

Now we can obtain an instance of the default preview container:

pc = .NULL.
DO (_reportpreview) WITH m.pc

Pre-set some of  its properties:

pc.TopForm = .T.
pc.Caption = "My Application Preview"
pc.ZoomLevel = 3 && 50%
pc.CanvasCount = 2
pc.Top = 100
pc.Left = 100

And now, give it to the preview listener:

_oReportOutput['1'].PreviewContainer = m.pc

And run a report preview:

REPORT FORM customer PREVIEW

 We should see our customized preview window.

Example: Pre-setting the default preview for your applications

We can also make our preferences the default by "wrapping" the call to ReportPreview.App with the code we wrote above. Consider the program, PRESET_PC.PRG:

LPARAMETER pc

pc = .NULL.

DO (HOME() + "ReportPreview.App") WITH m.pc

pc.TopForm = .T.
pc.Caption = "Hello Developers!"
pc.ZoomLevel = 3 && 50%
pc.CanvasCount = 2
pc.Top = 100
pc.Left = 100

RETURN

Now we just need to assign this program to _REPORTPREVIEW:

_reportpreview = "preset_pc.prg"

Example: Successors in the FFC listeners

One thing that all FFC listener classes share is the ability to operate in a "chain of responsibility" with other FFC report listeners. A listener can be configured as the successor of another listener. This is because support for successors is built-in to their common ancestor class _reportlistener.

Consider the following program:

PRIVATE oRlMaster, oRlHtml

STORE .NULL. TO ;
oRlMaster, oRlHtml

DO (_reportoutput) WITH 5, m.oRlHtml
DO (_reportoutput) WITH 1, m.oRlMaster

oRlHtml.QuietMode = .T.
oRlMaster.Successor = m.oRlHtml

REPORT FORM customer OBJECT m.oRlMaster

LOCAL x
x = "RUN " + oRlHtml.targetFileName
&x

Here we are using the Successor property of the primary listener (responsibility: Previewing) to attach a secondary listener (responsibility: HTML output) to the report run.

The secondary listener will have its methods invoked immediately after each of the primary listeners methods are called by the report engine.

There are some limitations here. Any "native" base class behavior (such as sending rendered pages to the printer automatically) must be performed by the primary listener.

We have another good example of successors coming up later on.

Summary

And that concludes our journey through the outsourced reporting components of Visual FoxPro 9.0. The basic points to take away from this are:

  • The Report Engine is responsible for traversing the data being reported on, and initially positioning the layout elements on the rendered page.
  • The ReportListener base class performs the object rendering and printing.
  • Classes derived from ReportListener can produce alternative types of output such as XML or HTML, or other user-defined functionality.