Chapter 38. Data Replication

Table of Contents

1. Introduction
2. Implicit Replication
2.1. Replication Manager
2.2. Pooling
2.3. Lazy replication
2.4. $path bindings and replication
2.5. $path syntax to determine order of replicated views
2.6. Be careful of mixing replication and classes declared procedurally
3. Explicit Replication

1. Introduction

There are two types of data replication, implicitand explicit.

Implicit replication is the earlier form, and while it works well, it has some problems:

  • It can make the code harder to read.

  • It can be inefficient.

  • It can sometimes be fragile.

The newly introduced explicit replication solves these problems by mirroring the (final) runtime view structure in the replicating declaration. We recommend that you use explicit replication in all new code that you write. You can change from implicit to explicit replication by changing this:

    <view ... datapath="...">
     ...
    </view>
    

to this:

   <replicator datapath="...">
     <view ...>
       ...
     </view>
   </replicator>
    

2. Implicit Replication

As shown in some of the examples above, datapaths that match multiple nodes cause their nodes to be replicated. By "replicated", we mean that for each match of the XPath expression one instance of the mapped view is created. This is one of the most important features of the databinding facilities in LZX.

2.1. Replication Manager

A replication manageris a runtime object that is created automatically whenever data replication occurs as a result of a datapath matching more than once. When that happens, the nameor idattribute of the replicated view (if the view is named) is taken over by the replication manager, and from then on referring to that name will access the replication manager object, and not the view. In order to reference the replicated views, known as clones, you should use the lz.ReplicationManagerAPI.

2.1.1. The replicationattribute

If a datapath matches multiple nodes, it will create a replication manager. If replicationis normal(the default), then the replication manager will be a direct instance of lz.ReplicationManager. If it is lazy, it will instead create a lz.LazyReplicationManager.

2.1.2. Clones and the onclonesevent

As mentioned above, when a view is replicated, its copies are managed by the replication manager object. Once clones are created, the instance of the replication manager contains references to them in the clonesproperty, which is an array of views. Note that lz.ReplicationManagerextends lz.datapath, and a cloned view along with its datapath is replaced with the replication manager object. Armed with this knowledge, we have a technique for determining when a view is cloned. The example below demonstrates the use of the clones property by declaring a handler for the onclonesevent on the view's datapath.

Example 38.1. Using clones and the onclones event

<canvas width="100%" height="200">
  <dataset name="tabnames">
    <title name="Account Info"/>
    <title name="Order History"/>
    <title name="Preferences"/>
    <title name="Shopping Cart"/>
  </dataset>
  <simplelayout axis="x" spacing="25"/>
  <button text="Create tabs">
    <handler name="onclick">
      gs.pane.setAttribute('datapath', 'tabnames:/title')
      bs.pane.setAttribute('datapath', 'tabnames:/title')
    </handler>
  </button>
  
  <class name="repltabelt" extends="tabelement" text="$path{'@name'}" visible="true"/>
    <tabslider width="150" name="gs" height="150" spacing="2">
      <repltabelt name="pane">
        <datapath>
          <handler name="onclones">
            if (!this['doneDel']) {
              this.doneDel = new LzDelegate(this, 'openOH')
              this.doneDel.register(clones[clones.length - 1], 'oninit')
            }
          </handler>
          <method name="openOH">
            parent.select(this.getCloneNumber(0))
          </method>
        </datapath>
      </repltabelt>
    </tabslider>
      
    <tabslider width="150" name="bs" height="150" spacing="2">
      <repltabelt name="pane">
        <datapath>
          <handler name="onclones">
          parent.select(this.getCloneNumber(0))
        </handler>
      </datapath>
    </repltabelt>
  </tabslider>
</canvas>

Because the onclonesevent is sent when the clonesattribute is set, it only signals the start of view replication, but in this example it is used to determine the exact moment when replication is finished. Since replicated views are initialized in the same order they are in inserted in the clones array, we only need to wait for the oninit event for the last clone in the list. This is necessary because initialization of the tabelements takes a non-zero amount of time, and an attempt to perform an operation on their container — tab slider — before it is completed will leave the component in an inconsistent state. For illustration purposes, the second tabsliderhas this problem, whereby selecting the first tabelement too soon renders its parent unusable (the other tabelements are gone).

This example also takes advantage of the fact that, by default, views become visible when they consume data (see section on visibility of datamapped views above). Before the button is clicked, there is a single tabelementobject within the tabslider. However, it is kept invisible until it receives data, at which point its replication occurs, and its clones are displayed.

