Script Dependencies and "recalculate()"

For older versions of Acrobat, there were a lot of forms which would call xfa.form.recalculate() in various places.  This was to ensure that fields will get updated which depend on other fields that may have changed.  Newer versions of Acrobat (8.1 and higher) are designed to not require so many calls to recalculate(), because the system is smart enough to evaluate your calculate script and figure out what fields it depends on.  The system puts "listeners" on those other fields so that whenever they change, my calculate script gets called automatically.  I don’t have to declare the dependency.

For example, I have a Total field that calculates its value from several other fields (Field1, Field2, Field3).   Any time Field2 changes (due to either a calculation or to the user typing something in Field2), the Total calculate script will be called automatically.

But now make this dynamic:  add a repeating subform with Add and Remove buttons so the user can create new instances of the subform.  For example, I have a repeating subform containing a Weight field.  Outside the repeating subform, a Total field has a calculate script which sums up all the instances of that Weight field.  Pretty simple.  

clip_image001

But the listeners aren’t correctly added when a new subform instance is created.  My Totals field is listening to the first instance of RepeatingSubform[0].Weight, but when RepeatingSubform [1] is created, Totals isn’t listening to changes to RepeatingSubform [1].Weight.  Stefan Cameron talks about this in an older blog post:  http://forms.stefcameron.com/2006/05/20/add-recalculate/

Interestingly, look at the calculate script on Total:

var sum = 0;

var columnArray = xfa.resolveNodes( "RepeatingSubform1[*].Weight1");

for( var i = 0; i<columnArray.length; i++)

{

    sum += columnArray.item(i).rawValue;

}

this.rawValue = sum;

The call to xfa.resolveNodes() happens to cause the listeners to be updated!  This leads to very odd behavior:  if I change RepeatingSubform [1].Weight, it won’t update Totals, until I change RepeatingSubform[0].Weight.  Then any additional Weight field changes will cause Total to update correctly!

So if I start with one weight field (W0) and add 3 more, I end up with weight fields like this:

                W0 (RepeatingSubform[0].Weight)

                W1

                W2

                W3

                Total

Then changes to the added ones (W1, W2, W3) won’t update the Total – yet.  Changing W0 will update the total, and once you do that, now changes to W1, W2,W3 will work!

Then if I add a W4:

                W0

                W1

                W2

                W3

                W4

                Total

Now changes to W0, W1, W2, W3 will all continue to work but my newly-added W4 won’t work (until I change one of W0-W3).

Workaround:

Add script to the indexChange event of RepeatingSubform, which will take care of all newly-added instances; Totals will be recalculated and will now be "listening to" the new instances.  This is more efficient than calling recalculate() on the entire form – it just recalculates that particular field or subform.

Total.execCalculate();   // Note: you can call this on a subform too:  TotalsSubform.execCalculate();

Add the same script to the delete button click() event so that the Total is recalculated when an instance is deleted.  The important part of that code is:

TotalsSubform.execCalculate();  // this avoids having to call the entire form’s recalculate() method.

oTargetSubform.instanceManager.removeInstance(oTargetSubform.index);

Delete button order of operations

Another interesting note:  in the delete button click() event, the call to recalculate() must be placed before the instance manager call to removeInstance().  This seems wrong: shouldn’t that cause the total to include the about-to-be-deleted weight?  It turns out that it doesn’t, because recalculate is deferred until after the current script finishes.  (That’s intentional – so that multiple operations that cause a field to be recalculated don’t cause the calculation script to run several times in succession.  And, it helps avoid infinite loops in script).

Why can’t I just put the call to execCalculate() after I remove the instance?  It has to do with scope; the remove button (‘this’ in the button’s click event script) is removing its own parent subform.  If I try to reference "TotalsSubform" after the removeInstance(), the system starts from the current location (this delete button), searching for TotalsSubform by walking up the tree of objects in the object model.  But it can’t "walk the tree" because the current subform is now an orphan – it’s no longer in the tree!  It was removed.    The search fails, and the calculation doesn’t take place.   A workaround for that problem is to put the call to execCalculate() after the removeInstance(), but use an explicit SOM expression from the root of the form.

 

The sample file is here:

Download file