Working with multiple datasets

| No Comments

One of the formfeed blog commenters ("mo") asked about preserving data during an import operation.   I gave her (him?) a flippant reply with some hand-waving about saving/restoring data before/after import. Then I tried it myself and discovered it was not nearly as easy as I thought.

Here's the problem description:  Your data arrives in two separate data files.  You need to import them both into your form.  Problem is that importing new data replaces existing data.  Loading the second data file will discard the data from your first import. 

Let's set up a specific example -- Suppose my data looks like:

<multidata>
  <set1> ... </set1>
  <set2> ... </set2>
</multidata>

We want to be able to load set1 and set2 from different data files.

There are a couple of solutions to this problem.   But first some review on dataset handling within XFA/PDF forms.  Normally form data gets stored under $data -- which is a shortcut to: xfa.datasets.data.  If the root node of your form is "multidata", then your data appears under xfa.datasets.data.multidata.

The XML hierarchy looks like this:

<xfa:datasets xmlns:xfa="http://www.xfa.org/schema/xfa-data/1.0/">
  <xfa:data>
    <multidata>
      <set1> ... </set1>
      <set2> ... </set2>
    </multidata>
  </xfa:data>
</xfa:datasets>

When Acrobat performs a data import, it replaces the <xfa:data> element. But it *appends to* any other datasets. 

Solution 1: Preserve/Restore data before/after import

If during import you could arrange your data to look like:

<xfa:datasets xmlns:xfa="http://www.xfa.org/schema/xfa-data/1.0/">
  <xfa:data>
    <multidata>
      <set2> ... </set2>
    </multidata>
  </xfa:data>
  <set1> ... </set1>
</xfa:datasets>

In this case, <set1> would be preserved and only <set2> would be replaced.  Then after the import is complete,  you'd move <set1> back where it belonged.  The way to control the import is to use the Acrobat script function: importXFAData();  Here's the outline of the script to import set2:

  1. Copy set1 data to be a child of xfa:datasets
  2. Remove the set1 subform
  3. Call importXFAData() to load set2
  4. Move the set1 data back under <multidata>
  5. Re-add the set1 subform

There are a number of tricky parts:

  • When importXFAData() is successful, it causes a remerge.  When remerge happens, any script commands after the call to importXFAData() will not execute.  The workaround is to perform steps 4 and 5 in a separate form:ready script.
  • importXFAData() does not return a status.  You have no way of knowing if the user cancelled.  If they did cancel, you need to restore set1 without depending on the form:ready script.
  • If the user is running in Reader, then importXFAData() will throw an error.  We need to catch this error and restore set1 data.
  • If the user imports data from the Acrobat menu (Forms/Manage Form Data/Import Data) then all your clever script won't run and set1 data will get cleared.  You need to figure out how to remove this option from the Acrobat menu.

Note that this specific example assumes you are loading data in the order set1 then set2.  The form could be coded more generally to load the data in any order.  It would just be a bit more complicated.  You'd need to move both set1 and set2 and then after the load you'd figure out which one(s) need to be moved back.

Here is a sample form.  Here is sample set2.xml data you can load.  Have a look at the button click and form:ready events for all the gory details.

Solution 2: Bind set1 outside of xfa:data

Instead of temporarily arranging your data so that set1 is under <xfa:datasets>, you could permanently arrange your data this way.  In the binding expression for the set1 subform, specify "!set1" -- which is a shortcut for xfa.datasets.set1.  Now whenever you import data for set2, it will leave set1 untouched.  However, this introduces a new problem.  Whenever you import new set1 data you will end up with multiple copies of set1.  You need a form:ready script that will delete all but the last copy.  This also means that the data file holding set1 needs to include the <xfa:datasets> element so that it can correctly specify the location for set1

Here is a sample file with set1 data and set2 data. The script to trim back the extra copies of set1 data is found in the multidata form:ready event.

My personal preference would be to use Solution 2.  The script is simpler.  The user can use the menu commands for loading the data.  But this approach might not be possible if your data is bound to a schema.

Editable Floating Fields V2

| No Comments

This is a follow-up to a previous blog entry that you probably should read first.

After doing the first version of the floating field editor, I tackled some issues/enhancements:

  1. A bug in the script where if you tabbed out and tabbed back in, the editor stopped working.
  2. Enforce constraints associated with the referenced fields
  3. When the editor does not have focus, display the field values using the formatted values of the referenced fields
  4. When the editor has focus, display the field values using the edit values of the referenced fields

