Archive for November, 2008

Form Design Code Review

A while ago I was asked to code review a customer form.  I enjoy doing this in order to get an understanding of what customers are trying to do with their forms and where their pain points are.  In this case, I wanted to share some of the code review on my blog so that others can learn from the experience.  This post is a miscellaneous collection of XFA scripting suggestions arising from one customer form.

Repetitive Code

The form had a large block of script that executed before print that looked like:

if (Page1.InvestProdSupp.InvestProdSupp1.MplsInd.rawValue == 0)
   {Page1.InvestProdSupp.presence = "hidden";}
   else
   {Page1.InvestProdSupp.presence = "visible";}

if (Page1.CompMan.CompMan1.MplsInd.rawValue == 0)
   {Page1.CompMan.presence = "hidden";}
   else
   {Page1.CompMan.presence = "visible";}

if (Page1.Oversight.Oversight1.MplsInd.rawValue == 0)
   {Page1.Oversight.presence = "hidden";}
   else
   {Page1.Oversight.presence = "visible";}

This pattern shown 3 times above was repeated 31 times in the method.  A total of around 155 lines of code. Whew.  I enjoy writing code as much as the next person, but this repetitive code is tedious and error prone.  Added or removed a new subform? Don’t forget to update this script. Copy and pasted some script code?  Hope you updated all the relevant pieces correctly.

The general pattern is that there are a bunch of fields called "MpIsInd" under nested subforms.  When the value of this field is zero, hide the parent subform, otherwise make it visible.  Here are two alternative approaches:

Solution 1: Use Recursion

We could define a method to recursively search for fields called "MpIsInd" and apply the logic to each one we find:

function printPrep(vObject)
{
    // Have we found the trigger field?
    if (vObject.name == "MplsInd")
    {
        if (vObject.rawValue == 0)
            vObject.parent.presence = "hidden";
        else
            vObject.parent.presence = "visible";

        // Once we’ve found the field, we can exit early.
        return;
    } else if (vObject.className == "subform")
    {
        // Call this method recursively for each child.
        for (var i=0; i<vObject.nodes.length; i++)
            printPrep(vObject.nodes.item(i));
    }
}
// Set visibility based on all the nested MplsInd descendents
printPrep(Page1);

The end result is 21 lines of script instead of 155 — and a form that is easier to maintain.

Solution2: Use wildcards in SOM

But recursion is not for everyone.  It certainly would not come easily to a novice script writer

The form author could also have used SOM expressions to find all the MplsInd fields.  We are used to using the wildcard character (*) inside braces to indicate "all occurrences". e.g. po.item[*].subtotal.  I suspect it is not widely known that the wildcard character may also appear in place of an element name to indicate "all elements".  Using the wildcard this way, our SOM expression to find all MplsInd grandchildren of Page1 is : Page1.*.MplsInd.

Given that these fields occur as either grandchildren of Page1 or as great-grandchildren, we can use two calls to resolveNodes() to find them:

// Generate lists of the descendant MplsInd fields…
var vList1 = xfa.resolveNodes("Page1.*.MplsInd");
var vList2 = xfa.resolveNodes("Page1.*.*.MplsInd");
for (var i=0; i<vList1.length; i++)
{
    var vChild = vList1.item(i);
    vChild.parent.presence =
                  vChild.rawValue == 0 ? "hidden" : "visible";
}
for (var i=0; i<vList2.length; i++)
{
    var vChild = vList2.item(i);
    vChild.parent.presence = 
                  vChild.rawValue == 0 ? "hidden" : "visible";
}

Relative SOM expressions

There are buttons on the form that insert subforms after the current ancestor subform.  The click event script looks like:

fields.Page1.RevSec.RevSec2.Table1.Row1.Table2.Row1.Cell1::click – (JavaScript, client)fields.Page1.RevSec.RevSec2.Table1.Row1.instanceManager.insertInstance(this.parent.parent.parent.index+1);