2.1.3. Nodes and the onnodesevent

Similarly to the clonesproperty, lz.ReplicationManagermaintains a list of matched data nodes in the nodesproperty. It is an array of lz.DataElementobjects that are mapped to the replicated views, and is available before any clones are created. And as with the onclonesevent, a handler for onnodesmay be declared to respond to data replication in a custom way. The code below qualifies the value of nameattribute of each replicated data node with the value of the text field, if any.

Example 38.2. Using the nodes property

<canvas height="200" width="100%">
  <dataset name="tabnames">
    <title name="Account Info"/>
    <title name="Order History"/>
    <title name="Preferences"/>
    <title name="Shopping Cart"/>
  </dataset>
  <simplelayout axis="x" spacing="25"/>
  <button text="Create tabs for user:">
    <handler name="onclick">
      nav.pane.setDatapath('tabnames:/title')
    </handler>
  </button>
  
  <edittext name="user" width="120" options="ignorelayout" y="25"/>
  <tabslider width="150" name="nav" height="150" spacing="2">
    <tabelement name="pane" text="$path{'@name'}" visible="true">
      <datapath>
        <handler name="onnodes"><![CDATA[
          if (user.text.length)
            for (var i = 0; i < nodes.length ; i++) {
              var title = nodes[i].getAttr('name')
              pos = title.indexOf(':')
              if (pos != -1)
                title = user.text + title.substr(pos)
              else 
                title = user.text + ': ' + title
              nodes[i].setAttr('name', title)
            }
        ]]></handler>
      </datapath>
    </tabelement>
  </tabslider>
</canvas>

2.2. Pooling

If your application uses data replication and the data backing replicated views changes at runtime, by default the replication manager destroys and re-creates the replicated views whose data has changed. The typical scenarios when this will occur are a change in the datapath of the replicated view, or deletion/addition of rows to the dataset. Because the dataset may contain many data elements, this adjustment is often an expensive operation that results in a noticeable flicker of the user interface while view removal/creation takes place.

In order to make updates to datamapped elements more efficient, you can declare the datapath that will match multiple nodes with the poolingattribute set to true. The effect of this is that the views that have already been created as a result of replication will be reused internally, instead of re-created. Since the replication manager only needs to remap the changed data to the existing clones, data updates are reflected in UI much faster than they would be if the runtime had to create new views. Consider the following example.

Example 38.3. Using pooling to optimize data updates

<canvas height="300" width="100%">
  <dataset name="phonebook" src="resources/phonebook.xml"/>
  
  <simplelayout axis="y" spacing="3"/>
  <view>
    <view name="newContact" datapath="new:/contact">
      <text>First Name:</text>
      <edittext name="firstName" datapath="@firstName" x="80"/>
      <text y="25">Last Name:</text>
      <edittext name="lastname" datapath="@lastName" x="80" y="25"/>
      <text y="50">Phone:</text>
      <edittext name="phone" datapath="@phone" x="80" y="50"/>
      <text y="75">Email:</text>
      <edittext name="email" datapath="@email" x="80" y="75"/>
      <button width="80" x="200">Add
        <handler name="onclick">
          parent.datapath.updateData();
          var dp = phonebook.getPointer();
          dp.selectChild();
          dp.addNodeFromPointer(parent.datapath);
          parent.setAttribute("datapath", "new:/contact");
        </handler>
      </button>
    </view>
  </view>
  <button text="Delete selected">
    <handler name="onclick"><![CDATA[
      for (var c = 0; c < all.nodes.length;) {
        var clone = all.clones[c];
        if (clone.datapath.xpathQuery('@checked') == 'true') {
          clone.datapath.deleteNode();
        } else {
          c += 1;
        }
      }
    ]]></handler>
  </button>
  
  <view name="all">
    <datapath xpath="phonebook:/phonebook/contact" pooling="true"/>
    <view>
      <simplelayout axis="x"/>
      <checkbox width="30" datapath="@checked">
        <handler name="onvalue">
          datapath.updateData();
        </handler>
        <method name="updateData">
          return String(this.value);
        </method>
        <method name="applyData" args="d">
          this.setValue(d);
        </method>
      </checkbox>
      <text datapath="@firstName"/>
      <text datapath="@lastName"/>
      <text datapath="@phone"/>
      <text datapath="@email"/>
    </view>
  </view>
</canvas>

