Table of Contents
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>
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.
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.
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.
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>
<!-- * X_LZ_COPYRIGHT_BEGIN ***************************************************
* Copyright 2007, 2008 Laszlo Systems, Inc. All Rights Reserved. *
* Use is subject to license terms. *
* X_LZ_COPYRIGHT_END ****************************************************** -->
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.
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>
<!-- * X_LZ_COPYRIGHT_BEGIN ***************************************************
* Copyright 2007, 2008 Laszlo Systems, Inc. All Rights Reserved. *
* Use is subject to license terms. *
* X_LZ_COPYRIGHT_END ****************************************************** -->
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>
<!-- * X_LZ_COPYRIGHT_BEGIN ***************************************************
* Copyright 2007-2008 Laszlo Systems, Inc. All Rights Reserved. *
* Use is subject to license terms. *
* X_LZ_COPYRIGHT_END ****************************************************** -->
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.
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.
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>
<!-- * X_LZ_COPYRIGHT_BEGIN ***************************************************
* Copyright 2008 Laszlo Systems, Inc. All Rights Reserved. *
* Use is subject to license terms. *
* X_LZ_COPYRIGHT_END ****************************************************** -->
See the paging.lzx examplefor another example of lazy 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>
<!-- * X_LZ_COPYRIGHT_BEGIN ***************************************************
* Copyright 2007, 2008 Laszlo Systems, Inc. All Rights Reserved. *
* Use is subject to license terms. *
* X_LZ_COPYRIGHT_END ****************************************************** -->
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}"/>
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:
Declare a view.
Add subviews to it (procedurally), and alter its properties.
Set a datapath on the view (from step 1) that would make it replicate.
Changes made in step 2 will be ignored after 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.
Copyright © 2002-2010 Laszlo Systems, Inc. All Rights Reserved. Unauthorized use, duplication or distribution is strictly prohibited. This is the proprietary information of Laszlo Systems, Inc. Use is subject to license terms.