Extending the Report Designer in VFP9 with Report Builders


 

This article series is based on a session I gave at the Advisor Visual FoxPro DevCon in June 2005, in which I described the architecture and extensibility features of the new Report Designer [1] in Visual FoxPro 9.0, and demonstrated the basic techniques that it uses "behind the scenes".

In this article, I introduce the Report Builder API and show you how to construct your own simple builder applications. I also introduce the default ReportBuilder application and describe some of its features.

In case you hadn't realised, the dialog boxes in the Report Designer are actually implemented in a FoxPro application, with a powerful framework behind it that yields tons of opportunities for extension.

The Report Builder API

But first, before I discuss extensibility, I'm going to talk about the underlying architecture in VFP9 that makes all this possible. Visual FoxPro 9.0 introduces a feature which I like to call the Report Builder API. It consists of the following:

  • a new system variable, _REPORTBUILDER, which specifies a Report Builder program or application;
  • 19 different events that occur at various times in the Report Designer;
  • 4 parameters passed from the Report Designer to the builder application during an event;
  • 2 binary flags returned from the builder to the Report Designer.

Don't worry, I'll take you through the process in detail:

What happens during a Report Designer event

The Report Designer:

  • Checks for a valid program assigned to _REPORTBUILDER
  • Creates a private data session
  • Buffers the underlying source table of the report currently being edited as a cursor in the new data session with the alias "FRX" [2]
  • Locates the record pointer on the appropriate record in the FRX cursor
  • invokes (_REPORTBUILDER) with 4 parameters.

Let's examine each of these parameters in detail:

1st parameter: returnFlags

A numeric value of -1 is passed (by reference) to the report builder program as a placeholder for returning values back to the Report Designer. I'll discuss the possible return values in detail later on.

2nd parameter: eventType

This parameter contains a numeric value that indicates what particular event has occurred: Was a properties dialog box invoked? A selection from the menu? Was a report layout opened for editing? The values are documented in the help file[msdn] and also in the FFC\foxpro_reporting.h header file:

*-- FRX Report Builder event types
#define FRX_BLDR_EVENT_PROPERTIES   1
#define FRX_BLDR_EVENT_OBJECTCREATE 2
#define FRX_BLDR_EVENT_OBJECTREMOVE 4
#define FRX_BLDR_EVENT_OBJECTPASTE  5
:

3rd parameter: commandClauses

This parameter is a reference to an object. The object is actually an instance of the Empty class, with additional properties added to it that indicate what clauses were used on the original FoxPro command line that launched the Report Designer, among other things. The exact properties are documented in the help file[msdn], but you can also see them listed in the Event Inspector (see below). As you'll see, having these clauses available to your program is very useful.

4th parameter: designerSessionId

This parameter indicates the ID of the data session that the Report Designer is running in. This allows the report builder application to switch back and forth between data sessions so as to interrogate any data tables open in the report deisgner session - either opened manually or automatically by the report's Data Environment.

This process is documented quite well in the help file[msdn].

What must the builder process do?

Due to the call and response mechanism of Report Designer events, the report builder process must be a modal one. It has all the information it requires in order to read from the FRX cursor, interrogate the environment of the Report Desginer, take action based on the parameters passed to it, and make changes to the FRX cursor. It then terminates, returning control to the Report Designer.

At that point, the Report Designer needs to know two things:

  • Has the report builder process handled the event, or should the native, original behavior be allowed to take place?
  • Does the report layout need to be refreshed from changes made by the builder program to the FRX cursor?

The Report Designer can get this information by looking at the returnFlags parameter, which - being passed by reference - can have its value set by the report builder program.

About that returnFlags parameter

The Report Designer assumes that the value placed in returnFlags by the builder program is a sum of two individual binary flags:

Flag Bits Similar to Instruction to Designer
1 01 NODEFAULT Suppress the native action - if any - that would normally be associated with the designer event.
2 10 REFRESH Respect changes made to the buffered FRX cursor by re-loading the report layout

Some examples:

returnFlags = 0 = 000

Neither flag is set, so the behavior of the Report Designer will be the same as if there was no report builder program assigned to _REPORTBUILDER. In other words, any changes made to the FRX cursor will be ignored, and the Report Designer will behave as in VFP 8.0 and earlier versions.

returnFlags = 3 = 011 = 1+2

Both flags are set, so the Report Designer will refresh the report layout in the design window, reloading any changes made to the FRX cursor. Also, if there is any "native" behavior attached to the event that occurred, the Report Designer will prevent it from taking place.