That is pretty verbose.  A more readable form of this would use relative references to the instance manager and to the ancestor subform:

fields.Page1.RevSec.RevSec2.Table1.Row1.Table2.Row1.Cell1::click – (JavaScript, client)
Table1._Row1.insertInstance(Table1.Row1.index+1);

Resolving SOM expressions involves what we call "scoped matching".  In this case, the starting context is the Cell1 button (the field hosting the script).  When asked to find Table1, we look for a match by looking at the children of Cell1.  If not found, we expand the scope of the search by moving up the hierarchy and checking the parent element: Row1 and the children of Row1.  This continues until we encounter Table1.  Note that because there were two Row1 elements in the hierarchy, we differentiated them by including Table1 in the SOM expression.  Note also that we did not worry about which instance of Row1 was returned.  The scoped match will find the direct ancestor before any of its siblings.

The replacement code found the instance manager using its name: _Row1 whereas the original used Row1.instanceManager. Using _Row1 is a better practise, because when there are zero instances of a
subform, the search for Row1 will fail, whereas _Row1 is always available.  The naming convention for instance managers is the subform name prefixed with an underscore.

Locking a form

The form had a script to lock all the fields in the form:

function lockForm(sState){
    try{   
        // Get the field containers from each page.
        for (var i = 0; i < xfa.layout.absPageCount(); i++) {
            var oFields = xfa.layout.pageContent(i, "field");
            var nodesLength = oFields.length;
            // Set the access type.
            for (var j = 0; j < nodesLength; j++) {
                var oItem = oFields.item(j);
                if (oItem != this) {
                    oItem.access = sState;
                }
            }
        }
    }catch(e){
        app.alert("Error in function lockForm() – " + e);
    }
}

Notice that this script is as efficient as possible when it loops through the children in a list. It declares a variable to hold the size of the list, and uses that variable in the "for loop". The advantage of doing this is that in a "for loop" the condition gets evaluated for each iteration of the loop.  It is more efficient to reference a JavaScript variable than it is to evaluate a list.length property.  If you have a "for loop" in an area of performance critical code, this is good practise.

Notice another clever part of the script — it makes sure not to lock the current field.

Unfortunately, it was hard to tell how the script was actually used, because it was not referenced anywhere on the form.  But a couple of observations:

As of Reader 9.0, the access property is now available on subforms.  If your target version is Reader 9, you can code: subform.access = "readOnly" and the result will lock all the descendant fields of the subform.  Setting this property on the root subform would lock the entire form — in one JavaScript statement.

Removing a Subform

The form has buttons to remove subform instances with code that looks like this:

fields.Page1.RevSec.RevSec2.Table1.Row1.Table2.Row1.Cell2::click – (JavaScript, client)
fields.Page1.RevSec.RevSec2.Table1.Row1.instanceManager.removeInstance(this.parent.parent.parent.index);

For starters, we can simplify the SOM expressions as described above. But the other problem is that this particular Row1 subform is defined to have a minimum of one instance.  This means that if the user clicks on the button to remove the last instance of Row1, Reader will generate an error.  The way Reader handles JavaScript errors is to write them to the JavaScript console.  The end user does not get an indication that there was an error.  But in the console you will see:

GeneralError: Operation failed.
XFAObject.removeInstance:1:XFA:fields[0]:Page1[0]:RevSec[0]:RevSec2[0]:Table1[0]:Row1[0]:Table2[0]:Row1[0]:Cell2[0]:click
The element [min] has violated its allowable number of occurrences.

This version of the script protects against attempting to remove beyond the minimum number of instances:

fields.Page1.RevSec.RevSec2.Table1.Row1.Table2.Row1.Cell2::click – (JavaScript, client)
if (Table1._Row1.count > Table1._Row1.min)
    Table1._Row1.removeInstance(Table1.Row1.index);

If the number of subforms has an upper limit, you should similarly guard code that adds instances.

When testing a form it is good practise to leave the JavaScript console open and make sure that it is clear when testing is complete. 

Validation Patterns: Part 3