I have updated the previous sample form -- as well as the Editor fragment.

Enforcing Field Constraints

Since the floating fields are all presented inside a single text field, there was originally no constraints on any of the user input.  Now the form will look at the referenced fields and will restrict user input:

  • Respect the max chars constraint of text fields (in the sample, they're all limited to 10 characters)
  • For numeric fields, limit input to valid numeric characters
  • For choice list fields, limit input to the set of valid choices

Locale-sensitive Numeric Fields

When restricting the set of valid characters for numeric input, it is tempting to just go with the obvious set:
[0-9\-\.]  However for many locales, the radix (decimal) and minus symbols will be different.  In order to know which symbols to use, the form queries the locale definition.  You XML source peepers will be aware of the <localeSet> packet in your XDP files.  This has all the data for the locales that are explicitly referenced on the form. 

The symbols are stored in a format that looks like:

<localeSet xmlns="http://www.xfa.org/schema/xfa-locale-set/2.6/">
   <locale name="de_DE" desc="German (Germany)">
      <calendarSymbols name="gregorian"> ... </calendarSymbols>
      <datePatterns> ... </datePatterns>
      <timePatterns> ... </timePatterns>
      <dateTimeSymbols>GjMtkHmsSEDFwWahKzZ</dateTimeSymbols>
      <numberPatterns> ... </numberPatterns>
      <numberSymbols>
         <numberSymbol name="decimal">,</numberSymbol>
         <numberSymbol name="grouping">.</numberSymbol>
         <numberSymbol name="percent">%</numberSymbol>
         <numberSymbol name="minus">-</numberSymbol>
         <numberSymbol name="zero">0</numberSymbol>
      </numberSymbols>
      <currencySymbols> ... </currencySymbols>
      <typefaces> ... </typefaces>
    </locale>
    <locale> ... </locale>
</localeSet>

I was able to extract the number symbols with this function:

function findLocaleNumberSymbols(vRefField) {
    var oSymbols = {decimal: ".",
                    minus: "-"
                   };
    var vLocale = localeSet[vRefField.locale];
    if (typeof(vLocale) !== "undefined") {
        var vNumberSymbols = vLocale["#numberSymbols"].nodes;

        for (var i = 0; i < vNumberSymbols.length; i++) {
            oSymbols[vNumberSymbols.item(i).name] =
                                     vNumberSymbols.item(i).value;
        }
    }
    return oSymbols;
}

Using the numeric symbols the form is able to more accurately restrict input for numeric fields.

Choice List Fields

The last field in the sample is a reference to a choice list with the American states.  Try out the editing experience here.  It's pretty cool:

  • Input characters are limited to the set of valid choices
  • As soon as you type enough characters to uniquely identify a state, the rest of the input is completed automatically

Use Formatted and Edit Values

In the updated sample. the editing field now behaves like any other widget.  When you tab in, referenced field values display in their edit format.  When you tab out, the referenced fields display their formatted value.  In the sample you will notice that the currency and date values change when you tab in/out.  This is functionality that happens automatically on normal fields but had to be emulated in script for this sample.

You don't have to read through all the script to figure out how it works, but it is worth noting that you can access a field value in three different ways:

  • field.rawValue -- the canonical value as it is stored in the data
  • field.formattedValue -- the value with the display pattern applied
  • field.editValue -- the value with the edit pattern applied

Note that if a format or edit picture/mask is not supplied, there are default patterns for numeric and date values.

Hook up via the enter event

Previously, the editor field tapped into the script object by delegating its initialize, change and exit events.  it now also needs to delegate the enter event:

form1.LetterEdit::change - (JavaScript, client)
scEditFF.handleChangeEvent();

form1.LetterEdit::enter - (JavaScript, client)
scEditFF.handleEnter();

form1.LetterEdit::exit - (JavaScript, client)
scEditFF.handleExit();

form1.LetterEdit::initialize - (JavaScript, client)
scEditFF.initialize(this, Letter, "#c0c0c0", 10);

Futures

There are more constraints that could be enforced, e.g. digits before/after decimal but those

Populate a listbox from a web service

| 1 Comment

Jeff K asked for a sample form where we populate a listbox from the results of a web service.  He was even kind enough to point me at a set of public domain web services that could be referenced from a sample form.

The web services can be found at: http://www.webservicex.net

The sample I wrote is based on the USA Zip Code Information. Specifically the request to GetInfoByCity: http://www.webservicex.net/WCF/ServiceDetails.aspx?SID=35

The specific WSDL file can be found at: http://www.webservicex.net/uszip.asmx?wsdl

This query takes a city name as input, and returns xml data with all the zipcodes that match that city name -- in each state that has a city with that name.

Here are the steps I followed to create the sample:

Create a Data Service

Once I downloaded a local copy of the WSDL file, I used it to create a data connection named CityQuery.  When I expanded the data hierarchy to look at the input and output data, I found one data field in the request area (USCity).  Good so far.  But I was expecting more in the response area.  All I found was: "GetInfoByCityResult":

image

Turns out this web service uses <s:any> element -- which means Designer has no idea what data will be found under that node and cannot offer specific guidance for binding decisions.

If you run the sample at: http://www.webservicex.net/WCF/ServiceDetails.aspx?SID=35 , you will discover the data format by looking at the returned result:

<NewDataSet>
  <Table>
    <CITY>Mission</CITY>
    <STATE>KS</STATE>
    <ZIP>66202</ZIP>
    <AREA_CODE>913</AREA_CODE>
    <TIME_ZONE>C</TIME_ZONE>
  </Table>
  <Table>
    <CITY>Mission</CITY>
    <STATE>SD</STATE>
    <ZIP>57555</ZIP>
    <AREA_CODE>605</AREA_CODE>
    <TIME_ZONE>C</TIME_ZONE>
  </Table>
  <Table> ... </Table>
  <Table> ... </Table>
</NewDataSet>

Set up List Box Binding

I created a text field (City) and bound it to the request data: USCity.

Then I created a drop down list and set up bind to the returned data.

image

The binding wizard took me only as far as the GetInfoByCityResult element.  The rest I had to enter manually -- based on what the sample data looked like.  The bind expression for items is:

!connectionData.CityQuery.Body.GetInfoByCityResponse.GetInfoByCityResult.NewDataSet.Table[*]

Note that the expression starts with "!".  This character is used in SOM as a shortcut to the dataset elements i.e. the elements below xfa.datasets.  The data exchanged with this web service is gathered under xfa.datasets.connectionData.CityQuery

Execute the WSDL

The last step is to make sure the SOAP request happens at the right time.  On the exit event of the City field I added this script:

form1.#subform[0].City::exit - (JavaScript, client)


// Invoke the web service


xfa.connectionSet.CityQuery.execute(false);


ZipCodes.selectedIndex = 0;


The form now works.  Every time you exit the City field, the SOAP request is made and the zip code list box gets populated.

The Deep End

I wanted to populate another drop down list with more than just the ZIP codes. I wanted to include city name and state name.  The hard part about this is that once the WSDL request completes, the transaction data is removed. The moment in time where you can access the returned WSDL data is in the postExecute event (preExecute fires before a web service request, and postExecute fires afterward).  But here is the problem.  Designer does not provide an interface to specify a postExecute script.  I had to add one in the XML source view:

<event activity="postExecute" 
       ref="xfa.connectionSet.CityQuery"


       name="event__postExecute">


  <script contentType="application/x-javascript">


    console.println(xfa.datasets.saveXML("pretty"));


  </script>


</event>

Once you've specified this event in XML source view, you can edit the script in Designer by selecting "Events with Scripts" in the script editor.

By specifying preExecute and postExecute events, you can access the SOAP data before and after the web service request. 

preExecute SOAP request:

<xfa:datasets xmlns:xfa="http://www.xfa.org/schema/xfa-data/1.0/">
   <!-- Form data (data bound to fields and subforms) -->


   <xfa:data>


      <f>


         <City>Mission</City>


         <ZipCode/>


         <Choice/>


      </f>


   </xfa:data>



   <!-- The distilled WSDL schema used by XFA to construct the SOAP request -->


   <dd:dataDescription xmlns:dd=
http://ns.adobe.com/data-description/


                          dd:name="CityQueryGetInfoByCitySoapInDD">

      <CityQuery>


         <soap:Body xmlns:soap="
http://schemas.xmlsoap.org/soap/envelope/">

            <tns:GetInfoByCity xmlns:tns="
http://www.webserviceX.NET">

               <tns:USCity dd:minOccur="0" dd:nullType="exclude"/>


            </tns:GetInfoByCity>


         </soap:Body>


      </CityQuery>


   </dd:dataDescription>



   <!-- The SOAP request -->


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

      <CityQuery xmlns="">


         <soap:Body xmlns:soap="
http://schemas.xmlsoap.org/soap/envelope/">

            <tns:GetInfoByCity xmlns:tns="
http://www.webserviceX.NET">

               <tns:USCity>Mission</tns:USCity>


            </tns:GetInfoByCity>


         </soap:Body>


      </CityQuery>


   </connectionData>


</xfa:datasets>

Note that during preExecute, your script may modify the outgoing request data.

postExecute SOAP response:

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


   <!-- Form data (data bound to fields and subforms) -->


   <xfa:data> ... </xfa:data>

   <!-- The distilled WSDL schema used by XFA to construct the SOAP request -->
   <dd:dataDescription xmlns:dd=http://ns.adobe.com/data-description/ 
    ...


   </dd:dataDescription>


   <!-- The SOAP response -->


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

      <CityQuery xmlns="">


         <Body>


            <GetInfoByCityResponse xmlns="
http://www.webserviceX.NET">

               <GetInfoByCityResult>


                  <NewDataSet>


                     <Table>


                        <CITY>Mission</CITY>


                        <STATE>KS</STATE>


                        <ZIP>66202</ZIP>


                        <AREA_CODE>913</AREA_CODE>


                        <TIME_ZONE>C</TIME_ZONE>


                     </Table>


                     <Table> ... </Table>


                     <Table> ... </Table>


                     <Table> ... </Table>


                  </NewDataSet>


               </GetInfoByCityResult>


            </GetInfoByCityResponse>


         </Body>


      </CityQuery>


   </connectionData>



</xfa:datasets>

I was then able to write a script to populate the "Choice" drop down list with zipcode, city and state information:

var vTables = xfa.datasets.connectionData.CityQuery.Body.GetInfoByCityResponse.GetInfoByCityResult.NewDataSet.Table.all;


var aChoices = [];


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


    var vItem = vTables.item(i);


    var sItem = vItem.CITY.value +


                  " \t" + vItem.STATE.value +


                  "\t" + vItem.ZIP.value


    aChoices.push(sItem);


}