In the code above, we handle data removal by going through the list of data nodes, and deleting the nodes whose checkedattribute is set to "true". Note how this attribute is controlled by and mapped to the value of the corresponding checkbox. Any change in the state of the checkbox results in an update to the data node attribute, and vice versa — when views are created or reused (due to deletion), the appearance of their checkboxes is unchecked because initially the attribute is not set.

This kind of syncing to the underlying data is generally required when pooling is in effect and the state of the visual elements can be changed as a result of a user interaction. In a simpler case, the UI would not be modifiable by the user, so the data flow is one way only and the views are completely data-driven, and therefore consistency of data with its presentation would be maintained automatically.

2.2.1. When not to use pooling

Pooling is generally a good optimization in cases where the data completely informs the state of a replicated view. If the view has additional state which can change through user interaction or depends on setting attributes at init time, then this option cannot usually be used. The default value for the poolingon <dataset>is "false", except when replication is set to lazy, in which case it must be true, as described below.

2.3. Lazy replication

If a datapath's replicationattribute is set to lazy, then a match to multiple nodes will create an lz.LazyReplicationManagerinstead of an lz.ReplicationManager. This kind of replication manager is called "lazy" because it doesn't do the work of creating a view until it has to, and it does the bare minimum of work. The lazy replication manager creates only enough replicated views necessary to display the data, so there is not a view for each data node. This enables the display of very large datasets.

Because the lz.LazyReplicationManageris relatively specialized, there are several restrictions on its use:

  • The replicated views should be contained in a view which is not the view that clips. The replicated views can be positioned by moving this container. This container will be sized to the size of the replicated list.

  • The parent of the container must be a view that clips (that is, its clipattribute is set to "true".

  • The replicated view cannot change its size in the replication axis, and the size cannot be a constraint. If the replicated view is sized by its contents, then lazy replication may not work in all cases.

  • The data should completely inform the display of the view. Any attributes that are changed through interaction with a replicated view should be stored in the dataset.

  • Selection within the replicated views should be controlled by a lz.dataselectionmanager.

This example shows use of the lazy replication manager to display a large dataset. The replication does not create a view for each node in the dataset; rather it only creates enough views to fill the clipping view that contains it. As you click the "Make it bigger" button, you will see that more items from the list are shown. Notice also that these views are actually being created when you press the button, as you can see by then "number of subviews" value at the top of the canvas.

Example 38.4. Using a lazyreplicationmanager to display a large dataset

<canvas height="350" width="100%">
  <dataset name="vegetables">
    <celery/> <celeriac/> <carrot/> <florence_fennel/> <parsnip/> 
    <parsley/> <winter_endive/> <witloof_chicory/> <cardoon/> 
    <artichoke/> <head_lettuce/> <cos_lettuce/> <black_salsify/> 
    <swedish_turnip/> <cauliflower/> <cabbage/> <brussels_sprouts/> 
    <kohlrabi/> <broccoli/> <savoy_cabbage/> <turnip/> <radish/> 
    <water_cress/> <garden_cress/> <foliage_beet/> <spinach/> 
    <sweet_potato/> <watermelon/> <melon/> <cucumber/> <winter_squash/> 
    <marrow/> <chickpea/> <lentil/> <runner_bean/> <common_bean/> 
    <pea/> <faba_bean/> <leek/> <shallot/> <onion/> <salsify/> 
    <welsh_onion/> <garlic/> <chives/> <asparagus/> <ladyfinger/> 
    <sweet_corn/> <rhubarb/> <capsicum_pepper/> <tomato/> <eggplant/>
  </dataset>

  <simplelayout spacing="10"/>
  <text width="200">
    <handler name="onaddsubview" reference="replicationParent">
      this.setAttribute("text", 'number of subviews: ' + 
                   replicationParent.subviews.length);
    </handler>
  </text>

  <view clip="true" width="100" height="100" id="clipper" bgcolor="silver">
    <view id="replicationParent">
      <text>
        <datapath xpath="vegetables:/*/name()" replication="lazy"/>
      </text>
    </view>
    <scrollbar/>
  </view>
  <button>Make it bigger
    <handler name="onclick">
      clipper.setAttribute('height', clipper.height + 50);
    </handler>
  </button>
</canvas>

See the paging.lzx examplefor another example of lazy replication.

2.4. $path bindings and replication

Only a datapath can cause replication. Although it might seem that $path might be used to implicitly force replication, it will not. A $path expression will only yield a single value. If it matches multiple values, it is an error and it will act as if it matched none. In the example below, note that The $path constraint does not update when the enclosing datapath is set.

Example 38.5. $path does not replicate

<canvas height="300" width="100%" debug="true">
   <debug y="100"/>
   <dataset name="ds">
     <data>
       <person name="a assdfasfva asdf sad" surname="a surname"/>
       <person name="b" surname="b surname"/>
       <person name="c" surname="c surname"/>
     </data>
   </dataset>
   <simplelayout axis="y"/>
   <button onclick="thedata.setAttribute('datapath', 'ds:/')">Set datapath</button>
   <view id="thedata" ondata="Debug.warn('data %#w', arguments[0])">
     <simplelayout axis="y"/>
     <view>
       <simplelayout axis="x"/>
       <text>Datapath:</text>
       <view>
         <simplelayout axis="y"/>
         <text datapath="data/person/@name" resize="true" ondata="Debug.warn('datapath.ondata')"/>
       </view>
     </view>
     <view>
       <simplelayout axis="x"/>
       <text>$path:</text>
       <view>
         <simplelayout axis="y"/>
         <text text="$path{'data/person/@name'}" resize="true" ondata="Debug.warn('$path.ondata')"/>
       </view>
     </view>
   </view>
 </canvas>

2.5. $path syntax to determine order of replicated views

Sometimes you want to know the position of a view; for example, say you wanted to alternate background colors. You might think of checking for the position of the view in its oninit()method.

However, if you're using datapath pooling (you'll probably want to for long lists), the oninitevents for views created by data replication don't necessarily fire because the views may be reused. In that case, the ondataevent will fire, so you might consider using the ondata()handler. However, incrementing a counter isn't the most reliable way to determine order because views may not instantiate in linear order.