Here is the last (for now) in the series on how to validate templates in a user-friendly manner.  I have attached a new and improved sample (with data).

What is new in this version:

  • An email submit button that becomes active only when there are no errors on the form
  • A listbox field that shows all the errors on the field.  When you enter into the list field, the field corresponding to the error gets highlighted.  Try selecting different elements in the list.

But more importantly, this sample form now holds a toolkit of functions that allow you to take control of the way Reader validates form fields.

Behind the scenes on the script side, there is a fair bit more in the toolbox.  My goal is that you can re-used these script objects in your forms and keep your form designs as clean and simple as possible.  If you look at the form you will see that the vast majority of script is inside the script objects.  The fields and subforms on the form itself have minimal amounts of script.

An added benefit of following this particular form development methodology is that is should be very consistent with future enhancements we add to XFA and Reader.

Here is a summary of the script functions that you can use in your form development:

/**
* setStatus(vContainer, vStatus)
* Call this method as the last line of your validation script.
* This function does two things:
* 1) highlights or resets the field according to whether it is valid or not
* 2) If invalid, logs the error
*
* @param vContainer — the container we are validating. 
*                      If a subform, process recursively.
* @param validStatus — true | false
* @return the status for the validation script to use 
*         (for now hardcoded to "true")
*/

/**
* formHasErrors()
*   Useful for changing the state of form objects depending on if there are
*   validation errors or not.
*   Place this call inside a validation or calculation script and make sure
*   you have called registerListener(this) from your initialization event.
* @return true if there are errors.  False otherwise.
*/

/**
* registerListener(vListener)
*   Call this from you initialization event if you want to be able to
*   call formHasErrors() in your calculation or validation events.
* @param vListener — The object that cares about whether there are errors. 
*   We’ll keep track of all the listeners. Every time a field changes valid
*   state (becomes valid or invalid), we will loop through all listeners and
*   force their calculate and validate scripts to run.
*   If a listener is a choiceList object, we’ll synchronize the choicelist
*   entries with field errors.
*   When a choicelist has zero entries, we hide it.
* @return void
*/

/*
* setFieldMandatory(vObject, vMandatoryState, vForce)
*   Mark a field or exclusion group as being mandatory.
*   When calling this from a validation script,
*   be sure to also call setStatus()
*
* @param vObject — the field or exclusion group that we’re marking
* @param vMandatoryState — true or false 
* @return boolean — true if the state changed
*/

/**
* clearErrList()
* Call this method before removing a subform or re-ordering subforms.
* Our tracking is based on storing SOM expressions.
*
The SOM expression for a field can change if the order of its parent
* subform changes. 
* After clearing the list and removing/moving subforms,
* call xfa.form.execValidate() to rebuild the list.
* Note that it is not necessary to call this method when appending new subforms.
*/

Then there are a couple of methods you might choose to modify for your own use — in case you do not like the way the sample highlights invalid fields:

/** 
* highlight(vObject)
*   Highlight a field (or exclusion group or subform) to indicate that it
*   has a validation error
* @param vObject — the subform/field/exclusion group to highlight
*/

/**
* unhighlight(object)
*   reset the form field/subform/exclusion group to the state it was in
*   the template.
* @param object — the subform/field/exclusion group to highlight
*/

 

The one thing that does not work back to Reader 7 is the button script that sets focus on an error field.  xfa.host.setFocus() came later.

But the rest of script functionality should work fine in Reader 7. The hardest part about making the scripts work in Reader 7 was that the convenience methods for manipulating choice lists are not available there.  The script has to manipulate the XML structures directly. 

Validation Patterns: Part 2

Continuing from the previous post, we are looking at the problem of validating fields without inundating the user with message boxes during their form session in Adobe Reader — and without centralizing all the validation logic.

In the previous post, we established a design pattern for highlighting fields where a script validation fails. In this post we will deal with mandatory (required) fields.  Today, when you mark fields as mandatory, then any time your form is validated you will get an error message for each missing field.  We can do better.