Choice.setItems(aChoices.join(","), 1);


Choice.selectedIndex = 0;

New Reader 9.2 API

| No Comments

Last week Adobe released Reader 9.2.  Included with this release is a new API for loading file attachment data: util.readFileIntoStream().  The reason this method has been added is because certified PDFs cannot add file attachments.  This method allows the form author to embed attachments in their XML form data stream rather than as PDF file attachments.

util.readFileIntoStream(cDIPath, bEncodeBase64)

cDIPath

(optional) A device-independent path to an arbitrary file on the user's hard drive. This path may be absolute or relative to the current document.  If not specified, the user is presented with the File Open dialog to locate the file.

If the cDIPath parameter is specified, this method can be executed only in a privileged context, during a batch or console event, or when the document is certified with a certificate trusted to execute "embedded high privileged javascript".

bEncodeBase64
(optional) If true, base 64-encode the file content. Defaults to false.

Returns

The File content as a ReadStream object that is optionally base 64-encoded.  If the user cancels the dialog, the method returns "undefined".

Example

The following sample shows how to use the API to load attachments.  The button script to load an attachment looks like:

var vStream = util.readFileIntoStream({bEncodeBase64:true});
if (typeof(vStream) !== "undefined") {
    var vNewAttach = _attach.addInstance();
    vNewAttach.contents.rawValue = util.stringFromStream(vStream);
}

