Archive for September, 2009

Working with Multiple Data Records

The XFA processor has a notion of data records.  A record is the data that populates one instance of your form.  If you are designing interactive forms, you have probably never had more than one data record in your form.  The notion of multiple records is primarily for processing large print jobs in LiveCycle Print.  Consider the case where you want to print customer statements.  You extract an XML file from your database with the data for all 2000 customers.  Then in a single call to LiveCycle Print you print all 2000 records.

I won’t go into all the details of setting up a multi-record print.  My intention today is to show how you can leverage multiple record support in interactive forms.

Data Record

Suppose you have a form that binds to a root data node named: "form1".  When there is a single data record, your data looks like this:

<xfa:datasets xmlns:xfa="http://www.xfa.org/schema/xfa-data/1.0/">
   <xfa:data
>
      <form1>…</form1>
   </xfa:data>
</xfa:datasets>

In the simple case, a form with three records will look like:

<xfa:datasets xmlns:xfa="http://www.xfa.org/schema/xfa-data/1.0/">
   <xfa:data>
      <form1>…</form1>
      <form1>…</form1>
      <form1>…</form1>
   </xfa:data>
</xfa:datasets>

But by default Acrobat/Reader will display only the first record.  But with a few simple script commands you can navigate to different records as well as add and remove records.

Script Expressions

The Current Record
The current data record is easy to find.  We have set up a convenience property: xfa.record.

Record Count
xfa.record.parent.nodes.length;

Current Record Number
xfa.record.index;

Record Count
xfa.record.parent.nodes.length;

Strictly speaking, this will not always return the record count… but that’s in the deep end.

Add a Record
var newRecord = xfa.datasets.createNode("dataGroup", xfa.record.name);
xfa.record.parent.nodes.append(newRecord);

Remove a Record
var vRecordToRemove = xfa.record.parent.nodes.item(xfa.record.index);
xfa.record.parent.nodes.remove(vRecordToRemove);

Goto a Record
xfa.datasets.dataWindow.gotoRecord(nRecord);

Sample

I have attached a sample that exercises all these script commands.

Exporting XML data

One thing you will notice when you export or submit data as XML, the result will change depending on if there is one or more than one record included.  From the example above, if I export as XML and there’s one record I’ll get:

<form1> … </form1>

If I export a data with 3 records, we need to add an aggregating element so that it remains valid XML:

<xfa:data xmlns:xfa="http://www.xfa.org/schema/xfa-data/1.0/">
    <form1>…<form1>
    <form1>…<form1>
    <form1>…<form1>
</xfa:data>

Changing the Current Record

You need to be aware that when you call dataWindow.gotoRecord(), the XFA processor will perform a re-merge with the new data.  Doing a remerge means that you lose any changes you’ve made to your form objects.  e.g. you will lose field highlighting, choice list definitions populated by script etc.

The Deep End

There are some advanced options for handling very large datasets on the server.  If you think about it, a large print job could contain many thousands of records, and the input could be a data file that is many gigabytes.  To handle that case there are options in configuration that allow you to process the file incrementally.  Part of that involves specifying a record element by level or by name.  e.g. you could say that the name of the record node is "customer".  In this sample XML:

<customerRecords>
  <jobDetails>…</jobDetails>
  <customer> … </customer>
  <customer> … </customer>
  <customer> … </customer>
  <customer> … </customer>
</customerRecords>

there are 4 records, but the <customer> parent element has 5 children.  That’s why, strictly speaking, the expressions: xfa.record.index; and xfa.record.parent.nodes.length; are not always reliable.

But if you haven’t set the config file options, then there’s no problem.

Data Window

To round out the story of data records, we need to say a bit more about incremental processing — commonly referred to as "lazy loading".  The idea is that the XFA processor holds only a small number of data records in memory.  As the XFA processor loads the data file it progressively loads data records into a window and when they’ve been processed they are discarded.  The window can specify how many records before and after the current record are kept in memory.  In this way we can process a multi-gigabyte data file without loading it all into memory.