The updated sample (with data) builds on the previous sample. The strategy is to

  • author the form fields using the standard XFA “required field” settings
  • At runtime disable the “required field” setting and store our own mandatory flag elsewhere in the field
  • Use our setStatus() function during validation to monitor whether fields are populated or not.

Changes from the previous sample:

  • Marked various fields as being mandatory
  • Additional logic in scValidate.setStatus() to cause empty mandatory fields to show up as invalid
  • A new script function: scValidate.setFieldMandatory()

Storing the mandatory state in a field involves a technique described in a previous post where we store a variable under a field.desc element.

Once we have logic in the form that stores our mandatory state, we let the XFA processor believe that no fields on the form are required. This way we don’t get any warnings from Acrobat/Reader, but we graphically modify the fields to highlight them for the user.

The only deviation from standard form design is that for mandatory fields you need to add a call to utility.setStatus() in the validation script – even if there is no script validation required.  i.e. if you mark a field as mandatory, you need to add this validation script:

expenseReport.details.empID::validate – (JavaScript, client)
scValidate.setStatus(this, true);

Conditionally Mandatory

On the sample form, when the payment type field choice is "Direct Deposit", there are three more fields that need to be filled in. If the payment type is any other choice, these fields do not need to be filled in.

Normally we toggle a field’s mandatory status by setting the property: field.mandatory. But now that our design pattern for mandatory fields has co-opted the XFA mandatory mechanism, we will use one of our new global methods: scValidate.setFieldMandatory(vField, vMandatoryState).

For this to work, the form defines validation scripts on the financialInstitution, branchNumber and accountNumber fields that look like this:

expenseReport.payment.deposit.financialInstitution::validate

var vMandatory = directDeposit.rawValue == 1;
scValidate.setFieldMandatory(this, vMandatory);
scValidate.setStatus(this, true);

Updated Exclusion Groups

If you are looking at the script code you will notice that I have included the exclusion group script objects that were defined in a previous post.  The form has placed the direct deposit subform in an exclusion group with the other payment types. I have modified those scripts so that they work with the validation framework. (You will notice I have also broken them out into separate script objects with a new naming scheme).

The exclusion group sample has been updated so that:

  • When an exclusion subform has not yet reached its minimum number of entries, the subform gets highlighted instead of the individual fields in the subform.
  • The call to scGroup.setMinAndMax() now includes an optional validation message parameter.  This is needed because designer does not yet expose a UI for assigning a validation message to a subform.

Note that this sample now introduces the concept of an invalid subform.  If the user has not filled in all the credit card fields, then we mark the subform as invalid — similar to the case where the subform exclusion group did not reach its minimum number of entries.  The script to do this looks like:

form1.#subform[0].Payment.CC.CardType::validate – (JavaScript, client)
// Valid state is where either all fields are null or all fields are populated.
var vValid = (CardType.isNull  && CardNo.isNull  && Expiry.isNull) ||
             (!CardType.isNull && !CardNo.isNull && !Expiry.isNull);

// populate the subform validation message so that users get a meaningful message
if (this.parent.validationMessage.length == 0)
    this.parent.validationMessage = "All credit card fields must be filled in";

scValidate.setStatus(this.parent, vValid );

The validation button

The samples have also updated the logic for the button to validate the form.  Now when there are no errors, the button caption will be updated to say: "no errors" (and will be made read-only).  This works as long as you do not change the name of the button from : "checkValid".  You might prefer a variation on this where the button is hidden when there are no errors.

Backward compatibility

We would like to make it easier you to control validation message handling in a future version of Reader, but for the here and now, you are likely targeting Reader 7/8/9 and need an immediate solution.  You can take these script objects, copy them into new forms and use them in forms compatible as far back as Reader 7.

Next up: Another error reporting option

Validation Patterns: Part 1

Time to tackle my pet peeve. The aspect of customer written script that concerns me most is how customers validate field data. The number one reason for script bloat is that our users bypass the standard XFA/Reader validation framework.

