By Colin Nicholls
One important thing about Visual FoxPro 9.0 is that it is the first version in which fundamental components of the reporting system have been implemented in the FoxPro language itself. FoxPro has always featured an extensible development environment, and has always relied on Xbase components, ever since GENMENU.PRG and its brethren. GENMENU at least is still used today. Up until version 9.0, the reporting engine in FoxPro has always been a "black box", implemented in Visual C, with the internals hidden from view. As a result of the 9.0 re-write (in Visual C++ and FoxPro), a new array of techniques are available to the developer to create new kinds of report output.
Prior to the commencement of the beta program for VFP 9.0, a rumor hit the user forums that Microsoft would be over-hauling the Reporting System in VFP, and developers would finally get what they've wanted since version 3.0: an object-oriented report designer.
Almost inevitably, no two developers seem to have the same idea of how an object-oriented report engine would behave, once you get past the initial specification of "you know, it's like the current report designer, only with, like, objects", and into the devilish details of implementation.
Microsoft did not provide an object-oriented reporting system in VFP 9.0. Instead they provided maximum functionality and extensibility, in a hybrid system that gives developers all the power and benefits of object-orientation that they want, along with the all the backwards-compatibility that they really need.
This hybrid system is what we call object-assisted reporting. And hopefully, by the end of this article, you'll understand why that's an apt term, and exactly how it works.
In Visual FoxPro 9.0, the reporting engine outsources some fairly major portions of functionality out to FoxPro applications:
- ReportBuilder.App handles the user interface of the Report Designer Properties dialogs;
- ReportPreview.App handles the user interface of the Report Designer's Preview Window;
- ReportOutput.App operates to support and allow simple and straight-forward command syntax to invoke the power of the object—assisted report engine.
New system variables
Because it's FoxPro, we are able to replace the default components with our own custom applications, if we should decide we need to. We can do this because each application is specified via its own special system variable:
In this whitepaper, we will cover how the object-assisted reporting system performs a report run. But first, let's take a look at how the Report Designer and Report Builder application work together.
1. The Reporting System at Design Time
In VFP9, the Report Designer is mostly unchanged from VFP 8.0. Menu options have been moved around a bit, and there are some cosmetic changes such as resizing handles appearing when the mouse pointer moves over them, but essentially it is the same design surface.
The biggest change is that it will delegate the task of displaying dialog boxes to an external application, if one has been specified in the _REPORTBUILDER system variable. This allowed Microsoft to develop completely new Properties dialogs for the Report Designer, with more functionality than they would otherwise have been able to do with the resources available.
System Variable: _REPORTBUILDER
When we double-click on a report layout element, or select an option from the Report menu, the Report Designer looks at _REPORTBUILDER and, if there is a program or application specified, calls out to that program to service the event.
It is important to understand that if _REPORTBUILDER is empty, then the Report Designer will display the familiar "old-style" native dialogs.
Any program assigned to _REPORTBUILDER must support the Report Builder API. We can refer to them as "report builder" applications.
It is important to understand that the "old-style" dialogs are still available.
What is the Report Builder API?
The Report Builder API is a contract between the Report Designer and a program or application assigned to _REPORTBUILDER. It consists of:
- 19 different points at which the Report Designer will invoke the report builder;
- 4 parameters passed to the report builder during each event
- 2 binary flags returned from the builder to the Report Designer so that it can take appropriate action.
What happens during a Report Designer Event
The Report Designer:
- checks for a valid program or application specified by _REPORTBUILDER;
- creates a private data session;
- buffers the underlying source table of the report currently being edited as a cursor open in the new data session with the alias "FRX";
- locates the record pointer on the appropriate record in the FRX cursor;
- invokes (_REPORTBUILDER) with 4 parameters.
Report Builder Parameters
Let's examine each of these parameters in detail.
A numeric value of -1 is passed (by reference) to the report builder program as a placeholder for returning values back the Report Designer. More about this later.
This parameter contains a numeric value that specifies what particular event has occurred: Was it a properties dialog box? A selection from the menu? Was a report layout opened for editing in the Designer? The possible values of this parameter are documented in the Help file 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
This parameter is an object reference. The object is actually an instance of the FoxPro base class Empty, 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. As we'll see, having these clauses available to our programs is very useful.
This parameter specifies the session 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 Designer session - either opened manually or automatically by the report's Data Environment.
What a report builder must do
Due to the call and response mechanism of Report Designer builder 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 Designer, 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.|
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 process is best understood by looking at a simple example that allows us to choose the values of the return flags.
Consider the following program:
PARAMETERS returnFlags, eventType, commandClauses,
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
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"
Now we should see as you click around the Report Designer, that each event is high-lighted with our little WAIT WINDOW. Try right-clicking and selecting Properties... Try selecting Variables... from the Report menu.
Aside: ReportBuilder.App's Event Inspector
We 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 our review in a messagebox:
As we'll see shortly, the default builder, ReportBuilder.App, 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.
Consider the following program:
PARAMETERS returnFlags, eventType, commandClauses,
IF MESSAGEBOX( ;
"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
returnFlags = BITSET( m.returnFlags, 0 ) && NODEFAULT : suppress event
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"
We 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 we 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. We can't prevent the report from being opened for editing.
Now select Close from the File menu to close the Report Designer window. An event type of 8 is triggered. What will happen if we choose "no" this time? Well, it turns out that we can prevent the Report Designer from closing if we 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. We can prevent the Optional Bands dialog from appearing if we set the NODEFAULT return flag.
We 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.
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
cReportFile = GETFILE("FRX", "Template Report:")
IF NOT EMPTY( m.cReportFile )
SET SAFETY OFF
SET SAFETY ON
APPEND FROM (m.cReportFile)
returnFlags = BITSET( m.returnFlags, 1 ) && refresh layout
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 vengeance on the contents, including ZAP and APPEND FROM.
It really is that simple. All we 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"
If you choose a source report that contains members in its Data Environment, you will notice that this process duplicates these as well.
VFP 9.0 ships with a default report builder application: ReportBuilder.App. By default, the _REPORTBUILDER system variable should be set accordingly:
_reportbuilder = HOME()+"ReportBuilder.App"
The ReportBuilder application integrates with the Visual FoxPro Report Designer using the same report builder API that we 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.
Along with the other component reporting applications, ReportBuilder.App is redistributable with our applications. We also get the source to ReportBuilder.App in the XSOURCE.ZIP file located in the Tools\ subdirectory in Visual FoxPro's home directory.
It's data-driven by a configuration table
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.
Additional ReportBuilder.App functionality
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.
If we SET REPORTBEHAVIOR 80, we can print and preview using the older GDI API in Visual FoxPro 9.0 as well. This non-object-assisted mode has some advantages (such as speed) but any advanced formatting features (such as string trimming mode) won't be available, of course.
Let's take a look a report as it would look VFP 8.0:
SET REPORTBEHAVIOR 80
REPORT FORM fileexpr.frx PREVIEW
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 we can see, the expression is truncated "to nearest word", with no clue that we are not seeing the full text.
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.
We 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 we 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!
Browsing the FRX structure
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"
TIP: ReportBuilder.App has many command-line parameters. See Appendix A for a complete list (as of SP2).
Recap: The structure of the FRX table
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
The ReportBuilder Options dialog box is accessible from any ReportBuilder dialog by using the right-click context menu. Alternatively, if we do not happen to be editing a report layout, we can get there directly by running the ReportBuilder.App from the Command window:
DO (_reportbuilder) [ WITH 1 ]
The parameter 1 is optional.
From here we can configure how the ReportBuilder.App operates.
TIP: The ReportBuilder's Options dialog is not available when a report is being edited in PROTECTED mode.
The ReportBuilder's Event Handling 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. We can choose how to set the returnFlags parameter for every Report Designer event.
Handle Mode: Event Inspector
We 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.
Because it is a modal process, the ReportBuilder application saves its settings between invocations in a public variable - actually a member object of _SCREEN called "ReportBuilderData":
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.App features
With the default ReportBuilder application plugged in to the Report Designer, we get the following additional features:
- We can edit the contents of the USER field in the FRX source table
- We can store and edit structured meta data with each object
- We can copy a data environment from an existing report
- We can link a report to a visual data environment class
- There's a new Multiple selection dialog
- We are able to configure Protection flags
- We can assign "design-time" captions to field/expression objects
- We can assign Tooltips to any layout element
- We have greater control over the precise location of report layout elements
How ReportBuilder.App uses the resource file
The ReportBuilder.App saves selected window layout and preference information in the resource file, if SET RESOURCE ON. These records can be identified by the following column values:
|PREFW||9REPORTBDLR||RegistryExplorer||Size and position of the Configuration table browser.|
|PREFW||9REPORTBDLR||MemoEditor||Size, position and font of the text editor window.|
|PREFW||9REPORTBDLR||MetaBrowser||Size and position of the memberdata browser.|
|PREFW||9REPORTBDLR||RuntimeExtensionEditor||Size, position and font of the XML editor window.|
|PREFW||9REPORTBDLR||FrxBrowser||Size and position of the FRX browser.|
This article continues in Part Two...