VFP9 Reporting Design Time
VFP9 Reporting Design Time
Doug Hennig
Stonefield Software Inc.
1112 Winnipeg Street, Suite 200
Regina, SK Canada S4R 1J6
Voice: 306-586-3341
Fax: 306-586-5080
Email: [email protected]
Web: www.stonefield.com
Web: www.stonefieldquery.com
Overview
Among the new and improved features in the reporting system of VFP 9 is the ability to extend
the Report Designer to provide easier-to-use, more powerful, and more flexible report writing to
your development team and even your end-users. In this document, you will learn about the new
Report Builder application, how it captures and handles events raised by the Report Designer,
and how you can create your own handlers to extend the VFP Report Designer in ways you never
thought possible.
Introduction
One of the biggest changes in VFP 9 is the incredible improvements made in the reporting
system. There are several aspects to this, one of which we’ll explore in this document: the ability
to extend the Report Designer.
The VFP development team had several goals in mind when they worked on the design-time
improvements, including:
Simplifying and improving the UI. In VFP 8 and earlier, there were a lot of dialogs related to
reporting. Some of them had somewhat unusual interfaces, and some spawned yet other
dialogs. You can see an example of the clunky interface when you double-click on a text
object: that action brings up a properties dialog for the object, but that dialog doesn’t allow
you to change the font or color of the object. In VFP 9, there are only a few dialogs, because
all of the properties for an object are now in one place.
New features. Features such as report and object protection, DataEnvironment reuse, absolute
positioning, and design-time labels are now available in VFP 9.
Extendible. VFP has always had a very extendible IDE, but VFP 9 blows the lid off
extensibility! You can now replace some or all Report Designer dialogs and completely
change both the UI and behavior of report events if you wish.
The Report Designer passes the following parameters to the report builder application when an
event occurs:
Parameter Type Description
ReturnFlags N Passed by reference with an initial value of -1. Used to return values to the
Report Designer.
CommandClauses O An Empty object with properties indicating what clauses were used in the
CREATE/MODIFY REPORT command.
ReturnFlags is used to return values back to the Report Designer. The possible return values are
shown in the table below, along with constants representing these values that may be defined in
FOXPRO.H in the release version of VFP 9. These values are bit flags that can be summed if
desired.
EventType contains a value identifying the event that occurred. The possible values, along with
constants representing them and the type of event (a report event, an object event, or a band
event), are shown in the following table. The table also indicates whether the event can be
suppressed by adding 1 to the ReturnFlags parameter. “Must delete record” means that since a
newly-created object’s record has already been added to the FRX, to suppress the creation of an
object, you must delete its record in the FRX cursor and set the ReturnFlags parameter to 3 (the
event was handled and changes should be reloaded).
AddTableToDE L .T. if the Add Table to DataEnvironment option was turned on in the Quick Report
dialog.
Alias L .T. if the Add Alias option was turned on in the Quick Report dialog or the ALIAS
clause was specified in the CREATE REPORT FROM command.
FieldList O A collection of numeric field numbers representing the fields specified in the
Quick Report dialog or the FIELDS clause of the CREATE REPORT FROM
command.
File C The file name of the FRX open in the Report Designer. This file may not actually
exist if CREATE REPORT/LABEL was used.
Form L .T. if a form layout was chosen in the Quick Report dialog or the FORM clause
was specified in the CREATE REPORT FROM command; .F. if a column layout
was chosen or COLUMN was specified.
From C Contains the table name specified in the CREATE REPORT FROM command.
InWindow C The name of the window specified in the IN <window> clause of the
CREATE/MODIFY REPORT/LABEL command.
IsCreate L .T. if the command was CREATE REPORT/LABEL or .F. for MODIFY
REPORT/LABEL.
NoOverwrite L .T. if the NOOVERWRITE clause was specified in the CREATE REPORT FROM
command.
Save L .T. if the SAVE clause was specified in the CREATE/MODIFY REPORT/LABEL
command.
Titles L .T. if the Titles option was turned on in the Quick Report dialog or the TITLES
clause was specified in the CREATE REPORT FROM command.
Width N The number of columns specified in the CREATE REPORT FROM command.
Window C The window name specified in the WINDOW <name> clause of the
CREATE/MODIFY REPORT/LABEL command.
The report builder application runs within a private datasession that contains the FRX cursor.
The Report Designer passes its own datasession ID to the report builder application in case it
needs to access the tables open in the DataEnvironment of the Report Designer.
The FRX cursor the Report Designer creates for the report builder application has the alias
“FRX”. The record pointer is on the record for the object the event occurs for; this may be the
report header record (the first record in the cursor) if it’s a report event rather than an object
event. The records for any objects selected in the Report Designer have the CURPOS field set to
.T. There’s one slight complication with this: since CURPOS is used by the report header record
to store the value of the Show Position setting, you should ignore this record when looking at
records with CURPOS set to .T. For example, to count the number of selected objects, use:
count for CURPOS and recno() > 1
ReportBuilder.APP
By default, _REPORTBUILDER is set to ReportBuilder.APP in the VFP home directory. This
application provides a framework for handling design-time report events, plus provides a new set
of more attractive and functional dialogs that replace the native ones used by the Report
Designer. ReportBuilder.APP can be distributed with your applications to provide its behavior in
a runtime environment.
In addition to being called automatically by the Report Designer, you can call ReportBuilder.APP
manually to change its behavior for the current VFP session (it doesn’t write settings to any
external location, such as a table, INI file, or the Windows Registry, so, with one exception we’ll
see in a moment, state isn’t preserved from session to session).
If you call it with no parameters or pass it 1, it’ll display an options dialog in which you can
change the behavior of the report builder application. You can also right-click in the
Properties dialog and choose Options to launch the Options dialog.
Pass 3 and the name of a DBF file to use as the handler registry (we’ll discuss this later).
Pass 4 and a numeric value to set the “handle mode”. The numeric values match the buttons
in the When handling Report Designer events, the builder will setting shown in the
options dialog. For example, DO (_REPORTBUILDER) WITH 4, 3 tells ReportBuilder to
use the event inspector.
Pass 5 and optionally the name and path of a file to create a copy of the internal handler
registry table (if you don’t pass the second parameter, you’ll be prompted for the table to
create).
You can define what happens when ReportBuilder.APP receives a report event. The choices
are to search for a handler class in the handler registry table (the default behavior), use a
“debug” handler for events (displays a dialog showing the FRX in a grid and allowing
modifications to it and other settings), use an “event inspector” handler (displays information
about the event and the FRX in a MESSAGEBOX() window), or ignore report events.
You can specify what handler registry table should be used, or make a copy of the internal
one (we’ll discuss the handler registry later). You can also browse the selected registry table.
There are several ways you can extend the functionality of the Report Designer.
You can replace ReportBuilder.APP with your own report builder application by changing
_REPORTBUILDER.
You can register event handling objects in ReportBuilder.APP’s registry table. I suspect this
will be the most popular choice because ReportBuilder.APP provides a report builder
framework and lets you simply focus on handlers for report events.
DO (_REPORTBUILDER) and click on the Create Copy button to write the internal handler
registry table to an external one. By default, this copy will be named ReportBuilder.DBF.
When ReportBuilder.APP starts, it looks for a table called ReportBuilder.DBF in the current
directory or VFP path. If it finds such a table, it uses that table rather than the internal one.
So, simply creating this copy means ReportBuilder.APP will use it without having to do
anything else.
DO (_REPORTBUILDER) and click on the Choose button to select the table to use.
Once you’ve specified an external handler registry table, you can manually add or edit records in
that table or, in the ReportBuilder.APP Options dialog, click on the Explore Registry button and
edit the records in the resulting dialog.
NATIVE L .T. or .F. .T. to force the report event to be passed back to the Report
Designer for native behavior.
DEBUG L .T. or .F. .T. to force the debug handler to be used for this event/object
combination.
FLTR_ORDR C(1) " ", "1", "2", .. For filters and exit handlers only, specifies the order in which
they are applied.
When a report event occurs, ReportBuilder.APP looks for the handler class to instantiate by
looking for a record where EVENTTYPE matches the report event ID (or is -1), OBJTYPE
matches the OBJTYPE column of the selected FRX record (or is -1), and OBJCODE matches the
OBJCODE column of the selected FRX record (or is -1). Because ReportBuilder.APP uses the
first handler record it finds that meets its conditions, you may need to delete or disable built-in
handlers if you wish to implement your own. One way you can disable a handler record without
deleting it is to change EVENTTYPE to an invalid value. I like to add 100 to EVENTTYPE to
disable a record because I can easily re-enable it by subtracting 100.
As you can see in the table, REC_TYPE registers different types of records. Here are the
different types available:
A report event filter is a class that gets an earlier crack at the report event than a handler does.
While only a single handler is instantiated, ReportBuilder.APP instantiates the classes
specified in all filter records, in FLTR_ORDER order, and calls their Execute methods. Upon
return from Execute, if any filter object’s AllowToContinue property is .F., no further
processing happens and the Report Designer is informed that the event has been handled.
As you can see from this description, although they can both respond to report events, there’s
a big difference between event handlers and filters:
Filters are instantiated on every report event, while a handler is only instantiated for the
event it’s registered for in the handler registry table.
All filters are instantiated on an event, while only a single handler is.
This means filters are good for the behavior you want to occur on multiple, possibly all,
events, while handlers are specific for one type of event.
Exit handlers are similar to a combination of filters and event handlers in that after the other
processing is done, ReportBuilder.APP runs all registered exit handlers by instantiating the
appropriate classes, in FLTR_ORDER order, and calling their Execute methods. These
handlers are really just intended to perform any post-event cleanup behavior.
Run-time extension editors replace the dialog displayed when you click on the Edit Settings
button for the Run-time Extension property in the Other page of the Properties dialog for an
object.
Runs any registered filters as described earlier. Processing stops if any of them have
AllowToContinue set to .F.
Tries to find the handler for the event using the following search pattern:
If a handler was found, ReportBuilder.APP runs all registered exit handlers as described
earlier.
Handler Interfaces
Report event filters, event handlers, exit handlers, GetExpression wrappers, and run-time
extension editors can be based on any class and have only a single required method. The method
signatures are:
Event Object
ReportBuilder.APP passes an event object to the methods of handlers. This object includes as
properties the parameters passed by the Report Designer to ReportBuilder.APP, plus some other
useful information.
DefaultSessionID I The data session of the Report Designer (the fourth parameter passed in
from the Report Designer).
EventType I The event type (the second parameter passed in from the Report Designer).
FRXCursor O A helper object containing useful functions for interacting with the FRX
cursor.
FRXSessionID I The data session in which the FRX cursor is open (the default session when
the event handler is instantiated).
ObjCode I The value of the OBJCODE field of the selected record in the FRX cursor.
ObjType I The value of the OBJTYPE field of the selected record in the FRX cursor.
Protected L .T. if the Report Designer was launched with the PROTECTED keyword.
ReturnFlags I The value of this property is returned to the Report Designer in the first
parameter passed in from the Report Designer. It’s initially set to 1
(FRX_REPBLDR_HANDLE_EVENT); set it to 3
(FRX_REPBLDR_HANDLE_EVENT +
FRX_REPBLDR_RELOAD_CHANGES) if your class makes changes to the
FRX cursor that need to be reloaded into the layout.
SelectedObjectCount I The number of selected objects in the report layout, determined by counting
CURPOS = .T. in the FRX cursor (not counting the header record).
UniqueID C The value of the UNIQUEID field of the selected record in the FRX cursor.
It has several public methods; however, some are used by ReportBuilder.APP rather than an
event handler. The ones useful for an event handler are:
HasProtectionFlag tcBytes, tiFlag L Returns .T. if the given binary data (bytes) has
a specific protection flag set.
The Execute method of SFReportEventHandler saves the passed-in event object to its oEvent
property and the oEvent property of the SFReportEventUtilities object, then calls the OnExecute
method, which is abstract in this class. In a subclass, I won’t override the Execute method, but
will instead put the appropriate code into the OnExecute method.
lparameters toEvent
with This
.oEvent = toEvent
.oUtilities.oEvent = toEvent
.OnExecute()
.oEvent = .NULL.
.oUtilities.oEvent = .NULL.
endwith
To handle a particular report event, create a subclass of SFReportHandler and register it in the
handler registry table. To make it easy to do the latter, I created a program called
InstallHandler.PRG. Pass it the class and library for the handler, the event number, and optionally
the object type and code. It adds a record to the registry table (expected to be named
ReportBuilder.DBF; change the USE and INSERT INTO statements to use a different table
name) if it doesn’t exist, and disables any other handlers for the same event.
lparameters tcClass, ;
tcLibrary, ;
tnEventType, ;
tnObjType, ;
tnObjCode
local lcClass, ;
lnObjType, ;
lnObjCode
use ReportBuilder
* Disable any other handlers for the same event by setting their
* EventType code to an used value.
use
Report Templates
When you create a new report, you get a blank report. Wouldn’t it be nice if VFP would
automatically add certain common elements you want in every report? In other words, we’d like
to have a report that’s used as the template for all new reports.
Back in the FoxPro 2.6 days, we actually had this capability (although it was undocumented):
since FoxPro always created a new report with the name Untitled if you didn’t specify a name, if
you had a report called Untitled.FRX, VFP would open it, but prompt you for a new name when
you saved the new report for the first time. Unfortunately, this trick doesn’t work in VFP. First, if
you don’t specify a name, you get a default name of ReportN, where N is a number that
increments from 1 every time a report is created in a particular VFP session. Second, even if a
report named Report1.FRX exists and N will be 1 because this is the first time you’ve used
CREATE REPORT since starting VFP, VFP doesn’t open that report but gives you a blank
report.
Now, with design-time report events, we can have report templates. There isn’t an event that fires
when a report is created, but there is when one is opened, so we simply have to check if the
report is a new one or not (the utility method IsNewReport returns .T. if that’s the case). If it’s a
new report, we ZAP existing records in the FRX, APPEND FROM a template FRX file, and set
the return flag to indicate that the event was handled and the FRX was changed. Here’s the code
in the OnExecute method of SFNewReportHandlerBasic:
local lnSelect, ;
lnRecno
if This.oUtilities.IsNewReport()
lnSelect = select()
select FRX
zap
lnRecno = recno()
append from (This.cTemplateReport)
This.oEvent.ReturnFlags = FRX_REPBLDR_HANDLE_EVENT + ;
FRX_REPBLDR_RELOAD_CHANGES
go lnRecno
select (lnSelect)
endif This.oUtilities.IsNewReport()
The custom property cTemplateReport contains the name of the template FRX to use. By default,
it contains Template.FRX. To register this class, run InstallNewReportHandlerBasic.PRG.
Let’s get even fancier: how about asking the user which template they’d like to use?
SFNewReportHandlerFancy is a subclass of SFNewReportHandlerBasic with the following code
in OnExecute:
local loForm
if This.oUtilities.IsNewReport()
loForm = newobject('SFSelectTemplateForm', 'SFReportBuilder.vcx')
loForm.Show()
if vartype(loForm) = 'O'
This.cTemplateReport = loForm.cTemplate
endif vartype(loForm) = 'O'
endif This.oUtilities.IsNewReport()
return dodefault()
This code uses the SFSelectTemplateForm class to display a list of available templates. This list
comes from Templates.DBF, which has fields containing the name of the template FRX, a
descriptive name for the template, and a memo containing comments about the template. To
register this class, run InstallNewReportHandlerFancy.PRG.
Custom Dialog for New Fields
One of the things I’ve always wanted to do was replace the dialog that appears when a user adds
a new field to a report. I want a dialog that is both simpler (it displays descriptive names for
tables and fields) and more powerful (it doesn’t require the tables to be in the DataEnvironment
or open and it has options for adding a label to go along with the field). Since I can now take over
the “new field” report event, I can finally create the dialog I want.
SFNewTextBoxHandler is registered as the handler for new fields with this line of code (taken
from TestNewField.PRG):
do InstallHandler with 'SFNewTextBoxHandler', 'SFReportBuilder.vcx', 2, 8, 0
TestNewField.PRG also creates a meta data object that has collections of tables and fields read
from a meta data table. We won’t look at that object here; feel free to examine it yourself.
When you run TestNewField.PRG, it automatically creates a new report, but nothing else appears
different. However, since SFNewTextBoxHandler is now the handler for new fields, when you
add a field, you’ll get the dialog shown below rather than the usual one.
This dialog displays descriptive names for the tables and fields in the SQL Server Northwind
database, which, of course, aren’t in the DataEnvironment or open in VFP (they don’t need to be
since this information comes from meta data). It also allows you to indicate whether a label is
created or not, and if so, whether it should be placed in the page header band above the field or in
the detail band to the left of the field.
The next thing it does is determine where to put the label object for the field. If it’s supposed to
go in the page header, the SFReportEventUtilities object is asked to find a label in the page
header band that has “*:TEMPLATE” in its USER memo. If such an object exists, it’s used as
the template for the new label (font, style, vertical position, etc.). If “REMOVE” also appears,
the template object is removed from the report.
if lnPosition = 1
loBand = .oUtilities.GetBandObject(FRX_OBJCOD_PAGEHEADER)
loTemplate = .oUtilities.FindTemplateObject(loBand, ;
FRX_OBJTYP_LABEL, '*:TEMPLATE', .T.)
if vartype(loTemplate) = 'O'
if '*:TEMPLATE REMOVE' $ upper(loTemplate.User)
.oUtilities.RemoveReportObject(loTemplate.UniqueID)
loTemplate.User = strtran(loTemplate.User, ;
'*:TEMPLATE REMOVE', '*:TEMPLATE')
endif '*:TEMPLATE REMOVE' $ upper(loTemplate.User)
loObject = loTemplate
lnVPos = loObject.VPos
else
lnVPos = loBand.Stop - lnHeight - BAND_SEPARATOR_HEIGHT_FRUS
endif vartype(loTemplate) = 'O'
If the label is supposed to go in the same band as the field, the SFReportEventUtilities object is
asked to find a band in the report, then like the previous code, to look in that band for a label
with “*:TEMPLATE” in its USER memo and use it as the template.
else
loBand = .oEvent.FRXCursor.GetBandFor(FRX.UniqueID)
loTemplate = .oUtilities.FindTemplateObject(loBand, ;
FRX_OBJTYP_LABEL, '*:TEMPLATE', .T.)
if vartype(loTemplate) = 'O'
if '*:TEMPLATE REMOVE' $ upper(loTemplate.User)
.oUtilities.RemoveReportObject(loTemplate.UniqueID)
loTemplate.User = strtran(loTemplate.User, ;
'*:TEMPLATE REMOVE', '*:TEMPLATE')
endif '*:TEMPLATE REMOVE' $ upper(loTemplate.User)
loObject = loTemplate
endif vartype(loTemplate) = 'O'
endif lnPosition = 1
By the way, this sample shows another cool new feature in VFP 9: design-time labels. Notice that
when you add a field to the report, it displays the caption rather than the field name in the field
object. That’s because the code above fills in the NAME column of the field object in the FRX
with the caption for the field. When you use CREATE or MODIFY REPORT with the
PROTECTED keyword, the Report Designer will display the contents of the NAME column
rather than the EXPR column for field objects. This means you can display nice descriptive
names for fields rather than the actual field names. Of course, they’ll see the actual field names in
the Properties dialog, but at least they’ll see the nice names on the design surface.
Since we can now create a report from meta data using the SFNewTextBoxHandler handler, what
happens when we try to preview the report? It won’t work, because the cursors haven’t been
opened. Ah, but what if we hooked into the preview event and generated the cursors before the
report is run?
SFPreviewHandler gets registered as the handler for events 10 (previewing) and 18 (printing) by
InstallPreview.PRG. It’s OnExecute method is fairly simple: it generates a SQL SELECT
statement by looking at the fields in the report (we won’t look at the code for the
CreateSQLStatement method here), instantiates an object that opens a connection to the SQL
Server Northwind database, sends the SQL SELECT statement to SQL Server to create a cursor,
and tells the report engine to cancel the preview or print if we failed for some reason.
local lcSelect, ;
loConnection, ;
llOK
* Create a connection object and try to connect to the SQL Server Northwind
* database. If we succeeded, execute the SQL SELECT statement in the report's
* datasession.
Summary
The VFP team certainly met their goals in the improvements they made to the reporting system.
The dialogs available in ReportBuilder.APP are more attractive, easier to use, and more capable
than those in earlier versions. The ability to hook into design-time report events means you can
create customized report designers that are more powerful, flexible, and easier to use, both for
your development team and your end users.
Note: since this document was written during the beta of VFP 9, many of the details described in
this document may be different in the release version. Be sure to check my Web site
(www.stonefield.com) for updates to this document and the accompanying samples.
Biography
Doug Hennig is a partner with Stonefield Systems Group Inc. and Stonefield Software Inc. He is
the author of the award-winning Stonefield Database Toolkit (SDT), the award-winning
Stonefield Query, and the MemberData Editor, Anchor Editor, and CursorAdapter and
DataEnvironment builders that come with Microsoft Visual FoxPro. Doug is co-author of
“What’s New in Visual FoxPro 8.0”, “The Hacker’s Guide to Visual FoxPro 7.0”, and “What’s
New in Visual FoxPro 7.0”. He was the technical editor of “The Hacker’s Guide to Visual
FoxPro 6.0” and “The Fundamentals”. All of these books are from Hentzenwerke Publishing
(http://www.hentzenwerke.com). Doug writes the monthly “Reusable Tools” column in FoxTalk.
He has spoken at every Microsoft FoxPro Developers Conference (DevCon) since 1997 and at
user groups and developer conferences all over North America. He has been a Microsoft Most
Valuable Professional (MVP) since 1996.
Copyright © 2004 Doug Hennig. All Rights Reserved