Of course, there is a reason why script writers bypass validation. The problem is that each validation message appears in an individual dialog box. In some cases when a form is validated, the user may have to dismiss dozens of dialogs. Most users would prefer no dialog boxes at all – just show the invalid fields graphically. This is a problem we would like to solve in a product release, but in the mean time, you need forms that work in previous versions of Reader. Starting today I will tackle this topic in a series of blog entries.

Current Practise

Form authors currently work around the “validation message” issue by moving their validation logic outside the prescribed validation mechanisms. They typically place all the validation logic for their form under the click event of a “validate” button. They end up with a very large chunk of script that look like:

if (purchaseOrder.amount.rawValue >= 1000) {
     errorMessage += "Amount must be less than 1000\n";
     errorFlag = true;
}
if (purchaseOrder.quantity.rawValue == null || quantity.rawValue == 0) {
     errorMessage += "Quantity must be specified\n";
     errorFlag = true;
}
... repeat for each business rule ...
if (errorFlag) {
    xfa.host.messageBox(errorMessage);
}

The result is that field definitions are no longer encapsulated (self-contained). The problems introduced:

  • Fields cannot be copied/pasted/moved/deleted/renamed without breaking script
  • Adding new a field requires updating a global script – hard to correlate which fields have validations script and which do not.
  • Validation messages are stored and handled in script. Any attempt to isolate strings from the template for translation or spell checking is very difficult.
  • Validations happen only when the user explicitly asks for a validation – e.g. by clicking a button. They do not get incremental notifications.
  • Total amount of script is greater, and the script is more complex

Best Practise

Here is a sample form where:

  • Validation logic is placed in field validation scripts
  • Invalid fields are highlighted with a red border
  • No message boxes appear during data entry
  • There is a button that checks overall form validation status and if there are errors, places the user in the first invalid field

The solution has 3 parts:

  1. Global Script Objects
  2. Field validation logic
  3. Validation button

Global Scripts

There are six global scripts under the utilities script object. The ones that the form author should be aware of:

setStatus(vField, vStatus)

Called by field validation logic to handle the validation logic for a field.

highlightField(vObject)

Changes the appearance of a field (or exclusion group) to indicate that it is invalid. In this case we change the border color. Anyone who wants to re-use this mechanism can modify this script to get the visual effect they prefer.  Note that for this sample, the highlighting works best if fields do not have a raised border.  It also works best for dynamic forms.  It should be re-specified if used for a static form.

unhighlightField(vObject)

Reverts the field to the appearance defined in the template. If you modified code in highlightField(), you need to make the corresponding change in this function.

There are a three more scripts that are used internally:

registerFieldStatus(vField, vStatus)

Saves a list of invalid fields in a form variable.

getMessage(vObject)

Retrieves the validation message from a field object.

findTemplateField(vField)

Finds the template field that we use to reset visual properties.

Field Definition

Several fields on the sample form have validation logic. The general form of the validation script looks like this:

expenseReport.#subform[0].expenses.expense.amount::validate – (JavaScript, client)

// place field validation logic in the getStatus() method.

// Make sure it returns true (valid) or false (invalid).

function getStatus()
{     // place field validation logic here.

}
utilities.setStatus(this, getStatus()); // register valid/invalid state

Note the properties of this script:

  • The validation will fire every time the field (or any of its dependencies) change
  • Since the setStatus() function always returns true, the field is always valid according to the XFA processor.
  • The setStatus() function does the work to change appearances etc.

Other than the field validation script, the important aspect of this design is that the validation message is specified at the field level.

Validation Button

The validation button will:

  • Check the form variable holding the list of invalid fields
  • If there are invalid fields, extract the validation message from the first invalid field and issue a message box
  • Set focus to the first invalid field

Topic for next post: Handling mandatory fields.

Validating a Postal Code

There are several interesting scripting validation techniques that can be demonstrated in the context of a Canadian postal code field.  While the subject domain is fairly specific, the techniques used are applicable to other field validations.