This is best understood by looking at a simple example that allows you to choose the values of the return flags.

Example: SimpleBuilder

Consider the following program:

PARAMETERS returnFlags, eventType, commandClauses, designerSessionId
WAIT WINDOW ;
	"eventType = "   + TRANSFORM( eventType ) + chr(13) + ;
	"Data session = " + TRANSFORM( SET("DATASESSION")) + chr(13) + ;
	"ALIAS() = "     + alias() + chr(13) + ;
	"RECNO() = "     + TRANSFORM( recno("frx")) + chr(13) + ;
	"frx.OBJTYPE = "     + TRANSFORM( frx.OBJTYPE) + chr(13) + ;
	"frx.OBJCODE = "     + TRANSFORM( frx.OBJCODE) 
returnFlags = 0
return

This example performs a WAIT WINDOW command, listing certain interesting values at each Designer event that occurs. Save this program as, say, SimpleBuilder.prg and assign it to _REPORTBUILDER:

_REPORTBUILDER = "SimpleBuilder.prg"
CREATE REPORT

Now you should see as you click around the Report Designer, that each event is high-lighted with your little WAIT WINDOW. Try right-clicking and selecting Properties... Try selecting Variables... from the Report menu.

Aside: ReportBuilder's Event Inspector Feature

You can get an even more comprehensive view of a Designer event by using the Event Inspector feature of the default Report Builder. The Event Inspector can be accessed from any Report Builder dialog's right-click context menu:

All the information available to the builder application is laid out for your review in a messagebox:

As you'll see shortly, the default builder doesn't respond to every possible event, so sometimes it is handy to use a simple builder like the one shown in the example above.

Example: EventAlert

Consider the following program:

PARAMETERS returnFlags, eventType, commandClauses, designerSessionId
IF MESSAGEBOX( ;
	"Event: "   + TRANSFORM( eventType ) + chr(13) + ;
	"RECNO('frx') " + TRANSFORM( RECNO("frx")) + CHR(13) + ;
	"frx.OBJTYPE: " + TRANSFORM( frx.OBJTYPE) + ;
	chr(13) + chr(13) + "Allow this event?"  ,4)=6
	
    returnFlags = 0    && clear both flags, allow default action to proceed
ELSE
    returnFlags = BITSET( m.returnFlags, 0 ) && NODEFAULT : suppress event
ENDIF
RETURN

This example demonstrates the effect of changing the "NODEFAULT" return flag, using a simple MESSAGEBOX() Yes/No call. Try it out in the Command window:

_REPORTBUILDER = "EventAlert.prg"
MODIFY REPORT _samples + "\Solution\Reports\colors.frx"

You will see the report builder program responding to the first event triggered during a report design session - which happens to be the "report open" event. What will happen if you select "No" to tell the Report Designer to suppress its native behavior?

If you thought that the Report Designer would close, don't feel bad. It's a trick question. There doesn't happen to be any native Designer behavior associated with the "report open" event. You can't prevent the report from being opened for editing. [3]

Now select Close from the File menu to close the Report Designer window. An event type of 8 is triggered. What will happen if you choose "no" this time? Well, it turns out that you can prevent the Report Designer from closing if you set the NODEFAULT return flag in response to event type 8.

Now select Optional Bands... from the Report menu. An event type of 11 is triggered. You can prevent the Optional Bands dialog from appearing if you set the NODEFAULT return flag.

You have now observed the following:

  • Report builder programs can prevent the Report Designer from responding to certain actions taken by the user during a report design session.
  • Not all actions can be suppressed - some events do not have associated suppressible actions.

Example: TemplateChooser

Consider the following program:

PARAMETERS returnFlags, eventType, commandClauses, designerSessionId
returnFlags = 0
IF eventType = 7 AND commandClauses.IsCreate
    IF MESSAGEBOX("Do you want to use a template?",4)=6
        LOCAL cReportFile
        cReportFile = GETFILE("FRX", "Template Report:")
        IF NOT EMPTY( m.cReportFile )
    	    SELECT frx
     	    SET SAFETY OFF
        	ZAP
     	    SET SAFETY ON
            APPEND FROM (m.cReportFile)
            returnFlags = BITSET( m.returnFlags, 1 ) && refresh layout
        ENDIF
    ENDIF
ENDIF
RETURN

First, notice that this example takes no action unless a report layout has just been opened for editing (event type = 7) and it is a newly created layout (commandClauses.IsCreate = true). The program filters the kinds of designer events that it will respond to. If these conditions are met, the program prompts you to select an existing FRX file that it then copies in to the new layout, using standard syntax (ZAP, USE, APPEND FROM, etc). With the FRX layout exposed as a read-write cursor, there is nothing stopping you from wreaking terrible vengence on the contents, including ZAP and APPEND FROM.