As mentioned, there are configuration options for setting up the data window behaviour as well as a dataWindow scripting object.  But these details would need to be the topic of another blog entry.

XFA Scripting API Reference

Some time ago we had an intern build a flex application that gave internal people a nice reference for the XFA scripting object model.  Our nightly build process generates XML files that describe all the objects, properties and methods available within XFA scripting.  This flex application loads those files and gives a very convenient tree view where we can drill into the details of the object model.  Any time the build is broken and this application isn’t available, I suffer withdrawal symptoms.  So I figured maybe you would find it useful as well.  I have taken the flex application, changed it up so that it works stand-alone and I’ve embedded it in a PDF.  The result is ScriptReference.pdf. (Warning: I encountered some strangeness when I tried to launch this pdf directly from firefox.  Probably best to right-click/download and then launch.)

ScriptReference.pdf

The reference divides up the objects among the various top-level models in XFA (form, template, data, etc).  The way you use it is that you select the object you are interested in — e.g. the field object under the form category — and then the right hand side of the page will display all the properties and methods available. 

Notice that the reference exposes some of our object hierarchy — each object has a parent class.  The display shows only the properties and methods for the current object.  If you want to see the properties and objects inherited from the parent class, then click on the parent object class.

Under the "XFA" category, you’ll find a miscellaneous list of objects, including the classes that occur higher in the class hierarchy.

The set of descriptors needs a bit of explanation.  For Properties:

type: The type of content (String, Object, Bool, integer etc).  Some properties may say "Multiple" because the property type changes depending on the context.  e.g. field.rawValue returns a string for text fields, but a number for numeric fields.

Based on schema: yes/no: indicates whether this property is exposed as a result of being part of the XFA schema.  i.e. part of the file format.  This value will be "no" for properties such as field.fillColor (a shortcut property) or field.formattedValue (a calculated value).

access: GET or SET or both. i.e. whether this property is read-only or if it is read-write.  Note that objects in the template may tell you that they are ‘set’able, but if you try to set them in Acrobat you’ll get an error.  This is because the template is read-only in Acrobat — but is read/write in other contexts — specifically on the server.

version: The XFA version where this property was introduced.

For methods, the form shows the method signature, displays the version number and describes the parameters.

Search

At the bottom left you will see a couple tabs where you can enter a search string.  When searching by name, the tree list will get filtered to those objects/properties that match the given name.  When searching by version, the tree list gets filtered to those objects/properties that match the specified version.

Warning: developer-speak

The descriptions of the objects, methods and properties are exactly as they were written by the developers who implemented the functionality.  What you probably already know about developers is that we not always good grammar writing.  The documentation you get with Designer has been produced by our talented bilingual tech writers who can translate from developer-speak to English.  So take the descriptions with a grain of salt, and if you find them confusing, consult the real documentation.

Check for New Version

Notice the "Check for New Version" button.  When we release a new version of XFA inside Acrobat/Reader/LiveCycle I will need to produce an update.  Or maybe I’ll fix a bug.  Periodically you should click the button and find out if the copy you have is the latest.

Script Objects: Deep Dive

Anyone looking to reduce code repetition in their forms will be familiar with script objects.  Script objects are a way to group functionality in a single location and share it from multiple places.  Most users will localize some logic in a series of JavaScript functions.  Advanced JavaScript programmers will try to do more.  And at that point they will notice some quirks around script object usage.  In this blog entry I’ll give a little background as to how script objects are implemented internally and also how advanced programmers can use them and still flex their programming skills.  We’re pretty much in the deep end here.  If you’re a novice/intermediate script writer, you’re not the target audience for this information :-)

Managing Script Objects

An XFA script object is very much like a JavaScript "class" (where a class is represented as a function).  When you create a JavaScript class object you might code something like this:

function Rectangle() { 
    this.width = "0in";
    this.height = "0in";
    this.getInches = function(measurement) { … }
}

The user of this class can then write code such as:

var rect = new Rectangle();
rect.width = "8cm";
rect.height = "4cm";
var area= rect.getInches(rect.width) * rect.getInches(rect.height);