The challenge is to get an accurate and user-friendly data capture experience for a postal code. Some of the considerations:

  • Make sure the field value conforms to the rules for Canadian postal codes (use regular expressions for validating)
  • Make sure that error messages are as specific as possible to assist better user input (customize validation error messages)
  • Make sure the user can optionally key in a space inside their postal code  (use an edit picture clause)
  • Make sure the postal code is entered in upper case (modify keystrokes on the change event)
  • Make sure the postal code value is consistent with other address fields: province and city (inter-field validation)

The sample form can be found here.

Canadian Postal Code Rules

The default way to validate a postal code is to use a validation picture clause: text{A9A 9A9}. However, this picture allows many illegal postal codes. For example, the meta character “A” in a picture clause allows any Unicode letter value – whereas a postal code letter is far more restricted:

  • The first character of a Canadian postal code corresponds to a geographic region and is limited to the set of characters: ABCEGHJKLMNPRSTVXY
  • The third and fifth characters may not include the letters D, F, I, O, Q or U (because they are hard for OCR software to deal with).

In order to implement these restrictions, we can use regular expressions. The JavaScript string class has a match() method that searches for a pattern.  For example, the code to validate the first character is:

if (vTestValue.charAt(0).match(/[ABCEGHJKLMNPRSTVXY]/) == null)
{
… error condition …
}

Specific Error Messages

When the user enters an invalid postal code, be as specific as possible in telling them what to fix. There is a world of difference between:

“Invalid postal code” and “Second, fourth and sixth postal characters must be numeric”

For the person who typed in a letter “O” instead of a “0” (zero), this will be the clue they need to correct their error.

To make this work, validate the portions of the postal code individually and then set the field error message accordingly:

var vNums = vTestValue.charAt(1) +
vTestValue.charAt(3) +
vTestValue.charAt(5);
if (vNums.match(/[0-9][0-9][0-9]/) == null)
{
vTestField.validationMessage =
“Second, fourth and sixth postal characters must be numeric”;
return false;
}

Note that you might choose to store error messages as form variables.  That way they will be examined by Designer’s spell checker.

The Optional Space

The user ought to be able to enter their data with or without a space – but we want to store it consistently – without the space. To accomplish this, use an edit picture clause: text{OOO OOO}

Edit picture clauses are used by Reader to:

  1. format the raw value for editing when the user enters the field
  2. parse the edited value to set the raw value when the user exits the field

For example, a raw value: A2A2A2 will be displayed as A2A 2A2 when the user enters the field. When they exit the field, both A2A2A2 and A2A 2A2 will parse into A2A2A2 successfully. Note that I used the meta character “O” (letter or digit). This is so that if the user enters invalid characters, we still parse/format the space correctly.

Naturally, we also use a display picture clause to render with the space: text{A9A 9A9}

Upper Case Only

The easiest way to force the value to upper case is to trap the characters as the user enters them and convert them as they type. We do this with a very simple change event:

form1.address.PostalCode::change – (JavaScript, client)
// Change all user entered characters to upper case.
xfa.event.change = xfa.event.change.toUpperCase();

Because we know that the input data is in upper case, it makes our validation logic cleaner, as it needs only be concerned with the upper case variations.

Consistency with City and Province

The first character of the postal code determines the geographic region.  We can assign the province based on the postal code. In a couple of cases (M and H) we can also assign the value for the city (Toronto and Montréal).

More Considerations

While we now have a better-than-average postal code validation, it does not ensure100% correctness. Since only about 12% of the possible 7.2 million postal code variations are currently allocated, it is still pretty easy to enter a non-existent postal code.

Because this validation script is as specific as possible, it does mean that the script may need to be updated as the postal code rules evolve (The Canadian postal code rules were modified after Nunavut was added in 1999).

My sources for postal code specifics were:
http://en.wikipedia.org/wiki/Canadian_postal_code and http://www.columbia.edu/kermit/postal-ca.html.

