PDA

View Full Version : Lazy population of trees


yoDon
08-29-2004, 10:05 AM
I'm building an app with a couple tree views, and the trees are getting big enough that it's taking many seconds for them to populate. I've eventually encountered this problem with just about every ui toolkit I've ever used, and have generally solved it by lazily populating the tree (adding child nodes only when the user opens the corresponding branch of the tree).

It looks like there should be some really slick way to do this using Laszlo's tree onopen event, but I can't quite figure it out.


<!-- NOTE: this is just pseudo code -->
<!-- it won't actually work, but hopefully -->
<!-- it gets the idea across -->

<tree id="Root" datapath="Dp:/Root/" showroot="false">
<tree datapath="Level1" onopen="HandleOpen">
<tree datapath="" lazyDatapath="Level2" onopen="HandleOpen">
<tree datapath="" lazyDatapath="Level3" isleaf="true"/>
</tree>
</tree>
</tree>

<method name="HandleOpen" args="NodeThatOpened">
for( Child in NodeThatOpened.subtrees )
{
Child.datapath = Child.lazyDatapath;
}
</method>

Unfortunately (as mentioned in my previous post on procedurally selecting nodes) I can't find a way to locate the tree node children of a tree node. The code wouldn't need to find the children if there were an onparentopened or onbecomesvisible handler, but I haven't found anything of that sort, either.