Futures

It would be nice if the method provided the name of the loaded file.  It would also be nice if there was a corresponding util.writeStreamToFile() method.  I don't know whether these enhancements are planned or not.

Populating List Boxes

| 6 Comments

When I occasionally browse around some of the LiveCycle forums, I frequently see questions around how to populate a drop down list.  I have put together a sample form that illustrates several different options.

Data Source

There are two basic sources for updating a drop down list definition: data binding or script.  If the list contents are defined as part of your form data, and if they don't change during your form session, then use data binding.  If the definitions are more fluid, then use script.

The preOpen Event

The most important take-away from this blog entry concerns what event to use for updating lists.  I have seen customer forms that update list contents using change, exit, calculate, validate, mouseover, and enter events.  However, the proper place to do it is in the preOpen event.  The preOpen event fires when the user activates the control for the choice list.  Think of it as the "just-in-time" option.  If you try to maintain your list box definition from other events then often your script will update your list box contents too frequently. 

The only reason for updating a choice list sooner than the preOpen event is if you need to assign a value to a list field.  e.g. if your drop down list has a display value: "ONE" and a bound value: "1", then assigning
field.rawValue = 1; will cause the field to display "ONE".  Obviously this works only if the field has up to date list contents.  If you need your list contents updated more frequently, you should still put the list populating logic in the preOpen event, and use execEvent("preOpen") to populate the list from other contexts where it's needed.

