Visual FoxPro 9.0 provides extended metadata attributes for class members in the Class Designer using XML in a document format specified by the VFP MemberData Schema. While the Visual FoxPro Report System does not share the Class Designer, it shares the MemberData schema for similar extensibility purposes. Using MemberData extensions with reports, you can:
-
Specify custom design-time instructions for individual report layout elements, for use by through event hooks exposed by the Report and Label Designers.
-
Provide dynamic instructions for individual report layout elements that can be carried out by a ReportListener-derived object at run time.
This topic describes the MemberData schema components used for reporting, and provides examples using MemberData XML documents when you design and run reports.
Report XML MemberData Document Design
Report MemberData, like Class Design MemberData, contains a sequence of elements under a VFPData
root node. For a complete listing of the shared MemberData schema (.xsd), see MemberData Extensibility. The following table describes the attributes specified for reporting use.
![]() |
---|
The schema permits users to add attributes not explicitly specified. Any tools you create to parse or create the XML should allow for the possibility that additional, unknown attributes are present for one or more elements in the sequence. Tools should also handle the possibility that attributes not marked as required by the schema might be missing for one or more elements. The run-time example in this topic provides a model for this behavior. |
The Style column of report and label definition files (.frx and .lbx tables) is reserved for storage of Report MemberData. For more information on the structure of reports and labels, see Understanding and Extending Report Structure.
You can use the default Report Builder Application to insert XML documents of the required structure into report and label records for individual layout elements. You can examine the results, to see Report XML MemberData example documents. For more information, see How to: Assign Structured Metadata to Report Controls.
Node name | Node type | Parent node | Remarks and recommended usage | ||
---|---|---|---|---|---|
|
Element (required) |
None |
Root node for the MemberData document. |
||
|
Element (required) |
|
An element containing metadata for a specific report or label definition table record. |
||
|
Attribute (required) |
|
Shared with Class Designer MemberData. Can be used, with the type attribute, to filter and manage reporting MemberData Records in a common global store, such as FoxCode.dbf. For more information, see _FOXCODE System Variable.
|
||
|
attribute |
|
Shared with Class Designer MemberData. Can be used, with the name attribute, to filter and manage records in a global store.
|
||
|
attribute |
|
Shared with Class Designer MemberData, and specified, similarly to its use in class design, for use in reporting design-time extensions. |
||
|
attribute |
|
Specified to hold the name of a class to be used for any and all of the following:
|
||
|
attribute |
|
Specified to hold the name of the class library or procedure file (.vcx or .prg) from which the helper or template class will be instantiated. Reporting extensions should assume a file extension of |
||
|
attribute |
|
Specified to hold the name of a DataEnvironment class in a visual class library (.vcx) to be used as a template for Cursor and Relation records in this report or label in the default Report Builder Application's implementation of the Load DataEnvironment event. The Report Builder Application also writes code binding an instance of this class to the report's run-time DataEnvironment events. For more information, see How to: Load Data Environments for Reports.
|
||
|
attribute |
|
Specified to hold the filename of the visual class library or procedure file from which the DataEnvironment class should be instantiated, when the |
||
|
attribute |
|
Specified for script used in run-time reporting extensions. |
||
|
attribute |
|
Specified for conditions to be evaluated by run-time reporting extensions to determine if, and when, the script contents of the
|
Report XML MemberData at Design Time
As described in the table above, the Report Builder Application uses the declass
and declasslib
attributes of the Report XML MemberData stored in the header record of the report or label definition file. By default, it does not use any other MemberData you store in the report or label. However, you can easily leverage the Report Builder Application's extension architecture to read and use the XML.
The following class implements the Report Builder Application's exit handler mechanism. When registered with the Report Builder Application, this class receives information about the Report Designer event that occurred and whether the Report Builder Application made any changes to the current report layout element. If the layout element contains text, and if changes occurred, the handler object checks for a template class in the XML MemberData. If one exists, and if it has a property with the name Fontname
, the handler asks the user whether the template class's font should be applied to the report layout element.
![]() |
---|
For more information on registering custom handlers and filters in the Report Builder Application's registry table and implementing the required API, see Report Builder Event Handler Registry Table. Use of the exit handler mechanism is convenient, but illustrates only a fraction of the potential you have for design-time interactions using Report Designer event hooks and Report XML MemberData. |
В | ![]() |
---|---|
DEFINE CLASS TemplateObjectHandler AS Custom * register this as an exit handler PROCEDURE Execute( oEvent ) IF BITTEST(oEvent.ReturnFlags,1) AND ; INLIST(FRX.ObjType,5,8) AND NOT EMPTY(FRX.STYLE) LOCAL lcAlias, loX lcAlias = "T"+SYS(2015) TRY XMLTOCURSOR(FRX.Style,lcAlias) SELECT (lcAlias) IF NOT EMPTY(Class) AND ; MESSAGEBOX("(Re)apply Font from Template Object?",4) = 6 IF EMPTY(ClassLib) loX = CREATEOBJECT(ALLTRIM(Class)) ELSE loX = NEWOBJECT(ALLTRIM(Class),ALLTRIM(Classlib)) ENDIF IF VARTYPE(loX) = "O" AND ; PEMSTATUS(loX,"Fontname",5) REPLACE Fontface WITH loX.Fontname IN FRX ELSE MESSAGEBOX("Could not apply template.") ENDIF ENDIF CATCH WHEN .T. * not valid XML * or other error occurred FINALLY IF USED(lcAlias) USE IN (lcAlias) ENDIF SELECT FRX ENDTRY ENDIF RETURN .T. ENDPROC ENDDEFINE |
Report XML MemberData at Run Time
The following example leverages the run-time effects processing architecture suggested in Considerations for Creating New Report Output Types.
The superclass in the example, FXMemberData is a custom-derived class implementing the simple FX API, and suitable for being called from an instance of the FXListener class, as described in that topic. FXMemberData is an effect object with the ability to read and parse Report XML MemberData, placing the results in a cursor indexed on a column, FRXRecno
, associating each record with the elements of the original report or label definition table.
FXMemberData performs its service at the beginning of the report run. For the rest of the report run, if the value of its ApplyMemberData
member is set to True (.T.
), FXMemberData positions the record pointer of its cursor appropriately for the current report event, but does not take any other action.
Having this type of object avoids the necessity of having each ReportListener or effect object requiring access to the structured metadata parse the XML document separately. With the metadata easily accessible to them in this common cursor, each object using the data can SELECT the columns of interest to it into a separate, private cursor, with additional columns for dynamic changes they might want to make at run time for their own purposes.
![]() |
---|
To find the relevant cursor created by FXMemberData or a similar provider, other objects can investigate the FRXDataSession to find a cursor of the correct structure, parsing the XML themselves if they cannot find one. Alternatively, they can observe a simple convention, shown here as MemberDataAlias , of a ReportListener property holding the appropriate alias. Notice that FXMemberData uses the AddProperty Method to attach this property to any ReportListener.
|
A second class derived from FXMemberData, FXProcessMemberDataScript, shows you some strategies for using the Execute
and ExecWhen
attributes of Report XML MemberData. This class evaluates ExecWhen
to determine when to invoke the script in Execute
. If it determines that it should process the script, it checks to see if a PARAMETERS
or LPARAMETERS
line is the first line in the script. If not, it prepends a LPARAMETERS
statement to the script, to allow it to pass all the parameters received by the ApplyFX method of the effect class API, which effectively allow effect objects to handle and adjust all report events' parameters. The LPARAMETERS
statement it creates also includes a reference to the FX object as the first parameter, before all the parameters received in the ApplyFx method. Having made these adjustments, it uses the EXECSCRIPT( ) Function to process the script, passing these parameters to the script.
В | ![]() |
---|---|
* to use this class, follow the pattern * illustrated for use of the FXListener class, * as follows : LOCAL loPrimaryRL * the following line assumes availability of * the class definition for FXListener listed in * code example in the topic * Considerations for Creating New Report Output Types: loPrimaryRL = CREATEOBJECT("FXListener") * add successor ReportListeners if desired loPrimaryRL.FXs.Add("FXProcessMemberDataScript") * or use its superclass if you don't * need script processing but want to * parse the XML MemberData: * loPrimaryRL.FXs.Add("FXMemberData") * add other effect objects to the collection * as required * Effect class suitable for calling * by FXListener example class * Because it works with unknown attributes * and unknown memberdata requirements, * FXMemberData requires that the values you use * for all custom attributes be evaluated by * XMLTOCURSOR() as a string. Values * that do not evaluate as a string will error. * This behavior makes it possible * for you or other users to use multiple data types * for the same custom attributes on different FRX * records. * When you use a non-string value, you should * re-datatype it as appropriate for use in your code. * (Use EVAL() or otherwise translate the data * type as needed.) DEFINE CLASS FXMemberData AS Custom MemberDataAlias = "" ApplyMemberData = .F. PROCEDURE ApplyFX(toListener, tcProgram,; tP1, tP2, tP3, tP4, tP5, tP6, ; tP7, tP8, tP9, tP10, tP11, tP12) LOCAL liSession, liSelect, llInBeforeReport llInBeforeReport = (ATC("BeforeReport", tcProgram) > 0) IF (llInBeforeReport OR THIS.ApplyMemberData) AND ; (TYPE("toListener.FRXDataSession") = "N" AND ; toListener.FRXDataSession > -1) liSession = SET("DATASESSION") SET DATASESSION TO (toListener.FRXDataSession) liSelect = SELECT() IF llInBeforeReport * pull the memberdata out of the FRX for later use THIS.PullMemberData(toListener) ENDIF IF THIS.ApplyMemberData * this FX object * might apply the results of * the memberdata pull * or it might just make them * available to other FX objects * after the initial read SELECT (THIS.MemberDataAlias) THIS.UseMemberData(; toListener, tcProgram,; @tP1, @tP2, @tP3, @tP4, @tP5, @tP6, ; @tP7, @tP8, @tP9, @tP10, @tP11, @tP12) ENDIF SELECT (liSelect) SET DATASESSION TO (liSession) ENDIF ENDPROC PROTECTED PROCEDURE UseMemberData(toListener, tcProgram,; tP1, tP2, tP3, tP4, tP5, tP6, ; tP7, tP8, tP9, tP10, tP11, tP12) LOCAL lnFRXRecno lnFRXRecno = -1 DO CASE CASE ATC(".Before",tcProgram) > 0 OR ATC(".After",tcProgram) > 0 DO CASE CASE RAT("REPORT",UPPER(tcProgram)) = (LEN(tcProgram)-5) lnFRXRecNo = 1 * pull global data CASE VARTYPE(tP2) = "N" && Band events lnFRXRecNo = tP2 OTHERWISE * called inappropriately ENDCASE CASE VARTYPE(tP1) = "N" && Render, other events lnFRXRecno = tP1 OTHERWISE * called inappropriately ENDCASE IF NOT SEEK(lnFRXRecno,THIS.MemberDataAlias,"FRXRecno") lnFRXRecno = -1 ENDIF RETURN (lnFRXRecno # -1) ENDPROC PROTECTED PROCEDURE PullMemberData(toListener) LOCAL lcAlias, lcTempAlias, lcAttributes, liIndex, loAttr IF TYPE("toListener.MemberDataAlias") = "C" AND ; NOT EMPTY(toListener.MemberDataAlias) lcAlias = toListener.MemberDataAlias ELSE lcAlias = "M"+SYS(2015) toListener.AddProperty("MemberDataAlias", lcAlias) * "publish" this for others in case they want it ENDIF THIS.MemberDataAlias = lcAlias lcTempAlias = "T" + SYS(2015) CREATE CURSOR (lcAlias) ; (FRXRecno I, Name M, Type M, ; ExecWhen M, Execute M, Class M, ; ClassLib M, DEClass M, DEClassLib M) * we're going to take every attribute, whether * we understand the column or not, * but we'll start off with the * core set minus script since script attribute is * officially reserved for design-time use lcAttributes = ; "|FRXRecno|ExecWhen|Execute|Class|" + ; "Classlib|Name|Type|DEClass|DEClassLib|" SELECT FRX SCAN FOR NOT EMPTY(Style) TRY XMLTOCURSOR(Style,lcTempAlias) CATCH WHEN .T. * not valid XML FINALLY IF USED(lcTempAlias) IF RECCOUNT(lcTempAlias) > 0 SELECT (lcTempAlias) FOR liIndex = 1 TO FCOUNT() IF ATC("|"+FIELD(liIndex)+"|",lcAttributes) = 0 ALTER TABLE (lcAlias) ; ADD COLUMN (FIELD(liIndex)) M lcAttributes = lcAttributes + ; FIELD(liIndex) + "|" ENDIF ENDFOR SCATTER MEMO NAME loAttr INSERT INTO (lcAlias) FROM NAME loAttr REPLACE FRXRecno WITH RECNO("FRX") IN (lcAlias) ENDIF USE IN (lcTempAlias) ENDIF ENDTRY loAttr = NULL ENDSCAN SELECT (lcAlias) INDEX ON FRXRecno TAG FRXRecno ENDPROC ENDDEFINE DEFINE CLASS FXProcessMemberDataScript AS FXMemberData ApplyMemberData = .T. PROTECTED PROCEDURE UseMemberData(toListener, tcProgram,; tP1, tP2, tP3, tP4, tP5, tP6, ; tP7, tP8, tP9, tP10, tP11, tP12) IF DODEFAULT(toListener, tcProgram,; @tP1, @tP2, @tP3, @tP4, @tP5, @tP6, ; @tP7, @tP8, @tP9, @tP10, @tP11, @tP12) * We are now positioned on the correct * record by the parent class, * and can take action based on the memberdata contents. * For example, if we're in BeforeReport, * we could instantiate a collection of the appropriate * template objects for each label or text record that has a * class and classlib available. * For each EvaluateContents or Render * event we can call methods of the class or * apply font attributes to the runtime result. LOCAL loMemberdata, llExecute SCATTER MEMO NAME loMemberdata IF NOT EMPTY(loMemberdata.Execute) IF EMPTY(loMemberdata.ExecWhen) llExecute = .T. ELSE DO CASE CASE ATC(loMemberData.ExecWhen,tcProgram) > 0 * simple event evaluation * ExecWhen contains an event name * Note that each event, via script, * could potentially change the contents of * ExecWhen to hold another value (the next * event during which this script * should be evaluated) llExecute = .T. CASE (TYPE(loMemberdata.ExecWhen) = "L") AND ; EVALUATE(loMemberdata.ExecWhen) * ExecWhen contains a logical expression * to be evaluated llExecute = .T. CASE ATC(SUBSTR(tcProgram,RAT(".",tcProgram) + 1),; loMemberData.ExecWhen) > 0 * ExecWhen contains a delimited string of events llExecute = .T. ENDCASE ENDIF IF llExecute IF NOT (BETWEEN(ATC("PARAM", ; ALLTRIM(CHRTRAN(loMemberData.Execute,; CHR(10)+CHR(13), ; SPACE(2)))),1,2)) * add a parameters statement; this adjustment * should just happen the first time * any FRX record is processed. loMemberData.Execute = ; "LPARAMETERS toFX, toListener, tcProgram,;"+ ; CHR(13) + CHR(10) + ; "tP1, tP2, tP3, tP4, tP5, tP6,"+; "tP7, tP8, tP9, tP10, tP11, tP12" + ; CHR(13) + CHR(10) + ; loMemberData.Execute REPLACE Execute WITH loMemberData.Execute ENDIF ExecScript(loMemberData.Execute,; THIS, toListener, tcProgram,; @tP1, @tP2, @tP3, @tP4, @tP5, @tP6, ; @tP7, @tP8, @tP9, @tP10, @tP11, @tP12) ENDIF ENDIF ENDIF ENDPROC ENDDEFINE |
To use a script-processing effect of this nature, you could add the following XML MemberData document to a Field or Expression control in the report layout holding a numeric value. This example provides automatic color changes for numeric values below 0, using the EvaluateContents event, and formats the negative numbers with parentheses, using the Render method.
![]() |
---|
Notice that the ExecWhen attribute indicates the script should be processed at these two points by specifying a delimited string ("|EvaluateContents|Render| "). This is one of several alternative means of evaluating ExecWhen provided by the FXProcessMemberDataScript class.
|
В | ![]() |
---|---|
<VFPData> <reportdata name="" type="R" script="" execwhen="|EvaluateContents|Render|" execute= "DO CASE CASE ATC("Render",tcProgram) > 0 * Render's 7th parameter is * cContentstoBeRendered * notice the conversion from Unicode to DBCS tP7 = VAL(STRCONV(tp7,6)) IF tp7 < 0 tP7 = "("+TRANS(ABS(tp7)) + ")" ELSE tP7 = TRANS(tP7) ENDIF * convert back to Unicode for use by the native ReportListener: tP7 = STRCONV(tp7,5) OTHERWISE * EvaluateContents' second parameter * is objProperties IF VARTYPE(tP2.value) = "N" AND ; tP2.value < 0 tP2.penred = 255 tP2.penblue = 0 tP2.pengreen = 0 tP2.reload = .T. ENDIF ENDCASE" class="" classlib="" declass="" declasslib=""/> </VFPData> |
![]() |
---|
The well-formed XML document above shows a number of escaped character references within the script; for example, a character such as < must be stored as the entity reference < when it is part of the value of an attribute or element node in XML. When you create the script using the Report Builder Application's Run-time Extensions text box interface, you can type these characters as you normally do, without using the entity references. The Report Builder Application stores the document properly, escaping the characters as necessary, when you save the XML.
|
The following class is another class definition following the FX API. Unlike FXMemberData and its derived classes, FXMemberDataAware does not understand XML and does not read the MemberData directly. Instead, it reads the cursor produced by FXMemberData and understands its structure. If it finds the cursor in its environment, it uses the cursor, adding columns if desired. If the cursor is not available, it provides a temporary instance of FXMemberData during the BeforeReport method, so this temporary object can create the cursor.
FXMemberDataAware is an abstract class, performing no service during the report run. However, you can derive many FX classes from FXMemberDataAware, each with a specialized purpose. If you add instances of each FX to an FXListener's collection, they all share the same MemberData cursor during the report run. They can also create private MemberData extension cursors related to the shared cursor, as needed.
В | ![]() |
---|---|
DEFINE CLASS FXMemberDataAware AS Custom MemberDataAlias = "" HasMemberData = .F. PROCEDURE ApplyFX(toListener, tcProgram,; tP1, tP2, tP3, tP4, tP5, tP6, ; tP7, tP8, tP9, tP10, tP11, tP12) IF ATC("BeforeReport",tcProgram) > 0 THIS.VerifyMemberData(toListener) ENDIF IF ATC("AfterReport",tcProgram) > 0 THIS.DetachMemberData(toListener, .T.) ENDIF ENDPROC PROCEDURE VerifyMemberData(toListener) IF toListener.FRXDataSession = -1 RETURN .F. ENDIF LOCAL loMemberData, liSelect, liSession liSession = SET("DATASESSION") SET DATASESSION TO (toListener.FRXDataSession) liSelect = SELECT() IF (EMPTY(THIS.MemberDataAlias) OR ; (NOT USED(THIS.MemberDataAlias))) * can be supplied by the Listener * by leveraging a different FX object, * but might not be, so in this case * let's scarf it up with a temporary object IF (NOT PEMSTATUS(toListener,"MemberDataAlias",5)) OR ; EMPTY(toListener.MemberDataAlias) THIS.MemberDataAlias = "M" + SYS(2015) toListener.AddProperty("MemberDataAlias", ; THIS.MemberDataAlias) ELSE THIS.MemberDataAlias = toListener.MemberDataAlias ENDIF IF NOT USED(THIS.MemberDataAlias) * could be sharing loMemberData = NEWOBJECT("FXMemberData") loMemberData.MemberDataAlias = THIS.MemberDataAlias loMemberData.ApplyMemberData = .F. loMemberData.ApplyFX(toListener, "BeforeReport") SET DATASESSION TO (toListener.FRXDataSession) IF USED(THIS.MemberDataAlias) * we can proceed... THIS.AlterMemberDataInfo() ENDIF ENDIF ENDIF THIS.HasMemberData = USED(THIS.MemberDataAlias) SELECT (liSelect) loMemberData = NULL SET DATASESSION TO (liSession) RETURN THIS.HasMemberData ENDPROC PROTECTED PROCEDURE AlterMemberDataInfo() * Hook for derived classes * to add their own columns, * or even to create private cursors * in the FRX Data session that * function in relation to the MemberData * shared cursor. ENDPROC PROCEDURE DetachMemberData(toListener, tlCloseMemberDataTable) IF tlCloseMemberDataTable AND toListener.FRXDataSession > -1 LOCAL liSession liSession = SET("DATASESSION") SET DATASESSION TO (toListener.FRXDataSession) IF USED(THIS.MemberDataAlias) USE IN (THIS.MemberDataAlias) IF PEMSTATUS(toListener,"MemberDataAlias",5) toListener.MemberDataAlias = "" ENDIF ENDIF SET DATASESSION TO (liSession) ENDIF THIS.MemberDataAlias = "" THIS.HasMemberData = .F. ENDPROC ENDDEFINE |