I'm also in the dark on whether event handlers such as my HandleOpen above have access to the node that caused the event. It would sure be handy, but I haven't run across an example of this yet (I'm still very much a noob when it comes to Laszlo).

-Don

pablo
08-30-2004, 04:18 PM
Hi Don,

The visible components of a tree are located in a view called "items". See the source in WEB-INF/lps/components/base/basetree.lzx and WEB-INF/lps/components/lz/tree.lzx for details.

Here's an example on how to do what you want:

<canvas width="800" height="600">

<include href="lz/tree.lzx" />

<dataset name="l1">
<root>
<level name="a">
<level name="a1" />
<level name="a2">
<level name="A21" />
<level name="A22" />
<level name="A23" />
</level>
<level name="a3" />
</level>
<level name="b">
<level name="b1" />
<level name="b2" />
<level name="b3" />
</level>
<level name="c">
<level name="c1" />
<level name="c2" />
<level name="c3">
<level name="C31" />
<level name="C32" />
<level name="C33" />
</level>
</level>
</root>
</dataset>

<class name="lazytree" extends="tree" >

<!-- Turn off auto-recursion for tree. Get child nodes when tree is
opened. -->
<attribute name="recurse" value="false" type="boolean" />
<attribute name="haveChildren" value="false" type="boolean" />

<method event="onopen" args="o">
<![CDATA[
// If we already have children, don't fetch them again.
if (this.haveChildren) return;

if (o) {

// Now set recurse to true. This value is used in basetree's
// createChildTrees() method. See
// lps/components/base/basetree.lzx for and
// lps/components/lz/treee.lzx source code.
this.setAttribute('recurse', true);
createChildTrees();
this.setAttribute('recurse', false);

// We have child nodes.
this.setAttribute('haveChildren', true);

}
]]>
</method>

</class>


<view x="20" y="20" width="500" height="400">
<lazytree datapath="l1:/root" showroot="false">
<lazytree datapath="level" text="$path{'@name'}" />
</lazytree>
</view>

</canvas>

Note how recurse is set to false so the tree doesn't create its child nodes during init. We manually do it during the onopen event.

I'm also in the dark on whether event handlers such as my HandleOpen above have access to the node that caused the event.


Instead of calling your handler through the onopen attribute, try declaring the event handler:

<canvas ...>

<tree datapath="...">
<tree datapath="...">
<method event="onopen">
canvas.HandleOpen(this);
</method>
</tree>
</tree>

<method name="HandleOpen" args="NodeThatOpened">
// your code
</method>

</canvas>

pablo

yoDon
08-30-2004, 05:17 PM
Fantastic.

This looks like a much nicer solution to both my problems than I had been able to come up with.

many thanks,
-Don

yoDon
08-30-2004, 07:49 PM
Well, it looked good in the post, but the more I looked into it, the more "exercises for the student" I found...

To help me understand how your code was working, I wired up my classes to derive off of lazytree and fired up the app in the debugger. Without opening any of the branches, I started looking through the object hierarchy in the debugger. Unhappy me because the tree proves to be fully populated before any of the branches are opened (and as a second test I noticed that the tree took the same amount of time to populate as before the change).

I'm starting to wonder if there is a bug in createChildTrees().

As a test, I tried rewiring my app to use

<class name="NonRecursingTree" extends="tree" recurse="false"/>

Which (if my understanding of basetree::createChildTrees() is correct) should prevent any tree nodes from being created, but the tree ends up fully populated.

I'm not very familiar with JavaScript, but I'm wondering if the "var c = this.getChildClass();" in basetree might be returning "basetree" instead of "NonRecursingTree". That, coupled with the "<attribute name='recurse' value='true'>" line in basetree might explain why the tree is recursing even after it's been told not to.

-Don

ps. I couldn't find the "items" view you mentioned... it looked to me like I needed to look in children.subviews[ index ] for the child objects.

metasarah
08-30-2004, 08:34 PM
You can't override an event. (It is a feature that if you define an event, both the new event and that of the superclass will get called.) However, you can override a method, such as createChildTrees(), and the new method (of the subclass) will get called by the event in the superclass.

By the way: there are two views in basetree, item and children. children is the "defaultplacement" view, which means that all the child nodes will be placed in there. item is where you would place the visible component of the node (such as a folder icon and title).

Hope that helps!

Sarah

yoDon
08-30-2004, 08:50 PM
Whoops, sorry, I did an edit that removed the context for metasarah's event answer. My bad. I didn't expect anybody to respond that quickly.

-Don

yoDon
08-31-2004, 10:16 AM
BTW, metasarah- thanks for the tip on the "defaultplacement" attr. I'd been quite puzzled about how to get my classes to implement that type of behavior. Now that I know what to search for I can see how it's done.

cheers,
-Don

pablo
09-21-2004, 11:44 AM
Hi Don,

Check out the following code and let me know if you have any questions. I've included explanations in the comments, but let me know if you have any other questions.

Thanks for your patience!

pablo


<canvas width="800" height="600">

<debug x="145" height="540" width="640" />

<include href="lz/tree.lzx" />

<dataset name="l1">
<root>
<level1 name="a">
<level2 name="a1" />
<level2 name="a2">
<level3 name="A21" />
<level3 name="A22" />
<level3 name="A23" />
</level2>
</level1>
<level1 name="b">
<level2 name="b1" />
<level2 name="b2" />
</level1>
<level1 name="c">
<level2 name="c1" />
<level2 name="c2">
<level3 name="C31" />
<level3 name="C32" />
<level3 name="C33" />
</level2>
</level1>
<level1 name="d">
<level2 name="D1" />
<level2 name="D2" />
<level2 name="D3" />
<level2 name="D4" />
</level1>
</root>
</dataset>

<!-- Don't allow recursion for lazytree. Lazytree node sets datapaths for a
declared lazytree when it's open. -->
<class name="lazytree" extends="tree" recurse="false">

<!-- Array to save late data binding attribute. -->
<attribute name="lateDataBindAttr" value="[]" type="expression" />

<!-- Use this attribute instead of using datapath to make sure tree node
doesn't replicate on a datapath during init. -->
<attribute name="lazydp" value="$once{null}" type="string" />

<!-- During open, set correct datapath for children. -->
<method event="onopen" args="o">
<![CDATA[
var sv = this.children.subviews;

// Return if there was no declared lazytree view.
if (sv == null) return;

// if datapath exists, it indicates the child was already created.
if (sv[0].datapath != null) return;

if (sv[0].lazydp != null && o) {

// Set the datapath for the child with its lazydp setting.
sv[0].setDataPath(sv[0].lazydp);

// Iterate through lateDataBindAttr array and databind each
// attribute to set datapath.
for (var i=0; i < this.lateDataBindAttr.length; i++) {
var attr = this.lateDataBindAttr[i];
this.dataBindAttribute( attr[0], attr[1] );
}
}
]]>
</method>

<!-- The dataBindAttribute() method binds attributes with $path syntax
to a datapath (see doc in LzNode). However, since the datapath can
be null in lazytree, we'll want to wait before binding the
attribute. -->
<method name="dataBindAttribute" args="attr,path">
if ( ! this.datapath ) {
this.lateDataBindAttr.push( [ attr, path ] );
} else {
super.dataBindAttribute(attr, path);
}
</method>

</class>


<view x="20" y="20" width="500" height="400">
<lazytree id="xxx" datapath="l1:/root" showroot="false">
<lazytree datapath="level1" text="$path{'@name'}" >
<lazytree lazydp="level2" text="$path{'@name'}" >
<lazytree lazydp="level3" text="$path{'@name'}" />
</lazytree>
</lazytree>
</lazytree>
</view>

</canvas>

yoDon
09-21-2004, 04:25 PM
I'm seeing a *HUGE* performance boost in my application.

-Don

rpecoraro
11-23-2004, 08:34 PM
I was also looking for something like this, except with the additional complication that the onopen should trigger an update on the dataset itself.

So, given some tree node, if I don't have any children for that node yet, request them for this node from some servlet (using an attribute of this node as a request parameter). Retrieve any child nodes from the response using xpath and add them to this node (or make it a leaf, if there aren't any child nodes).