It really is that simple. All you have to remember to do is set the "REFRESH" bit (returnFlags parameter to 2 to ensure that the Report Designer respects the changes made to the FRX cursor - in this case, a wholesale replacement of the contents! Try it out:

_REPORTBUILDER = "TemplateChooser.prg"
CREATE REPORT

If you choose a source report that contains members in its Data Environment, you will notice that this process duplicates these as well.

Introducing ReportBuilder.App - the default builder

Visual FoxPro 9.0 includes a report builder application as part of the default installation. By default, _REPORTBUILDER should be set appropriately:

_REPORTBUILDER = HOME() + "ReportBuilder.App"

ReportBuilder Features

Integrates and provides the user interface

The ReportBuilder application integrates with the Visual FoxPro Report Designer using the same report builder API that you explored in the examples above. It replaces the native dialog boxes with alternate versions, implemented in FoxPro itself. As well as being more aesthetically pleasing and ergonomically designed, these new dialogs expose additional functionality that is not available in the native dialog boxes.

Redistributable

Along with the other component reporting applications [4], ReportBuilder.App is redistributable with your applications. You also get the source to ReportBuilder.App in the XSOURCE.ZIP file located in the Tools\ subdirectory in Visual FoxPro's home directory.

Data-driven

Just like the Form and Class Wizards and Builders in previous versions of Visual FoxPro, the ReportBuilder application uses a table of class definitions mapped to specific Report Designer events. This "event handler registry" table is stored as a read-only lookup resource inside the compiled ReportBuilder application.

In normal operation, after being invoked in response to an event in the Report Designer, the ReportBuilder application looks at the currently selected record in the FRX cursor; and uses the OBJCODE and OBJTYPE values together with the event type parameter, and performs a lookup operation in its registry table to obtain the name of a class to instantiate to handle the event.

Feature: String Trimming Mode

Speaking of additional functionality, the ability to specify the string trimming mode on Field/Expression layout elements is new to VFP9. It is possible because in VFP9 report printing and previewing uses GDI+ graphics API instead of the older GDI API used in previous versions of FoxPro [5]. Let's take a look a report in VFP 8.0:

This report (fileexpr.frx) shows the results of some common FoxPro functions. However, the Field/Expression control used on the right is not big enough to contain the full text of the evaluated result. As you can see, the expression is truncated, with no clue that you are not seeing the full picture.

GDI+ allows several different ways that a truncated string can be expressed. The ReportBuilder application exposes these settings in the Format tab of the new Field/Expression Properties dialog box:

You might expect the default mode to be "Trim to nearest word" so as to ensure backward-compatibility with earlier versions of FoxPro. It's not. "Default Trimming" actually means "Use whatever the default string trimming mode of the ReportListener class that is processing the report". And, by default, that is "Trim to nearest word, append ellipsis":

The decision to use this particular default mode was made mainly because GDI+ requires a little additional width to render text (MSDN includes a good article[msdn] on why this is so), and as a consequence, any reports designed with tight space constraints in previous versions of FoxPro might well experience text expression truncation in VFP9. The visible indication of the truncation is desirable because otherwise it would be easy to miss that it happens at all, unless you were very familiar with the data being displayed.

I chose this report because it is also ideal for showing one of the more interesting string trimming modes available under GDI+. See what happens when you change the trim mode of the field/expression control to "Filespec: show inner path as ellipsis":

Now both the head and tail of the text expression is shown. This works with any string, not just file specifications.

Manually invoking the ReportBuilder application

ReportBuilder.App is normally called automatically from inside the Report Designer via the _REPORTBUILDER system variable. But there are some features you may not be aware of that are available through the command line.

Browsing the FRX

As a developer, you may encounter complex report layouts that don't work the way you expect, and have to resort to browsing the .frx source table in order to debug the problem. The ReportBuilder includes a nice FRX browser dialog for this purpose:

DO (_REPORTBUILDER) WITH 2, _samples + "\Solution\Reports\colors.frx"

The second parameter is optional. If you leave it out, you will get prompted with a file chooser dialog box. The FRX Table Browser dialog displays the contents of the FRX file in a nice layout with the memo fields broken out for easy viewing:

If you are not familiar with the structure of the FRX source table, you should note that:

  • Each report element, including columns, bands (title, header, detail, footer, etc) is represented by a separate record in the table.
  • Each report element type is identified by the values in the OBJTYPE and OBJCODE columns.
  • If a report layout element is currently "selected" in the Report Designer, the corresponding record will have the CURPOS field set TRUE.

Accessing the ReportBuilder Options dialog box

The ReportBuilder Options dialog box is accessible from any report builder dialog by using the right-click context menu:

If you do not happen to be editing a report, you can get there more directly running the ReportBuilder application in the Command window:

DO (_REPORTBUILDER) WITH 1
  * or
DO (_REPORTBUILDER)

The parameter switch is actually optional, you get the Options dialog if you omit it.

From here you can configure how the Reportbuilder operates. I won't say much about the other settings at this time, because I really want to talk about the different ways ReportBuilder responds to Report Designer events.

Other command-line parameters

The following table documents all of the available command-line parameters that the ReportBuilder application exposes:

DO ReportBuilder.app WITH ... Result
[ 1 ] Display the ReportBuilder Options dialog.
2 [ , <frx file> ] Open an FRX source file for browsing in the FRX Browser dialog.
3 [ , <dbf file> ] Set ReportBuilder to use a specific external handler registry table.
4, n Sets the ReportBuilder event handle mode to a specific option, e.g. n=1: Normal, n=2: debug, n=3: Event Inspector, n=4: ignore events.
5 [ , <dbf file> ] Copy the internal handler registry table out to a specific external filename.
6 Add a "Report Builder Options" menu bar to the Tools menu.
7 Display the Event Handler Registry Explorer window in order to browse the currently assigned handler table.
8 [ , <frx file> ] Open an FRX source file for browsing in the "Report Bands & Objects" dialog box.
0 Instantiate an instance of the frxEvent class into a public variable _oReportBuilder.

The ReportBuilder's Event Handle Mode

In addition to the "normal" mode of looking up a handler class in its registry table, the ReportBuilder application has three other modes of handling events:

Handle Mode: Debug

In this mode, the ReportBuilder application presents a special "debug" dialog. You can choose how to set the returnFlags parameter for every Report Designer event.

Handle Mode: Event Inspector

I described the ReportBuilder's Event Inspector earlier. In this mode, the Event Inspector window is displayed for every Report Designer event, similar to the Debug handler, although as it is a MESSAGEBOX dialog, you obviously have no choice about the return flags.

Handle Mode: Ignore builder events completely

In this mode, the ReportBuilder application lets every event pass through unhandled, as though it were not assigned to _REPORTBUILDER at all.

Aside: Persistence of ReportBuilder settings

Because it is a modal process, the ReportBuilder application saves its settings in a public variable - actually a member object of _SCREEN called "ReportBuilderData":

? _SCREEN.ReportBuilderData.Get("handlemode")

This object is created the first time ReportBuilder.app is invoked. It does not survive a CLEAR ALL command.

The ReportBuilder application does not save its settings between sessions of Visual FoxPro.

Other ReportBuilder application features

With the default ReportBuilder application plugged in to the Report Designer, you get the following additional features:

  • You can edit the contents of the USER field in the FRX source table
  • You can store and edit structured meta data with each object
  • You can copy a data environment from an existing report
  • You can link a report to a visual data environment class
  • There's a new Multiple selection dialog
  • You are able to configure Protection flags[msdn]
  • You can assign "design-time" captions to field/expression objects
  • You can assign Tooltops to any layout element
  • You have greater control over the precise location of report layout elements

I hope I've given you some insight into the architecture of report builders in Visual FoxPro 9.0, and maybe even given you some incentive to try out some report builders of your own design.

You can download a zip file containing the source for this article here: ReportBuilder_extend.zip


Footnotes:

[1] From now on, if I use the words "Report Designer", please feel free to interpret them as "Report Designer or Label Designer".

[2] Even if you are editing a Label layout (source file with an .LBX extension), the buffered layout still has the alias "FRX".

[3] This is why "NODEFAULT" is a good name for this flag. You can specify NODEFAULT in any method you like in a FoxPro class, but that doesn't mean that there is always going to be something for it to have an effect on.

[4] There are three of them: ReportBuilder.App, ReportPreview.App, and ReportOutput.App.

[5] If you SET REPORTBEHAVIOR 80, you can print and preview using the older GDI api in Visual FoxPro 9.0 as well. This backwards-compatible mode has some advantages (such as speed) but any advanced formatting features (such as string trimming mode) won't be available, of course.


Acknowledgements:

Thanks to Lisa Slater Nicholls and Richard Stanton, without whom the Reporting System in VFP9.0 in its current form would not have existed.