Search This Blog

Wednesday 15 May 2013

JavaScript Objects in XFA Forms

Problem

JavaScript objects allow you to reuse functionality across your forms but there are a some tricks to getting them to work in the Adobe Acrobat / Reader environment.

Solution

There a a couple of ways to create objects in JavaScript. Using the new operator and a constructor function, or using the object literal syntax. The new operator has some advantages as it allows the developer to enforce an initialisation script be executed and is easily understood by programmers coming from languages that support classes. Using the literal syntax might require a static method to create the object or an 'initialise' method ( or 'start' method in the sample below ). But using the new operator requires a work around that is well described in John Brinkman's blog, http://blogs.adobe.com/formfeed/2009/09/script_objects_deep_dive.html. This is an alternative that might fit some situations and uses the literal syntax to create JavaScript objects defined within an XFA script object.

Detailed explanation

This sample defines an object that can be used for measuring elapsed time, a StopWatch, and can be used like;

var sw1 = StopWatch.stopWatch("StopWatch1");
sw1.start();
for (var i=0; i<1000; i++) {} // or something we want to measure
sw1.stop();
sw1.println();

This will output a message on the console that looks like;

StopWatch1:Time elapsed 127ms

Or using a static method to construct the object and achieve the same result;

var sw1 = StopWatch.startNew("StopWatch1");
for (var i=0; i<1000; i++) {} // or something we want to measure

sw1.stop();
sw1.println();


The script object is called StopWatch and the function defined within it is called stopWatch, but note that we do not use the new operator to create it. The stopWatch function returns a inner object that retains access to the variables and functions of stopWatch without exposing them to the calling code. This is called a closure and more information about closures and nested functions is available at https://developer.mozilla.org/en/JavaScript/Reference/Functions_and_function_scope/#Nested_functions_and_closures.

This means there is no way outside code can access the variables startTime, elapsed, isRunning or the function getElapsedTime.


form1.#variables[0].StopWatch - (JavaScript, client)
function stopWatch(label){
    /**
     *  Private variables used internally by the StopWatch object.
     */
    var startTime = 0;
    var elapsed = 0;
    var isRunning = false;
 

    /**
     *  A private helper function to return the number of milliseconds since the
     *  StopWatch was started.
     */
    function getElapsedTime()
    {
        return Date.now() - startTime;
    }
  

 
    /**
     *  The StopWatch object as visible to other form code.
     */
    return {
        /**
         *  Determines if a StopWatch is running
         *  @return {boolean} true is the StopWatch is running otherwise false.
         */
        getIsRunning: function IsRunning()
        {
            return isRunning;
        },
        /*
         * Returns the total time since the StopWatch was first started.
         * @return {int} The total number of elapsed milliseconds.
         */
        getElapsed: function getElapsed()
        {
            var result = elapsed;
            if (isRunning)
            {
                result += getElapsedTime();
            }
            return result;
        },
        /*
         * Starts (or continues), measuring elapsed time.
         */
        start: function start()
        {
            if (!isRunning)
            {
                isRunning = true;
                startTime = Date.now();
            }
        },
        /*
         * Stops (or pauses) measuring elapsed time.
         */
        stop: function stop()
        {
            if (isRunning)
            {
                elapsed += getElapsedTime();
                isRunning = false;
            }
        },
        /*
         * Stops the StopWatch and resets the elapsed time to zero.
         */
        reset:function reset()
        {
            elapsed = 0;
            isRunning = false;
            startTime = 0;
        },
        /*
         * Outputs the elapsed time to the console.
         */
        println: function println()
        {
            console.println(util.printf("%sTime elapsed %,0dms",
                                        (label === undefined) ? "" : label + ":",
                                        this.getElapsed()));
        }
    }
}


This approach would allow me to pass in an argument to stopWatch and have different objects returned, perhaps one measuring in decimal time, or more usefully in a real example having different objects returned depending on a country id passed in that provided appropriate calculations, validations and initialisations. Creating objects this way is known as the factory pattern.

I have often used the StopWatch code when experimenting with individual parts of my form that are not performing fast enough. But I have also modified John Brinkman's trace macro http://blogs.adobe.com/formfeed/2010/06/add_trace_to_form_script.html to add code to all script events in a form. More details of macros and how to install them can be found in one of his earlier blogs, http://blogs.adobe.com/formfeed/2010/01/designer_es2_macros.html, but it should be as simple as creating a directory C:\Program Files (x86)\Adobe\Adobe LiveCycle Designer ES2\Scripts\AddExecutionTimes, copy the AddExecutionTimes.js file to the folder and restart Designer. Once restarted there will be an option under Tools ... Scripts called AddExecutionTimes.js. If there is any problem running the macro then the messages should be displayed on the Log tab of the Report window.

Make a copy of the form before running this macro as I don't have a macro to remove the StopWatch code that gets inserted. Now when you preview your form messages will appear with the form object somExpression, the event name and the elapsed time, something like;

xfa[0].form[0].form1[0].#subform[0].TextField1[0].#calculate:Time elapsed 92ms
xfa[0].form[0].form1[0].#subform[0].Table1[0].Row2[0].DecimalField1[0].#calculate:Time elapsed 58ms
xfa[0].form[0].form1[0].#subform[0].Table1[0].Row2[1].DecimalField1[0].#calculate:Time elapsed 60ms
xfa[0].form[0].form1[0].#subform[0].Table1[0].Row2[2].DecimalField1[0].#calculate:Time elapsed 60ms
xfa[0].form[0].form1[0].#subform[0].Table1[0].Row2[3].DecimalField1[0].#calculate:Time elapsed 92ms
xfa[0].form[0].form1[0].#subform[0].Table1[0].Row2[4].DecimalField1[0].#calculate:Time elapsed 86ms
xfa[0].form[0].form1[0].#subform[0].TextField1[0].#validate:Time elapsed 57ms
xfa[0].form[0].form1[0].#subform[0].Button1[0].event__click:Time elapsed 2,565ms


The StopWatch object has been modelled after the .net system.diagnostics.stopwatch object that I have found useful, but that one uses performance counters, this one uses the JavaScript date object so all timings are just indicative, you may have to run your tests a number of times to get a more accurate measure and minimise the number of other processes running on your machine. But hopefully this will point to parts of your form that are chewing up the most time and help identify candidates for optimisation.

StopWatch.pdf is a sample with the macro already run, when opened the console will be displayed with initialisation messages displayed, as you interact with the form more messages will be displayed.

StopWatch.xdp a script fragment containing the StopWatch object.

AddExecutionTimes.js the macro for adding StopWatch code to all of your events.

No comments:

Post a Comment