Can this be done? Do I need to add any lazily retrieved child nodes to the dataset and then somehow fire an update on the lazytree, or is it enough to use the dataBindAttribute method?

Thanks
Renzo

kumar
06-28-2005, 02:06 AM
Hi Pablo,
This post regarding the lazy population of trees has been extremely useful.
However, when using the lazytree with a dataset that had a node with a single child, I encountered a problem that the text for such a child node does not appear. Is this an issue you have observed ?

Eg: I have just modified the dataset in your example so that one of the nodes has a single child (modified section in blue). Now, the text for that child node does not appear. Can you shed some light on this



<canvas width="800" height="600">

<debug x="145" height="540" width="640" />

<include href="lz/tree.lzx" />

<dataset name="l1">
<root>
<level1 name="a">
<level2 name="a1" />

<level2 name="a2">
<level3 name="A21" />
</level2>

</level1>
<level1 name="b">
<level2 name="b1" />
<level2 name="b2" />
</level1>
<level1 name="c">
<level2 name="c1" />
<level2 name="c2">
<level3 name="C31" />
<level3 name="C32" />
<level3 name="C33" />
</level2>
</level1>
<level1 name="d">
<level2 name="D1" />
<level2 name="D2" />
<level2 name="D3" />
<level2 name="D4" />
</level1>
</root>
</dataset>

<!-- Don't allow recursion for lazytree. Lazytree node sets datapaths for a
declared lazytree when it's open. -->
<class name="lazytree" extends="tree" recurse="false">

<!-- Array to save late data binding attribute. -->
<attribute name="lateDataBindAttr" value="[]" type="expression" />

<!-- Use this attribute instead of using datapath to make sure tree node
doesn't replicate on a datapath during init. -->
<attribute name="lazydp" value="$once{null}" type="string" />

<!-- During open, set correct datapath for children. -->
<method event="onopen" args="o">
<![CDATA[
var sv = this.children.subviews;

// Return if there was no declared lazytree view.
if (sv == null) return;

// if datapath exists, it indicates the child was already created.
if (sv[0].datapath != null) return;

if (sv[0].lazydp != null && o) {

// Set the datapath for the child with its lazydp setting.
sv[0].setDataPath(sv[0].lazydp);

// Iterate through lateDataBindAttr array and databind each
// attribute to set datapath.
for (var i=0; i < this.lateDataBindAttr.length; i++) {
var attr = this.lateDataBindAttr[i];
this.dataBindAttribute( attr[0], attr[1] );
}
}
]]>
</method>

<!-- The dataBindAttribute() method binds attributes with $path syntax
to a datapath (see doc in LzNode). However, since the datapath can
be null in lazytree, we'll want to wait before binding the
attribute. -->
<method name="dataBindAttribute" args="attr,path">
if ( ! this.datapath ) {
this.lateDataBindAttr.push( [ attr, path ] );
} else {
super.dataBindAttribute(attr, path);
}
</method>

</class>


<view x="20" y="20" width="500" height="400">
<lazytree id="xxx" datapath="l1:/root" showroot="false">
<lazytree datapath="level1" text="$path{'@name'}" >
<lazytree lazydp="level2" text="$path{'@name'}" >
<lazytree lazydp="level3" text="$path{'@name'}" />
</lazytree>
</lazytree>
</lazytree>
</view>

</canvas>



Thanks and regards,
kumar