The script object equivalent would look like:

form1.#variables[0].Rectangle – (JavaScript, client)

var width = "0in";
var height = "0in";
function getInches(measurement) { … }

and your calculation using the script object would look like:

form1.NumericField1::calculate – (JavaScript, client)
Rectangle.width = "8cm";
Rectangle.height = "4cm";
var area= rect.getInches(rect.width) * rect.getInches(rect.height);

Notice two things that the form author didn’t have to worry about: creating the script object and exposing properties for external access (prefixing members with "this.".  The internal machinery to manage the script object makes authoring easy.  However, this machinery can get in the way of advanced users.  But hopefully by the end of this entry the advanced users will have enough new data to accomplish what they want as well.

Creating Script Objects

Each script object gets created the first time it is referenced. If there are no form scripts that use the script object, it will never get created.  Note also that if your script object is housed inside a repeating subform, we will create one instance of that script object for each instance of the repeating subform.

There are a couple of things you want to be aware of related to the creation of script objects:

1. When a script object is created, Reader will execute the body of code in the object.   This is where any variable initialization happens.  In our example, this is where width and height get initialized.

2. If there are problems in your script object code and if executing the code results in a JavaScript exception, the script object will not initialize correctly. Depending on the error, it might be partially initialized or it might fail completely. Unfortunately, Reader is inconsistent in reporting errors during initialization.  You might not get a message in your console and the symptom will be that any script that references the script object will get an error such as:

Rectangle.getInches() is not a function
2:XFA:form1[0]:NumericField1[0]:calculate

The reason for this error is that because of the initialization failure the script object didn’t expose
getInches() as a function.

Exposing Properties

It is a bit much to expect novice (or intermediate) form authors to understand the need to define object properties in order to use them externally.  For the 95% use case, the form author just wants to define some functions and reference them from other script.  To make this easy, the XFA processor examines the JavaScript looking for function and variable declarations.  For each declaration, it exposes the declared object as a property.  The actual internal mechanism for exposing properties varies between client and server.  On the server, the script code gets modified to add declarations at the end:

form1.#variables[0].Rectangle – (JavaScript, client)
var width = "0in";
var height = 0in";
function getInches() { … }; 

this.getArea=getArea;
this.width=width;
this.height=height;

On the client (Reader/Acrobat), the properties are exposed by registering them with the internal script object.

Script Object Behaviours

References to "this"

As mentioned in previous posts, we use different JavaScript engines on the client and on the server.  If you are pushing on the boundaries of script object functionality, you will notice some differences in the two implementations. 

  • On the client (Reader/Acrobat), references to "this" inside a script object refer to the subform hosting the script object.
  • On the server, "this" evaluates to the script object itself

Dynamic Properties

On the client, a script object can be dynamically extended with new properties (even with strict scoping on).  On the server a script object cannot be dynamically extended with new properties

Naming

Don’t give script object functions the same name as the script object.  eg. don’t create a function named "foo" inside a script object named "foo".  The name lookup mechanism will fail to resolve this correctly.  You’ll get an error such as:

form1.foo.foo() is not a function
1:XFA:form1[0]:NumericField3[0]:calculate

Using the new operator

If you want to define a JavaScript ‘class’ in your script object, you need a workaround to manage it correctly.

eg. suppose your script object defines a rectangle:

form1.#variables[0].R – (JavaScript, client)
function Rectangle(width, height) {
  this.w = width;
  this.h = height;
}

In your script you’d like to create an instance of this rectangle.  Ideally you would simply code:

var rect = new R.Rectangle(4,5);

Unfortunately, the new operator does not work in this context.  The workaround is to include a helper function in your script object:

form1.#variables[0].R – (JavaScript, client)
function Rectangle(widt
h, height) {
  this.w = width;
  this.h = height;
}
function newRectangle(width, height) {
    return new Rectangle(width, height);
}

In order to create a Rectangle object your code can call:

var rect = newRectangle(4,5);

Exposing Variables as Properties

If you have variables that you want to use both within your script object *and* expose outside your script object, you need to be careful how you reference them.  Consider this example:

form1.#variables[0].R – (JavaScript, client) 
var width=0;
var height=0; 
function area() {
    return width * height;
}

As described above, on the server, this gets modified to:

form1.#variables[0].R – (JavaScript, client) 
var width=0;
var height=0; 
function area() {
    return width * height;
}
this.width = width
this.height = height;
this.area = area;

While this code addition nicely exposes the properties to the rest of the form, in doing so we create a copy of the width and height variables.  We now have both variables and properties named width and height.  When the width and height are referenced outside the script object, it is the property versions that are used.   However, when referenced inside the script object, it is the copies declared as variables that are referenced.  For the example above, on the server an external script could modify R.width and R.height, but the area() function will always return zero.  On the client because we expose the properties using a different mechanism, the external and internal references both access the property versions of the object.  In order to make your script work reliably on both client and server, make sure that references within the script object always predicate with "this":

form1.#variables[0].R – (JavaScript, client) 
var width=0;
var height=0; 
function area() {
    return this.width * this.height;
}

Variables in Script Objects

If you want to use variables in script objects that are preserved during your forms session, you need to be aware of variable scope issues.  These are described in previous blog entries: Scope of JavaScript Objects and Form Compatibility.

Design Pattern 1

Not happy with the way the XFA processor is managing your script object?  There is a workaround that allows you to regain some control.  Personally, I’m most interested in keeping my variables private.  Those of us who have written code in C++ and Java have always been encouraged to follow this pattern.  You provide the outside world access to your private members through accessor functions/methods.

The code in Reader that’s parsing your script looking for declarations can be fooled.  When the parser encounters a brace, it stops looking for declarations to expose.  If you enclose parts of your script with braces, you will have more control over how your class is handled. This is best explained by example:

form1.#variables[0].Rectangle – (JavaScript, client) 
{   // Opening brace causes parser in Reader to ignore contents

    // Declare these variables and keep them private
    // calcs (in Reader) will not be able to access Rectangle.width
    var width = 0;
    var height = 0; 
}
function setW(W) { width = W; }
function setH(H) { height = H; }
function getW()  { return width;  }
function getH()  { return height; }
function area()  { return width * height; }

Code referencing this script object can look like:

Rectangle.setH(4);
Rectangle.setW(5);
this.rawValue = Rectangle.area();

Note that this code works on the client and server — however on the server, the XFA processor discovers the variables without performing a parse — we use a function on the script engine itself.  The server script engine is not fooled by our trickery.  The impact is that on the server, code will continue to be able to access Rectangle.width and Rectangle.height.  As long as your form is tested on both client and server you might be willing to live with this behaviour.

Design Pattern 2

A second technique works a bit harder to hide member variables:

form1.#variables[0].Rectangle – (JavaScript, client) 
function R() {

    // Declare these variables and keep them private 
    var width = 0;
    var height = 0;

    // Declare these functions and make them public
    this.area = function() {return width * height;}

    // Provide accessors to private variables
    this.setW = function(W) { width = W; }
    this.setH = function(H) { height = H; }
    this.getW = function()  { return width;  }
    this.getH = function()  { return height; }
}
{   // add braces to hide this declaration in Reader

    var Rect = new R();
}
function setW(W) { Rect.setW(W); }
function setH(H) { Rect.setH(H); }
function getW(W) { return Rect.setW(W); }
function getH(H) { return Rect.setH(H); }
function area()  { return Rect.area(); }

Notice that the variable Rect will end up exposed on the server, but since it has kept its private members hidden, other scripts will not be able to modify the object (although on the server the entire variable could be replaced).  But this is a good design pattern for a coup
le of reasons:

  1. On the server when the XFA processor adds the code: this.Rect=Rect; we do not end up with a copy of the variable, because assigning to an object creates a reference, not a copy.
  2. Limiting access to member variables via function calls means that we will not fall into the classic JavaScript trap where we inadvertently assign a new property to an object when we misspell the member name.