Loop Through Subform Instances

A note today on a specific coding practice I have seen in a number of customer forms. The task is to write a loop that examines each subform instance in a repeating subform definition. The pattern I have seen is where users construct SOM expressions inside a loop and call resolveNode() to find the result. I will show some alternate coding patterns that are easier to write and more efficient to execute. I have included a sample that has five buttons – one illustrating each variation.  The sample looks at each expense row in an expense report and ensure that the description field is populated.

Variation 1: The Customer Solution

Here is the JavaScript as the customer wrote it:

expenseReport.#subform[0].validate1::click – (JavaScript, client)
var nItems = expenseReport.expenses.expense.instanceManager.count;
for(x=0; x<nItems; x++)
{
    var vName = "expenseReport.expenses.expense["+ x +"].description";
    var vDesc = xfa.resolveNode(vName).rawValue;
    if(vDesc == "" || vDesc == null)
    {
        xfa.host.messageBox("Missing description field");
        break;
    }
}

Variation 2: Use the "all" property

In SOM we have the "[*]" notation. "expense[*]" means "all the instances of the expense subform". Unfortunately, we cannot expose the "[*]" notation in JavaScript. Instead, we introduced the "all" property. Using "expense.all", the script can be written without constructing SOM expressions:

expenseReport.#subform[0].validate2::click – (JavaScript, client)
var vItems = expenses.expense.all;
for(i=0; i<vItems.length; i++)
{
    if (vItems.item(i).description.rawValue == null)
    {
        xfa.host.messageBox("Missing description field");
        break;
    }
}

Variation 3: Use resolveNodes()

Both previous solutions assume you have at least one instance of the expense subform to start with. In the case where we might not have any occurrences, we can use resolveNodes().

resolveNode() returns a node object . We use it when we expect our SOM expression to return a single node. Use resolveNodes() (plural) to return a list object when your SOM expression may return multiple objects. In this example, use the expression: "expenses.expense[*].description" to return all the description fields:

expenseReport.#subform[0].validate3::click – (JavaScript, client)
var vDescItems = this.resolveNodes("expenses.expense[*].description");
for(i=0; i<vDescItems.length; i++)
{
    if (vDescItems.item(i).rawValue == null)
    {
        xfa.host.messageBox("Missing description field");
        break;
    }
}

Variation 4: The FormCalc version

If you prefer using FormCalc, it looks like:

expenseReport.#subform[0].validate4::click – (FormCalc, client)
foreach vDesc in (expenses.expense[*].description) do
    if (not hasValue(vDesc)) then
        xfa.host.messageBox("Missing description field");
        break; 
    endif
endfor

Note the use of the hasValue() function. It has the added benefit that it will reject description fields that consist of only whitespace.

Variation 5: The Deep End

The most terse (and most efficient) variation would use a predicate expression:

expenseReport.#subform[0].validate5::click – (JavaScript, client)
if (this.resolveNodes(
          "expenses.expense.[not hasValue(description)]").length > 0)
    xfa.host.messageBox("Missing description field");

(more on predicates in previous blog entries: here and here.)

Other Issues

Validating outside the field

I should have mentioned this disclaimer earlier: I do not like form design patterns where validation logic lives outside the individual fields. I understand the motivation – the lack of control over validation messaging. But there are better design patterns. However, that is a topic for a whole series of blog entries.

Anchor resolveNode

Note the difference between "this.resolveNode()" and "xfa.resolveNode()". The SOM expression passed to resolveNode() is evaluated relative to the object hosting the call. The original customer version used:

xfa.resolveNode("expenseReport.expenses.expense[0].description");

Whereas if the call is anchored under "this" (the validation button context) the SOM expression can be shortened to:

this.resolveNode("expenses.expense[0].description");

Checking empty

The customer version of the script checks:

if (vDesc == "" || vDesc == null)

Unless you have customized your null handling, this check is sufficient:

if (vDesc == null)

By default, all empty text and numeric fields return null.