Archive for April, 2011

Debug merge and layout: Updated

Almost two years ago I published a blog post with a tool that I developed to help debug data merge and layout problems: http://blogs.adobe.com/formfeed/2009/05/debug_merge_and_layout.html

I continue to use this sample pdf on a regular basis.  It has a reserved location among my desktop icons for quick access.  But occasionally I’d run into PDF forms that cause it to hang.  So finally I took the time to find and fix the problem.  While I was at it, I took care of a couple other minor issues.  The updated form is available for download here.  I hope you find it useful.

Sample: Convert to Strict Scoping

In the past couple of months I’ve had the opportunity to help a couple of customers through migrating a form design so that it was not dependent on non-strict scoping.

First, to review what this means, you could read this blog post describing the problem.

There’s no single technique to convert a form script so that it conforms to strict scoping. But there are a couple of common patterns.  I’ll disclose these by way of an example.

I’ve designed a sample form that makes use of “expando properties” to add multi-step undo/redo functionality to individual form fields.  In this case, the form adds an expando script object to a text field.  For review purposes, the reason this is problematic is that allowing users to modify the JavaScript representations of XFA objects means that the script engine must preserve these JavaScript objects. On large forms, this means we cannot release JavaScript objects to garbage collection and memory usage climbs.

var This = this;

This.ctl = new function() {
    this.history = [This.rawValue],
    this.current = 0,
    this.startValue = null;

    this.undo = function() {
        if (this.current > 0) {
            This.rawValue = this.history[this.current-1];
            this.current--;
        }
    }
    this.redo = function() {
        if (this.current < (this.history.length - 1)) {
            this.current++;
            This.rawValue = this.history[this.current];
        }
    }
    this.enter = function() {
        this.startValue = This.rawValue;
    }
    this.exit = function() {
        if (this.startValue != This.rawValue) {
            this.current++;
            this.history.length = this.current;
            this.history[this.current] = This.rawValue;
        }
    }
};

With this code in place, the enter event can call:

this.ctl.enter();

Similarly, assuming the text field is named “Test”, the click event on an undo button can call:

Test.ctl.undo();

This is all pretty powerful, elegant stuff, and it’s tempting to adopt this form design pattern. But as I said — the side effects are a problem.

There are two possible ways to revise the form so that it works in strict scoping:

  1. Move the expando properties to script objects
  2. Re-code the logic so that it doesn’t use expando properties

Move the expando properties

The solution in this case consists of moving the logic from the field to a script object. A by-product of the move means we have to have a referencing mechanism from the script object back to the fields. (note that the strategy of moving expandos to script properties will work only in Reader version 8.1 9.0 and later)

The steps to modify this form to allow it to work under strict scoping.

1) Move the logic to a script object

In the original form, we added an instance of the control function to each field.  In our revised design, we’ll move the instances of the control function to a script object and maintain a mapping between each field and its corresponding control function.

I added a script object called “ctl” and moved the logic from the initization event there almost verbatim:

var This;

var control = function() {
    this.history = [This.rawValue],
    this.current = 0,
    this.startValue = null;

    this.undo = function() {
     . . . .
};

The difference is that in the initialization script I created an instance of the function (with the new operator) right away.  Whereas in the script object we simply declare the function and will create instances of it later.

2) Add some bookkeeping

I’ve added some bookkeeping functionality to the script object.  This “glue” code allows us to keep our original logic intact by providing a mapping from fields to the corresponding instance of the control object:

// The fieldList object is a place to store instances
// of the control function for each field
var fieldList = {};

// We need to assign unique id's to each field.
// The unique ids will index into fieldList.

// Use nUID as a global counter to assign id's
var nUID = 0;

// register a field and create an id, and an instance of control
function register(fld) {

    // We'll store the id in the field under "extras"
    var UID = fld.extras.nodes.namedItem("UID");
    if (UID === null) {
        UID = xfa.form.createNode("text", "UID");
        fld.extras.nodes.append(UID);
    }
    UID.value = "ID" + nUID.toString();
    This = fld;

    // create an instance of the control function
    fieldList[UID.value] = new control();
    nUID++;
}

// If a field is part of a subform that gets removed, remove it here also
function unregister(fld) {
	   delete fieldList[fld.extras.UID.value];
}

// Convenience methods for accessing the control functionality
// associated with each field
function getObj(field) {
   return fieldList[field.extras.UID.value];
}
function enter(field) {
    This = field;
    getObj(field).enter();
}
function exit(field) {
    This = field;
    getObj(field).exit();
}
function undo(field) {
    This = field;
    getObj(field).undo();
}
function redo(field) {
    This = field;
    getObj(field).redo();
}

3) Change the code references

The syntax of code that called methods directly on the Test field now needs to change. The initialization script of the Test field now makes a call to:

ctl.register(this);

The enter event changes to:

ctl.enter(this);

 

The click event is now:

ctl.undo(Test);

 

4) Cleanup

When the control function was hosted by the Test field, the instance was conveniently removed when the Test Field was removed.  But now this needs to be done explicitly. The code to remove the subform has a new call:

ctl.unregister(Test);
_item.removeInstance(this.parent.index)

After following the steps above, the revised form works under strict scoping.  Check it out.

Recode the Logic

But now the question needs to be asked: was this the best way to convert the form?  The process I followed was geared to keeping the control logic intact.  My assumption was that this is where the customer has invested the most and they’d prefer to keep that code intact.  But the implementation of the control function could have been implemented differently.  The history is represented as a JavaScript array, but could be re-implemented as an array of elements under <extras>.  The final solution is more elegant — but the effort to get there is riskier.

I’ve attached a 3rd variation of the form that implements undo/redo using extras. I won’t go into details on how it works here.  But one of the interesting side-effects of using extras rather than JavaScript variables is that due to formstate functionality, the undo history is preserved when the form is closed and saved. i.e. you can re-open the form later and the undo history is intact.