Wiki Home

Report Listener


Namespace: WIN_COM_API
This is a space to discuss the Report Listener Class introduced in VFP 9.0. As more information is gathered, we should probably have a topic for each type of Report Listener.

Deploying Report Behavior 90 In Runtime

Introduction
The Fox Development Team did an "Outstanding" job with VFP 9.0! Hearing our "cries in the wilderness", they introduced a radically different approach to reporting. It is not strictly Object Oriented (like some may have wished), but it is "object enhanced" and I believe if you educate yourself and keep your mind open, you will find a wealth of opportunity (and they didn't break any old reports - how do they do it?!)

The ReportBuilder and Report Listener give us much of the ability to implement features normally only found in 3rd party reporting tools. I expect this version to spur dramatic updates in all existing VFP reporting tools as well as encourage products only dreamed of in the past. I actually prefer it to using a 3rd party tool (like Crystal Reports) since it is all VFP, Royalty-Free, easy to distribute, easy to troubleshoot, and can exchange information with my application.

The VFP 9.0 Report Writer has several "components" including the Report Writer we programmers utilize, the Report Builder that can be customized for end-users, and the Report Listener(s) that allow you to interact with the report as it is running and can customize the output options. There are numerous ways to extend, document, and customize your reports. Even the "components" themselves are extensible and replaceable. In fact, there are sooooo many possible ways to accomplish things that were previously impossible that you may have the most trouble deciding on the best path to take! In my book, this is a welcome problem!

This topic will focus on the Report Listener classes and how to use them to accomplish real tasks. If you want to know more about all of the "components" and overall information, see the links below for "Must Read" and "Additional Information". Any additional Report Listener samples that are found can be linked below under "Report Listener Samples".

Must Read:
Additional Information:
Report Listener Samples:

Instantiating a Report Listener
There are many ways to instantiate a Report Listener object. The method you use will depend on what you are tying to achieve and on your personal preference. Remember, the primary purpose for defining a Report Listener object is to "interact" with the report while it is running. If you just want to "use" one of the new listener classes, just do the following:
REPORT FORM myreport OBJECT TYPE 1  &&PreviewListener
  -or-
REPORT FORM myreport OBJECT TYPE 5  &&HTMLListener
  -or-
SET REPORTBEHAVIOR 90
REPORT FORM myreport PREVIEW


If you just want to "tweak" some properties of an existing report listener (set output file name, etc.), just declare an object of the specific report listener you want, set your properties, and use that object to produce the report. Example:
SET CLASSLIB TO HOME() + 'FFC\_REPORTLISTENER'
oHTMListener = CREATEOBJECT('HTMLListener')
oHTMListener.TargetFileName = 'MyFilename'
REPORT FORM myreport OBJECT oHTMListener

  -or-

oListen = NewObject("UtilityReportListener","listener",_reportoutput)
oListen.ListenerType = 5
oListen.TargetFileName = 'MyFilename'
REPORT FORM myreport OBJECT oListen


*The Utility and Debug Listeners create output files. By default, these files are created (by the class) using this code (FORCEPATH(SYS(2015),SYS(2023))). This generates a "uniquely" named file in your current "Temporary" files folder (as defined in VFP).

*The object you create (oListen in the last example) will still be available to you after your report runs and it will contain information on the overall progress of the report production.


If this is all you want to do with reports, you can stop reading here. If you want to actually interact with the report at run-time, it is advisable to create your own subclass of the appropriate listener class (usually Utility Report Listener). This will allow you to overide/augment the default methods and properties as needed. Why would you need to "interact" with a report at run-time?
This last reason is the one that prompted me to learn to use the Report Listeners. I needed to be able to summarize a large amount of varied detail data that had very complex rules about how items should be grouped and totaled. I needed to then store the summarized records in a table for later review and editing by my users. They basically told me "they" wanted to change the subtotals and wanted "me" to filter those changes down to the appropriate detail records. This is what I would call a "real time" or "live" report, where the user is able to change the data presented. It goes one step beyond in that the changes made on the screen also be made to all related detail records.

I tried to use SQL statements to create the tables containing my subtotals, but the calculations and grouping were too complex. If I ever did get it to work, I wouldn't be able to touch it after three months had gone by and I had forgotten "the magic words". Then I tried to create my own set of "loops" to process the records myself. However, the logic to correctly fire individual headers and footers in the correct order while the record pointer was on the correct record felt like recreating the wheel. This wheel had already been created by the VFP reporting engine, I just had to tap into that power.

The Report Listener came to my rescue. The reporting engine handles the "grunt" work of knowing when to fire the correct event sequence, and it is nice enough to "inform" me when one of these events occurs so that I can do what I need to do and then get out of the way until the next event. To use the report listeners effectively, it is useful to know "best practices" and some technical information.


Report Listener Classes
The following classes are used during report generation and are part of the VFP FFC (foundation framework). They can be found under your VFP installation folder in a folder named "\FFC". Most of the classes found in the FFC have their own *.h (header/include) file to define commonly used constants.

_ Report Listener (based on Report Listener internal class)
_FrxCursor (has properties and methods for manipulating FRX files/cursors)
_GDIPlus (new GDI library interface used by VFP 9)

_ Report Listener

Report Variables
It is not documented (that I could find), but any variables defined on your report form are available to you in your report listener subclass code. The report form variables do not belong to any object and are not available outside the datasession used by your report. I recommend naming report variables, that are to be accessed from outside the report, with a naming convention that will quickly tell you what they are!


Successors - a quick definition:
A new very cool feature of the VFP reporting is the ability to run a report one time, but create multiple "outputs". For example, this would allow you to "print" a report and create an "HTML" copy of it with a single "report form" command. Report Listeners make this possible. You actually use one Report Listener for each "output" your report creates. You will typically have your primary listener (such as a Preview) and another listener for each extra output (such as HTML).

VFP calls each "extra" listener a "Successor". This name is appropriate because behind the scenes, VFP calls (in succession) each listener for each data record it processes. VFP stores all of the listeners you are using in a new system variable _oReportOutput[] (not to be confused with the system variable _ReportOutput). VFP (and you) can then access any of the defined listener objects by accessing it in the array (you could use a collection).

**Define a "Preview" listener.
oListener = NEWOBJECT("ltListener")
oListener.ListenerType = PreviewListener

**Create a new HTML Listener and set it as a "successor" to our Preview listener.
DO (_REPORTOUTPUT) WITH HTMLListener
oListener.Successor = _oReportOutput[TRANSFORM(HTMLListener)]
_oReportOutput[TRANSFORM(HTMLListener)].TargetFileName = ("c:\output\MyHTML")



Defined Report Listener Types:
-2 Debug Listener
0 Print Listener
1 Preview Listener
4 XML Listener
5 HTML Listener

Print - Your standard printer output.
Preview - Enhanced previewer.
XML - Creates XML file which can consist of "just" your data, or the XML schema and the data. I'm sure this will be useful down the road, but right now I can't figure out how to get the data back out into a usable state. Also, there seem to be some problems with "unpaired" items if you don't consistently pair your headers/footers.
HTML - Creates an HTML file that looks "remarkably" like your printed report. I haven't tried anything complex, but the HTML source code is "rendered" using classes that specify the same coordinates as the printed report.
Debug - Creates a text file with debug info.

The DebugListener is not really documented in depth anywhere that I could find. It is "very" handy as it produces the equivalent of an "event tracking" session as your report runs. It outputs "debug" information to a text file as your report runs that looks something like this:

DEBUGLISTENER.LOADREPORT
------------------------------
MEMBERS:
.ALLOWMODALMESSAGES=.T.
.APPNAME=VFP Report Output Class
... more stuff here ...
------------------------------
DEBUGLISTENER.LOADREPORT received CommandClauses
.ASCII=.F.
.DE_NAME=inv1
... more stuff here ...
------------------------------
DEBUGLISTENER.BEFOREREPORT
------------------------------
MEMBERS:
.ALLOWMODALMESSAGES=.T.
.APPNAME=VFP Report Output Class
... more stuff here ...
------------------------------
DEBUGLISTENER.BEFOREREPORT received CommandClauses
.ASCII=.F.
.DE_NAME=inv1
... more stuff here ...
------------------------------
DEBUGLISTENER.BEFOREBAND,0,2
DEBUGLISTENER.RENDER,13,288,0,1390,220,0,E X A M P L E    R  E  P  O  R  T ,0
DEBUGLISTENER.AFTERBAND,0,2
DEBUGLISTENER.BEFOREBAND,1,3
DEBUGLISTENER.AFTERBAND,1,3
DEBUGLISTENER.BEFOREBAND,3,4
DEBUGLISTENER.EVALUATECONTENTS,14,(Object)
DEBUGLISTENER.RENDER,14,308,420,7230,150,0,S  u  b  H  e  a  d    E  x  a  m  p  l  e ,0
DEBUGLISTENER.AFTERBAND,3,4
DEBUGLISTENER.BEFOREBAND,3,5
DEBUGLISTENER.EVALUATECONTENTS,21,(Object)
... more stuff here ...
DEBUGLISTENER.DOSTATUS,Rendering Page: 1
DEBUGLISTENER.DOSTATUS,Rendering Page: 1
DEBUGLISTENER.DOSTATUS,Rendering Page: 1
DEBUGLISTENER.AFTERREPORT
------------------------------
DEBUGLISTENER.AFTERREPORT current CommandClauses
... more stuff here ...
------------------------------
DEBUGLISTENER.UPDATESTATUS
DEBUGLISTENER.CLEARSTATUS
DEBUGLISTENER.CLEARSTATUS
DEBUGLISTENER.CLEARSTATUS
DEBUGLISTENER.UNLOADREPORT
------------------------------
DEBUGLISTENER.UNLOADREPORT current CommandClauses
... more stuff here ...



Unless noted, the following are for the UtilityListener.

Report Listener Properties:


XML Specific Properties:



Report Listener Methods:



EvaluateContents() -versus- Render(): If you are trying to "capture" information from a report or "change" information on a report, you will need to use the EvaluateContents() and Render() methods. They appear to serve a somewhat similar task, but they actually represent two different "events".


Data Environment:
When an event fires one your your Report Listener methods, that method has access to an unprecedented amount of information and data. You can access report variables (just use them as if they were "privately" defined variables, you can access the Data Environment and tables that are being used as the "datasource" for the report, and you can access a special cursor that holds the contents of the .FRX file that defines the report (uses the alias 'frx'). The SetFRXDatasession method will put you into the datasession to access the FRX cursor and the SetCurrentDatasession method will put you back in the datasession to access the report's "driving alias" (cursor/table).

Why would you care to access the FRX contents? Well, most of the methods in the base report listeners pass as a parameter the actual "RecNo" of the FRX record that is currently being evaluated. By having this Record Number and access to the FRX (as a table), you can quickly "GO" to that record in the FRX to retreive any information stored in the FRX table. This includes Comments, User-Defined information, and the new Member Data (XML structured text). I use any/all of these "fields" to store details about the "type" of data being processed.

For example, when a BeforeBand() or AfterBand() method is called, they only pass you the general band type (header, detail, footer, etc). If you have many levels of Details and Groupings, this will not tell you "which specific" Detail or Group Header/Footer you are positioned on. If, however, you store a unique "Band ID" in one of the available FRX fields (comment, user data, Member Data, etc.), you can easily "GO" to the FRX record and retrieve this stored information that will give any information you have stored.


BeforeBand() -vs- AfterBand():
In my testing, the record pointer of the DrivingAlias seems to be more stable if accessed in the AfterBand() than in the BeforeBand(). I primarily use the AfterBand(), but if you wanted to access any Report Variables or data "before" any calculations were made, you could use the BeforeBand(). In the AfterBand(), the record pointer (of the DrivingAlias) has NOT yet moved to the next record, so you should have access to the same record in either method.

Warning - Accessing the DrivingAlias workarea may have an anomoly at the point of each "group" band. It appears that the record pointer is on the same record for the last 2 records in the detail Group, then switches to the next record during the processing of the group footer. This could result in missing some detail records.


Run-Time Extensions (RTE):
A Run-Time Extension (or RTE) is a way that you can "embed" information in a report object (field, line, label, etc.) that is evaluated when the report is runing and is "trappable" within your Report Listener. In other words, it is a method of triggering your custom listener code and passing that code parameters while the report is running. You might say that you have always had the ability to call User-Defined Functions from any object on a report at runtime. Yes, but you now have the ability to have that code safely contained in your Report Listener object, and you have full access to the report variables, the FRX contents, the report Datasession, and all current pointers!

RTE's are tied to a report item (field, label, line, etc.) and can be triggered any time the report item is "evaluated" or "rendered". The BeforeBand() and AfterBand() events only occur ONCE per band. The EvaluateContents() and Render() events occur with every report item in every band, so RTE's are defined on individual report items.

As an example, I have one Footer band that actually printed several pieces of SubTotal information. I use RTE's trap when this information prints, then fire an event in my Listener to record the values that are printed. This is a way that you could print an "Invoice" for example, and have the report save the totals for each line item and the Invoice total into your tables without having to have one piece of code that calculated these totals to your table, then a seperate report to print the Invoice. It would also eliminate the chance of your code calculating one set of totals and the report printing different ones.

You "define" an RTE in the Report Designer. You edit the Run-Time Extension XML string of a Report Item (field, text, line, etc) by access the "Properties", click the "Other" tab, click the "Edit Setting" button under "Run-Time Extensions". In the XML string, you store the information your code will need to identify the report item, the method/function you want to be called, etc. The "Execute When" clause can give you even more control of when your RTE is triggered. You will see an example of an RTE in the code sample below.


A Real World Example:

The following example is not your "help document" example that shows the simplest possible scenario. This is an abbreviated and generalized copy of some real production code. In this example, I am creating a record in a table for each data grouping found by the report writer. I am manually performing my own calculations and storing the results in objects. When a "Footer" is hit, I write these results to a record in a table. I am also "stamping" each detail record with the "primary key" value of the group in which it exists. Even without understand exactly what I am doing, you can see how the process of capturing "report events" happens.

**WARNING - You must be very careful that you
**  "record" and "restore" the "alias" selected when placing
**  code in the "base" methods.  If you change the selected
**  alias, and forget to set it back before
**  continuing the report, unpredictable results can occur.

*Code to instantiate the listener object and run the report.
#INCLUDE reportlisteners.h
Set Classlib To Home()+"\ffc\_reportlistener.vcx" additive

#DEFINE DebugListener -2
#DEFINE PrintListener 0
#DEFINE PreviewListener 1
#DEFINE XMLListener 4
#DEFINE HTMLListener 5

**Custom Listener
Local oListener as object
oListener = NEWOBJECT("ltListener")
oListener.ListenerType = PreviewListener

*Setup HTML Listener as a successor
DO (_REPORTOUTPUT) WITH HTMLListener
oListener.Successor = _oReportOutput[TRANSFORM(HTMLListener)]
With _oReportOutput[TRANSFORM(HTMLListener)]
  .QuietMode    = .T.
  .TargetFileName = ("c:\output\myreport")
  .TargetFileExt  = ("HTML")  &&"TXT", "LOG", "XML"
  IF FILE((.TargetFileName+[.]+.TargetFileExt))
    ERASE((.TargetFileName+[.]+.TargetFileExt)) NORECYCLE
  ENDIF
EndWith

***Run Report
REPORT FORM myReport OBJECT oListener
Return


*+------------------------------------------------------
*| Custom Listener Class named: "ltListener"
*+------------------------------------------------------
DEFINE CLASS ltListener AS utilityreportlistener
************* Instance Specific Properties
oFRX      = .NULL.
oData     = .NULL.
nCurrBand   = 0
cCurrBandDesc = Space(0)

*Group objects
oGroup        = .NULL.
oDetail1      = .NULL.
oDR         = .NULL.
oWK         = .NULL.
oSC         = .NULL.
oTotal        = .NULL.

*Properties to track "current" items.
cCurrBandID     = Space(0)  && GRP_DRFOOT, GRP_DRHEAD, etc.
cCurrGroupPrefix  = Space(0)  && SC, DR, WK

*+---------------------------------------------------------
*| *** Parent Method Customizations ***
*+---------------------------------------------------------
*+---------------------------------------------------------
*| Error()  **Inherited Method
*|  Triggered when an Error occurs while running the report.
*+---------------------------------------------------------
Procedure Error(nError, cMethod, nLine)
  DoDefault(nError, cMethod, nLine)
  Set Step On
EndProc


*+---------------------------------------------------------
*| BeforeReport() **Inherited Method
*|  Triggered "Before the Report is run".
*+---------------------------------------------------------
PROCEDURE BeforeReport
  Local lcAlias as String
  lcAlias = Alias()

  *Create object for each "data grouping".
  *(There will typically be 2 report Bands per group,
  *  a header and a footer.)
  this.oGroup   = CreateObject('Empty')
  this.oDetail1 = CreateObject('Empty')
  this.oDR  = CreateObject('Empty')
  this.oWK  = CreateObject('Empty')
  this.oSC  = CreateObject('Empty')
  this.oTotal   = CreateObject('Empty')

  this.zCreateGroups()

  If !Empty(lcAlias)
    Select &lcAlias
  EndIf

  If !DoDefault()
    Return .f.
  EndIf
EndProc


*+---------------------------------------------------
*| BeforeBand() **Inherited Method
*|  Triggered "before the band has been processed".
*+---------------------------------------------------
PROCEDURE BeforeBand(nBandObjCode,nFRXRecno)
  DoDefault(nBandObjCode,nFRXRecno)

  Local lcAlias as String
  lcAlias = Alias()

  *...Put code here...

  If !Empty(lcAlias)
    Select &lcAlias
  EndIf
ENDPROC


*+---------------------------------------------------
*| AfterBand()  **Inherited Method
*|  Triggered "after the band has been processed".
*+---------------------------------------------------
PROCEDURE AfterBand(nBandObjCode,nFRXRecno)
  Local lcGroup, llDupRec, llCreateGroupRecord, ;
      llResetGroup, lcCompDataRecNo, llShowCols, lcAlias
  Store "" to lcGroup, lcCompDataRecNo
  Store .F. to llDupRec, llCreateGroupRecord, llResetGroup, llShowCols

  *Scatter data properties for any Group EXCEPT columns.
  If InList(nBandObjCode, 2, 6)
    DoDefault(nBandObjCode,nFRXRecno)
    Return
  EndIf

  lcAlias = Alias()

  this.zGetFrxDataObject(nFRXRecno) &&Scatter report info
  this.zGetDrivingDataObject()   &&Scatter data info

  this.nCurrBand        = nBandObjCode
  this.cCurrBandDesc      = Space(0)
  this.cCurrGroupPrefix   = Space(0)

  *Note - The parameter "nBandObjCode" only tell you
  *  which "type" of band is being processed.
  *If you have multiple Footer bands for example,
  *  this will not tell you which one is the current one.
  *I entered a "unique ID" for each band using the
  *  "User" field of FRX.  This can be easily accessed
  *  now from the report writer by clicking on the "Properties" of the band.
  this.cCurrBandID      = Alltrim(Upper(this.oFrx.User))

  llShowCols  = this.oData.jta_show_cols

  Do case
  case this.cCurrBandID = "GRP_TITLE"  &&Report Title
    this.cCurrBandDesc    = "Title"
    this.cCurrGroupPrefix   = "TOTAL"
    llCreateGroupRecord     = .T.

  case this.cCurrBandID = "GRP_SCHEAD" &&Task
    this.cCurrBandDesc    = "Task Heading"
    this.cCurrGroupPrefix   = "SC"
    llCreateGroupRecord     = .T.

  case this.cCurrBandID = "GRP_WKHEAD"
    this.cCurrBandDesc    = "Week Heading"
    this.cCurrGroupPrefix   = "WK"
    If llShowCols = .T.
      llCreateGroupRecord   = .T.
    EndIf

  case this.cCurrBandID = "GRP_DRHEAD" &&Date/Report
    this.cCurrBandDesc    = "Date/Rpt Heading"
    this.cCurrGroupPrefix   = "DR"
    If llShowCols = .T.
      llCreateGroupRecord   = .T.
    EndIf

  case this.cCurrBandID = "GRP_DETAIL1" &&Detail
    this.cCurrBandDesc    = "Detail #1"
    this.cCurrGroupPrefix   = "DETAIL1"
    llResetGroup      = .T.

    *Store (the current PK) back to the detail record.
    *This will allow us to relate each detail back
    *  to the "parent" group record.
    this.zUpdateDetail()

    *Perform data calculations for each Group for every Detail.
    *The logic in the Group will determine the calculations made.
    this.zCalcGroup("DETAIL1")
    this.zCalcGroup("DR")
    this.zCalcGroup("WK")
    this.zCalcGroup("SC")
    this.zCalcGroup("TOTAL")

  case this.cCurrBandID = "GRP_DRFOOT" &&Date/Report
    this.cCurrBandDesc    = "Date/Rpt SubTotal"
    this.cCurrGroupPrefix   = "DR"
    llResetGroup      =.T.
    If llShowCols = .T.
      llCreateGroupRecord   = .T.
    Endif

  case this.cCurrBandID = "GRP_WKFOOT" &&Year/Week
    this.cCurrBandDesc    = "Week SubTotal"
    this.cCurrGroupPrefix   = "WK"
    llResetGroup      = .T.
    If llShowCols = .T.
      llCreateGroupRecord   = .T.
    Endif

  case this.cCurrBandID = "GRP_SCFOOT" &&Task
    this.cCurrBandDesc    = "Task SubTotal"
    this.cCurrGroupPrefix   = "SC"
    llCreateGroupRecord     = .T.
    llResetGroup      = .T.

  case this.cCurrBandID = "GRP_SUMMARY" &&Summary
    this.cCurrBandDesc    = "Summary"
    this.cCurrGroupPrefix   = "TOTAL"
    llCreateGroupRecord     = .T.

  Otherwise
    If !Empty(this.cCurrBandID)
      =MessageBox("Unknown Band: "+this.cCurrBandID,48,"AfterBand")
    endif
  EndCase

  lcGroup = "this.o"+this.cCurrGroupPrefix

  If !Empty(this.cCurrBandID)
    **Determine if we have already processed this detail record
    **  for this Group, save detail record # for this Group.
    With &lcGroup
      If &lcCompDataRecNo = Recno(this.DrivingAlias)
        llDupRec = .T.
      Else
        &lcCompDataRecNo = Recno(this.DrivingAlias)
      endif
    EndWith
  EndIf

  If !llDupRec
    If llCreateGroupRecord
      this.zCreateGroupRecord() &&Create Group record.
    EndIf
    If llResetGroup
      this.zResetGroup()  &&Reset calcs in Group.
    endif
  EndIf

  If !Empty(lcAlias)
    Select &lcAlias
  EndIf

  DoDefault(nBandObjCode,nFRXRecno)
ENDPROC


*+-------------------------------------------------------------------------
*| AfterReport()  **Inherited Method
*|  Triggered "after the report has been run".
*+-------------------------------------------------------------------------
PROCEDURE AfterReport()
  Local lcAlias as String
  lcAlias = Alias()

  *Put custom code here

  If !Empty(lcAlias)
    Select &lcAlias
  EndIf

  DoDefault()
ENDPROC

*+-----------------------------------------------------
*|    *** Custom Methods ***
*+-----------------------------------------------------
*+-----------------------------------------------------
*| zCalcGroup()
*|  Performs calculations for the current "group".
*|  Called one time per Detail record.
*+-----------------------------------------------------
Procedure zCalcGroup(lcGroupPrefix)
  Local lcGroup, lcGroupTrig
  Store "" to lcGroup, lcGroupTrig

  lcGroup = "this.o"+lcGroupPrefix &&SC, DR, WK

  With &lcGroup
    *Make sure we don't process this record multiple
    * times for the same Group.
    If .DataRecNo = Recno(this.DrivingAlias)
      Return
    EndIf

    .DataRecNo    = Recno(this.DrivingAlias)

    *Make sure we are supposed to process this type.
    If !Evaluate(.GroupTrig)
      Return
    endif

    If this.cCurrBandID = "GRP_DRFOOT" or ;
      this.cCurrBandID = "GRP_DRHEAD"
        .Work_Date  = this.oData.bd_workdate
        .Reportnum  = this.oData.bd_reportnum
    endif

    If Evaluate(.Main_Trig)
      .Main_Qty   = .Main_Qty + this.oData.bd_qty
      .Main_Rate  = this.oData.bd_rate
      .Main_Amt = .Main_Amt + this.oData.bd_amount
    endif

    If Evaluate(.Hrs_Trig)
      .Hrs_Qty  = .Hrs_Qty + this.oData.bd_qty
      .Hrs_Rate = this.oData.bd_rate
      .Hrs_Amt  = .Hrs_Amt + this.oData.bd_amount
    EndIf

  EndWith
EndProc


*+------------------------------------------------------
*| zResetGroup()
*|  Resets the calculated values for a Group.
*|  Called from Footer/SubTotal Groups only.
*+------------------------------------------------------
Procedure zResetGroup(lcGroupPrefix)
  Local lcGroup, lcGroupTrig, liParentLinePK
  Store "" to lcGroup, lcGroupTrig, lcParentGroup
  Store 0 to liParentLinePK

  lcGroup = "this.o"+this.cCurrGroupPrefix &&SC, DR, WK

  With &lcGroup
    .CurrLinePK   = 0
    .Work_Date    = {}
    .Reportnum    = []
    .Main_Qty     = 0.00
    .Main_Rate    = 0.00
    .Main_Amt   = 0.00
    .Hrs_Qty    = 0.00
    .Hrs_Rate   = 0.00
    .Hrs_Amt    = 0.00
  EndWith
EndProc


*+-----------------------------------------------------
*| zGetFrxDataObject()
*+-----------------------------------------------------
Procedure zGetFrxDataObject(lnFRXRecno as integer)
  Local lcAlias as String
  Store "" to lcAlias

  *Select the datasession containing the .FRX file
  * open as a table with an alias of "FRX".
  THIS.SetFRXDataSession()
  If !Empty(Alias()) and Alltrim(Upper(Alias())) <> "FRX"
    lcAlias = Alltrim(Upper(Alias()))
  EndIf

  Select frx
      *Go to the record in the FRX that is currently being processed.
  GO lnFRXRecNo IN FRX
  Scatter name this.oFRX memo

  If !Empty(lcAlias) and Alltrim(Upper(Alias())) <> lcAlias
    Select (lcAlias)
  EndIf

  THIS.SetCurrentDataSession()
  Return .t.
EndProc


*+-------------------------------------------------------------------------------
*| zGetDrivingDataObject()
*+-------------------------------------------------------------------------------
Procedure zGetDrivingDataObject()
  Local lcAlias as String
  Store "" to lcAlias

  If !Empty(Alias()) and Alltrim(Upper(Alias())) <> Alltrim(Upper(this.DrivingAlias))
    lcAlias = Alltrim(Upper(Alias()))
  EndIf

  Select (this.DrivingAlias)
  Scatter name this.oData memo

  If !Empty(lcAlias) and Alltrim(Upper(Alias())) <> lcAlias
    Select (lcAlias)
  EndIf
EndProc


*+-------------------------------------------------------------------------------
*| zUpdateDetail()
*+-------------------------------------------------------------------------------
Procedure zUpdateDetail()
  Local lcAlias as String
  Store "" to lcAlias

  If !Empty(Alias())
    lcAlias = Alltrim(Upper(Alias()))
  EndIf

  If Seek(this.oData.bd_pk, 'bd_curr', 'bd_pk')
    Select bd_curr
    replace bd_inl_pk with this.iInvLinePK
  endif

  If !Empty(lcAlias)
    Select (lcAlias)
  EndIf
EndProc


*+-------------------------------------------------------------------------------
*| zCreateGroups()
*+-------------------------------------------------------------------------------
Function zCreateGroups()
  *Create a generic "Group" object
  AddProperty(this.oGroup, 'ParentGroupPrefix', '')
  AddProperty(this.oGroup, 'DataRecNo', 0)
  AddProperty(this.oGroup, 'IndentLevel', 0)
  AddProperty(this.oGroup, 'GroupTrig', '')
  AddProperty(this.oGroup, 'HeadDataRecNo', 0)
  AddProperty(this.oGroup, 'HeadLinePK', 0)
  AddProperty(this.oGroup, 'FootDataRecNo', 0)
  AddProperty(this.oGroup, 'FootLinePK', 0)
  AddProperty(this.oGroup, 'Work_Date', {})
  AddProperty(this.oGroup, 'Reportnum', '')
  AddProperty(this.oGroup, 'Main_Trig', '')
  AddProperty(this.oGroup, 'Main_Qty', 0.00)
  AddProperty(this.oGroup, 'Main_Rate', 0.00)
  AddProperty(this.oGroup, 'Main_Amt', 0.00)
  AddProperty(this.oGroup, 'Hrs_Trig', '')
  AddProperty(this.oGroup, 'Hrs_Qty', 0.00)
  AddProperty(this.oGroup, 'Hrs_Rate', 0.00)
  AddProperty(this.oGroup, 'Hrs_Amt', 0.00)

  *Define default settings.
  this.oGroup.Main_Trig = [this.oData.bd_auto_type = 0]
  this.oGroup.Hrs_Trig  = [this.oData.bd_item_type = "T" and ;
                this.oData.bd_otflag = .F. and ;
                this.oData.bd_auto_type <> 0]
  this.oGroup.OT_Trig   = [this.oData.bd_item_type = "T" and ;
                this.oData.bd_otflag = .T. and ;
                this.oData.bd_auto_type <> 0]
  this.oGroup.Mile_Trig   = [this.oData.bd_item_type = "M" and ;
                this.oData.bd_auto_type <> 0]
  this.oGroup.Trip_Trig   = [this.oData.bd_item_type = "U" and ;
                this.oData.bd_auto_type = 3]
  this.oGroup.Unit_Trig   = [this.oData.bd_item_type = "U" and ;
                this.oData.bd_auto_type <> 0]

  *Copy properties from default Group object to each Group object
  this.zCloneProps("this.oGroup", "this.oDetail1")
  this.zCloneProps("this.oGroup", "this.oDR")
  this.zCloneProps("this.oGroup", "this.oWK")
  this.zCloneProps("this.oGroup", "this.oSC")
  this.zCloneProps("this.oGroup", "this.oTotal")

  *Define what will cause Group to show and what will cause a new group for each Group.
  *Grand Totals
  this.oTotal.GroupTrig     = [.T.]
  this.oTotal.GroupKeyExpr    = []
  this.oTotal.ParentGroupPrefix = []

  *Task Group
  this.oSC.IndentLevel    = 1
  this.oSC.GroupTrig      = [.T.]
  this.oSC.ParentGroupPrefix  = [TOTAL]

  *Week Group (for columnar data)
  this.oWK.IndentLevel    = 2
  this.oWK.GroupTrig      = [this.oData.jta_show_cols = .T.]
  this.oWK.ParentGroupPrefix  = [SC]

  *Date/Report# Group (for columnar data)
  this.oDR.IndentLevel    = 3
  this.oDR.GroupTrig      = [this.oData.jta_show_cols = .T.]
  this.oDR.ParentGroupPrefix  = [WK]

  *Each Detail1
  this.oDetail1.IndentLevel = 4
  this.oDetail1.GroupTrig   = [.T.]
EndFunc


*+-------------------------------------------------------------------------------
*| zCloneProps()
*+-------------------------------------------------------------------------------
Function zCloneProps(cFrom, cTo)
  Local i as Integer

  =AMEMBERS(aProps, &cFrom)

  For i = 1 to Alen(aProps,1)
    AddProperty(&cTo, aProps[i], evaluate(cFrom+"."+aProps[i]))
  EndFor
EndFunc
ENDDEFINE


After sitting through 3 fantastic sessions with Doug Hennig and seeing how flexible/extensible reporting has become in VFP9 I would make 1 comment based upon my feeling during the sessions and after seeing the example above - it is great that the reporting system is so flexible that it allows you to update tables as part of the report processing but should a report be doing that ? That's not what they are designed for. Shouldn't business/data behaviour be elsewhere ?

I think that there is a danger that some developers will be so impressed with the new reporting system that they run the risk of forgoing good design in favour of having fun with cool new features. -- Jamie Osborn

Good point Jamie, but nothing new there right? VFP itself has long been case-study in giving people more than enough rope to hang themselves. It's the back stroke of the fact that the capabilities that provide productivity cut both ways. This is one of the reasons, IMO, that knowledge of good design fundamentals trumps almost any other knowledge category for programmers. All the more so for VFP. - ?lc

some sample code to center an image in a report.

Include this in your listener (requires the _gdiplus class library).
(n.b. Set the image control's 'control source type' to 'expression or variable name' and put a variable or fieldname containg the image file's path in brackets as the control source.)


FUNCTION RENDER(nFRXRecno, nLeft, nTop, nWidth, nHeight, nObjectContinuationType, cContentsToBeRendered, GDIPlusImage)
	LOCAL ll2bCentered,lnScale,lnSaveLeft,lnW,lnH
	IF FILE(cContentsToBeRendered) AND INLIST(LOWER(JUSTEXT(cContentsToBeRendered)),"jpg","bmp","png","gif","tif")
*!* if a file is to be printed cContentsToBeRendered will be the filename
		SET DATASESSION TO (THIS.FRXDATASESSION)
		GO nFRXRecno IN FRX
		ll2bCentered = FRX.DOUBLE
		lnScale = FRX.GENERAL
		SET DATASESSION TO (THIS.CURRENTDATASESSION)

		IF ll2bCentered
*!* we checked the 'center image' checkbox in report designer
			LOCAL loimg,lnW,lnH,lnSaveLeft
			loimg = NEWOBJECT("gpimage","classes\_gdiplus.vcx")
			loimg.createfromfile(cContentsToBeRendered)
			m.lnW = loimg.imageWIDTH
			m.lnH = loimg.imageHEIGHT
			RELEASE loimg

			IF lnW < nWidth/10 OR lnH > nHeight/10
				lnSaveLeft = nLeft
				lnsavetop = ntop     && also save vertical position (W.S.)

				IF lnScale = 1 && scale and maintain aspect ratio
*					IF lnH <> nHeight/10
*!* vfp will try and fill the container so the width may grow if there is room
*!* or if height is too much for container the width will get smaller
*!* so allow for it now
*						lnAdj = (nHeight/10)/lnH
*						m.lnW = m.lnW*m.lnAdj
*					ENDIF

					**** start W.S. insertion ****
	                                * calculate vertical and horizontal scaling factor,
					* then take the smaller to adjust image size to frame
					lnadj_v = (nheight/10)/lnh
					lnadj_h = (nwidth/10)/lnw
					lnadj = min(lnadj_h,lnadj_v)
					m.lnw = m.lnw * m.lnadj
					m.lnh = m.lnh * m.lnadj
					**** end W.S. insertion ****

				ENDIF

				* horizontal centering
				nLeft = nLeft+((((nWidth/10)-lnW)/2)*10)
				IF nLeft < lnSaveLeft
					nLeft = lnSaveLeft
				ENDIF

                                * vertical centering (W.S.)
				ntop = ntop+((((nheight/10)-lnh)/2)*10)
				if ntop < lnsavetop
				     ntop = lnsavetop
				endif

			ENDIF
		ENDIF
	ENDIF

	*-- Added by CathyPountney
        NODEFAULT
        *--

	DODEFAULT(m.nFRXRecno, m.nLeft, m.nTop, m.nWidth, m.nHeight, m.nObjectContinuationType, m.cContentsToBeRendered, m.GDIPlusImage)
	RETURN
ENDFUNC

(Nigel Gomm)

Hi Nigel,
thanks for this procedure. I had this problem, and it helped me dig into the report object universe! Only two things to improve:
1) critical: NODEFAULT missing at the beginning of the function
2) trivial: you didn't handle vertical centering
I took the liberty to insert the missing lines into your function. Hope you don't mind.
(Werner S.)