That's why it's better to use a datapath expression. Add attribute like this inside your replicated node:

<attribute name="pos"
      value="$path{'position()'}"/>

This will tie the pos attribute to the physical position in the data. You can then then tie the background color like so:

<attribute name="bgcolor"
      value="${this.pos % 2 == 0 ? 0x00EEEE :
      0x00DDDD}"/>

2.6. Be careful of mixing replication and classes declared procedurally

Views that you create procedurally are not the same as "clones" created by data replication. In fact, data replication overrides procedurally created views. For example:

  1. Declare a view.

  2. Add subviews to it (procedurally), and alter its properties.

  3. Set a datapath on the view (from step 1) that would make it replicate.

Changes made in step 2 will be ignored after replication.

3. Explicit Replication

Explicit replication improves upon implicit replication by adding a replicator tag representing a replication manager:

<view> <replicator
    datapath="ds:/people/person/"> <view
    name="$path{'@name'}"/> </replicator>
    </view>

The replicator tag has the following legal tag attributes (in addition to those inherited from datapath):

  • sortpath: xpath to the sort key

  • sortorder: comparator function for sorting

  • datapath: dataset:xpath

`datapath` is a shorthand for `data="${dataset.p.xpathQuery(xpath)}"` It constrains the `data` attribute of the replicator to the result of the xpath query on the dataset. Note that this meaning is different from the meaning of `datapath` on a view, which implies old-style implicit replication.

The single lexical subnode of the replicator tag is a view that will be used as a template to be replicated zero or more times, depending on the number of nodes the datapath attribute matches. The replicator tag only permits a single child node.

Note that inside a replicator tag, `${path}` constraints are used to bind views to the data (`datapath` is not used).

The replicator will be a child node of its parent view, appearing in the parent's subnodes array (but not in the parents subviews array). If named or given an identifier, it can be queried and controlled via that name or identifier.

For example, the code above might lead to the following DOM structure at runtime:

lz.view { … }
  lz.ReplicationManager { datapath: { ... }, ... }
  lz.view { name: 'Bob' }
  ...
  lz.view { name: 'Edna' }
  

The replicator has additional attributes and methods that are accessible to Javascript:

  • data: array of dataelements the datapath matches (possibly empty)

  • views: array of views bound to the nodes (possibly empty)

There is a one-to-one correspondence between data elements and views.

Replicator variants:

replicator can be subclassed. Two subclasses are pre-defined:

  • lazyreplicator: only creates views that will be visible within the parent view.

  • resizereplicator: a lazy replicator where each view may be of a different size.

lazy replication implies that the views will be `pooled`. There is no separate pooling (Section 2.2, “Pooling”) control in explicit replication.