The sample form has four choice lists that get populated from their preOpen event, using data found in form field values, JavaScript arrays and XML data.

Script Commands

There are two script methods for setting your list box contents:
field.addItem()
   and
field.setItems()

The API: field.addItem() works in all versions of Reader.  field.setItems() was introduced in Reader 9.0 and is much faster and more convenient.  The sample form has script that illustrates how to use each method.

Binding to Data

I constructed some sample data (dogs.xml) that looks like this:

<dogs>
  <category file="ugliest">
    <rank>1</rank><dog>Chinese Crested</dog>
    <rank>2</rank><dog>Pug</dog>
    <rank>3</rank><dog>Shih Tzu</dog>
    <rank>4</rank><dog>Standard Schnauzer</dog>
    <rank>5</rank><dog>Chinese Shar Pei</dog>
    <rank>6</rank><dog>Whippet</dog>
    <rank>7</rank><dog>Dandie Dinmont Terrier</dog>
    <rank>8</rank><dog>Japanese Chin</dog>
    <rank>9</rank><dog>French Bulldog</dog>
    <rank>10</rank><dog>Chihuahuas</dog>
  </category>
  <category file="dumbest">
   ...
  </category>
  <category file="comicBook">
   ...
  </category>
  <category file="smartest">
   ...
  </category>
</dogs>

On my form I want two drop down lists: one with the names of the categories and a second that gets populated with the contents of the category.  Since this XML is part of my data, I bound the category using these expressions:

binding

The second list has a preOpen event that locates the category in the data, and then populates a second listbox from the category contents:

// Find the data group that corresponds to the category chosen
// in the Category field

var vDogs = xfa.datasets.data.dogs.category.all;
var vChoice = null;
var i;
for (i = 0; i < vDogs.length; i++) {
    if (vDogs.item(i).file.value === Category.rawValue) {
        vChoice = vDogs.item(i);
        break;
    }
}
if (vChoice !== null) {
    // vChoice.dog.all is the equivalent of the
    //
SOM expression: "category.dog[*]"
    var vDisplayValues = vChoice.dog.all;
    var vBindValues = vChoice.rank.all;
    for (i = 0; i < vDisplayValues.length; i++) {
        this.addItem(vDisplayValues.item(i).value,
                    
vBindValues.item(i).value);   
    }
}

Performance

Populating a list from data is very efficient.  But populating large lists or many lists using addItem() can be slow.

The performance gains of setItems() over addItem() is substantial.  If your form makes extensive use of choice lists or has choice lists with large contents, you will appreciate the improvements of setItems().  Of course, this option is available only in forms designed for Reader 9 or later.

Web Services

In some cases, the definition of the choice list may be held on a server.  In this scenario, the best strategy is to add a WSDL connection to your form that retrieves the list contents.  Have your list box bind its contents to the data retrieved via the SOAP call.

Away for a week

After I eat lots of turkey on the weekend (Canadian Thanksgiving), I'm spending next week trying to empty the job jar at home.

Recent Comments

  • John Brinkman: Mo: Well, turns out it's complicated. So much so that read more
  • Mo: John, I can do this with Livecycle Designer 8.2? If read more
  • John Brinkman: Mo: Any time we import data into a form we read more
  • mo: Is it possible to populate a form from 2 different read more
  • Jeff K: Hi John, Thanks for the great example! Jeff K Columbus read more
  • John Brinkman: Jeff: There you go... A new blog entry that hopefully read more
  • John Brinkman: Good question. I've updated the sample to do what you're read more
  • DSP: Is there anyway that when you pick Dog Category "smartest" read more
  • Jeff K: John: In the past, for testing I have used www.webservicex.net. read more
  • John Brinkman: Jeff: That's a good suggestion. Do you know of a read more

Recent Assets

Find recent content on the main index or look in the archives to find all content.