Note. As Werner and Cathy have pointed out the NODEFAULT is necessary (and i had it in my production code {sigh}). The render method does not behave like other classes' overridden subclassed methods and will execute even if not explicitly called... the reportwriter seems to think of itself more as an innate windows event and thus needs the NODEFAULT.

Contributors: Paul James
frx2xml2html_uploaded.htm

  • This is a program in VFP 9 to report output to xml and then to html
*!*    When any VFP 9.0 report uses HTML output, the VFP 9.0 report system creates an XML
*!*    output file and then uses one XSLT document (generic for all report outputs) to
*!*    convert that XML to HTML.

*!*    First step
*   ----------
*!*    Extracting the VFP 9.0 report system XSLT document
*!*    Downloaded from: http://blogs.msdn.com/b/klevy/archive/2005/10/25/484951.aspx

LOCAL oHTMLListener,cXSLT
oHTMLListener=NEWOBJECT("HTMLListener",HOME()+"FFC\_ReportListener.vcx")
cXSLT=oHTMLListener.XsltProcessorUser.Stylesheet.xml
STRTOFILE(cXSLT,"VFPRptHTML.xsl")

*!*    Second step
*   -----------
*!*    Creation of xml report from the Vfp 9 report(.FRX) file

USE table_name  && on which the report is based
SELECT table_name

oxml = NEWOBJECT("xmlListener", "_reportlistener")
WITH oxml
    .xmlmode = 2 && For data and layout both
    .IncludeFormattingInLayoutObjects = .T.
    .TargetFileName = 'Any_Name.xml'
ENDWITH

REPORT FORM Your_Report.frx OBJECT oxml

USE IN SELECT("table_name")

*!*    Third step
*   ----------
*!*    Creation of html file from the Any_Name.xml file
    
DO (_reportoutput) WITH 5
_oReportOutput["5"].xsltProcessorRdl = GETFILE("xsl")   && i.e. the "VFPRptHTML.xsl" file
chtml = _oReportOutput["5"].applyXslt("Any_Name.xml", _oReportOutput["5"].xsltProcessorRdl, NULL)
STRTOFILE(chtml,"Any_Name.htm")

*Opens the generated HTML document in the Browser
loIE = NEWOBJECT("InternetExplorer.Application")
loIE.Navigate("Any_Name.htm")
loIE.Visible = .T.

RETURN

Category VFP Reports Category 3 Star Topics Category Code Samples
Meta Description: Visual FoxPro Report Listener
Meta Keywords: Visual FoxPro, Report Listener
( Topic last updated: 2011.07.18 11:30:19 AM )