VFP 9.0 Reporting System Fundamentals
By Colin Nicholls
This article was first made available as session notes to OzFox 2007 attendees.
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. Object AssistedPrior 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. OutsourcingIn Visual FoxPro 9.0, the reporting engine outsources some fairly major portions of functionality out to FoxPro applications:
New system variablesBecause 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 TimeIn 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: _REPORTBUILDERWhen 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:
What happens during a Report Designer Event
The Report Designer:
Report Builder ParametersLet's examine each of these parameters in detail.
returnFlagsA 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. eventTypeThis 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 commandClauses
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. designerSessionIdThis 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 doDue 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:
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 parameterThe Report Designer assumes that the value placed in returnFlags by the builder program is a sum of two individual binary flags:
returnFlags = 0 = 000Neither 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 + 2Both 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. Example: SimpleBuilderConsider the following program: PARAMETERS returnFlags, eventType, commandClauses,
designerSessionId 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 InspectorWe 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. Example: EventAlertConsider the following program: PARAMETERS returnFlags, eventType, commandClauses,
designerSessionId "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"
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:
Example: TemplateChooserConsider the following program:
PARAMETERS returnFlags, eventType, commandClauses, designerSessionId 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. Introducing ReportBuilder.AppVFP 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. It's redistributableAlong 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 tableJust 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 functionalityString Trimming modeSpeaking 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
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 structureAs 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 tableIf you are not familiar with the structure of the FRX source table, you should note that:
Accessing the ReportBuilder Options dialogThe 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 modeIn 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: DebugIn 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 InspectorWe 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 completelyIn this mode, the ReportBuilder application lets every event pass through unhandled, as though it were not assigned to _REPORTBUILDER at all. AsideBecause 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": _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.App featuresWith the default ReportBuilder application plugged in to the Report Designer, we get the following additional features:
How ReportBuilder.App uses the resource fileThe 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:
This article continues in Part Two... |