Up a Tree Without a Paddle (and Down Again)
June 21, 2006 on 12:03 pm | In Flex, Programming |The last few days have been spent working with Adobe, sorting through a blizzard of serious issues and bugs in Flex 2’s Tree control. Some of the outcomes from this seem worth sharing, so that others might avoid various painful problems.
I’ll start at the end, lest this be interpreted as a Flex Flame: Adobe was extremely responsive to fixing the problems and clearing up unclear points, and in the last release I’ve looked at (6/19), the Tree control behaves well — if you treat it right.
Now, how does a Tree want to be treated? I’ll start at the beginning.
In one of our applications at Allurent, we used a Tree to browse a large remote graph of relationships starting at a “root object” that is the root node of the tree. The data structure was so large (potentially millions of nodes) that there was no hope of reading it all in at once, so we opted to make a “lazy Tree” that didn’t access a node’s children until the node is expanded by the user. At any given point, the Tree represents a data provider that goes no deeper than what one sees in the display.
The official approach to this kind of problem is to implement an Flex 2 interface named ITreeDataDescriptor (not really an appropriate name, since it does much more than describe data), and provide the Tree with an instance of it. This object sits in between the Tree and its dataProvider; the Tree uses it to determine whether a node object can be expanded or not, whether it has children or not. The Tree also uses the descriptor to retrieve a node’s children as an ICollectionView of child nodes. The key methods in question here are:
[code lang=”ActionScript”]
public interface ITreeDataDescriptor
{
function getChildren(node, model):ICollectionView;
function hasChildren(node, model):Boolean;
function isBranch(node, model):Boolean;
// other methods omitted..
}
[/code]
We started with a data descriptor whose isBranch() method always returned true (since you always have to be able expand a node to find out what’s inside), whose hasChildren() method always returned true (for the same reason), and whose getChildren() method returned an empty child collection for an unexpanded node. Upon expanding a node, we would read that node’s child data over the net, and have that formerly-empty child collection would fire CollectionEvents with kind == ADD for all new children that were added to it.
This didn’t work: the Tree threw a runtime error as soon as we opened a node, because Flex assumed that any node with hasChildren() == true would have at least one child. On the other hand, if we tried returning hasChildren() == false from an unexpanded node to avoid this error, the Tree never showed the new children because it wouldn’t listen for ADD events on a node with no kids.
It turned out that Adobe hadn’t really thought through the contract between the descriptor and the Tree in this case. To their credit, they nailed the behavior down and fixed the Tree so that it would work with zero-length child collections. So…
Rule 1: If you have an empty node that might acquire children later, return hasChildren() == true and return an empty ICollectionView from getChildren() that fires CollectionEvents to signal the children’s arrival.
Another obscure Tree problem had also reared its head during our work: after expanding a bunch of nodes, our app would get slower and slower exponentially, eventually grinding to a halt. I used the debugger to pause the code and analyze what was going on. At that time, our nodes’ child collections were instances of IList and we would wrap them in a ListCollectionView to return them to the tree from our descriptor’s getChildren() method. Because we returned different wrapper objects every time this method gets called on a node, the Tree was adding event listeners to these multiple wrappers. Each time those event listeners fired, the handler methods would call getChildren() during their work, manufacturing even more wrappers and adding even more listeners… hence the exponential slowdown. So, another rule:
Rule 2: Always return the same identical object from a call to getChildren for a given value of the node argument. This might require some sort of weak-keyed Dictionary scheme, depending on how your data structures are set up.
Anyway, once Adobe fixed the descriptor contract, we started experiencing all kinds of issues with corrupt display in the Tree whenever remote data arrived while actively scrolling up and down. It seemed that the Flex team hadn’t considered this possibility thoroughly — the internal bookkeeping of the Tree simply didn’t account correctly for new data arriving on the fly. Another last-minute flurry of activity at Adobe fixed this problem.
At that point, we still thought there were more problems, but it turned out to be our bug: we were firing an extra ADD CollectionEvent sometimes, which threw off the Tree display. With that in mind…
Debugging Note: if you fire garbage change events into a Tree, you’ll get a garbage display out of the Tree.
One final issue: our big lazy data structure isn’t really a tree, but a graph. This means that one can find one’s way to the same node in the graph via multiple paths from the “root object”. Well, if you return the same node object in two different child collections returned from calling getChildren() on two different nodes, that messes the Tree’s bookkeeping up royally: it maintains internal dictionaries based on node object identity, and it assumes that a given node object is displayed in exactly one place in the tree. So:
Rule 3: Never return the same child node object within different collections returned by getChildren() for different parent nodes.
I remain a bit surprised that Adobe didn’t consider some of the angles that we were exercising while these controls were under development. I’m hoping that these oversights aren’t part of a larger pattern, but realistically, a platform as complex as Flex 2.0 is just going to need a lot of exposure and real-world hammering to bring out all the issues. The good news is that Adobe seems really committed to constantly improving the product and getting updates out in a timely fashion (as Macromedia failed to do with Flex 1.5). And the release of the framework source with the product makes it far easier to diagnose problems and create test cases.
12 Comments »
RSS feed for comments on this post. TrackBack URI
Leave a comment
Entries and comments feeds.
Valid XHTML and CSS.
All content copyright (c) 2006-2007 Joseph Berkovitz. All Rights Reserved.
Hi Joe,
Many thanks for this post. This days I was trying to build a tree similar to the one you explain here (but mine is not a graph) and getting remote data to build branches on the fly. It was very hard, and assume that I must wait for next release of the framework to get it work. Maybe with a lot of workarounds I could make work with the actual framework, but that works seems to be a complete lost since we will have a new one in a few days.
Is great to know that people at ADobe is trying to fix all the issues people are finding, but although the framework is free, the updates are very slow. Maybe they fix all this issues a few days ago, but we have to wait for next release.
When Flex 2.0 SDK final release ship, we could expect to have fixings to all the bugs we’ll find?
Why don’t put the framework under a SVN so all their users get lastest changes and workarounds to all the bugs I’m sure we’ll find?
As Flex Framework will not be open source, only Adobe people could control the source code, but we all could get the last changes as soon as it will be commited.
makes sense all this?
Thank again for this great post : )
Comment by Carlos Rovira — June 21, 2006 #
I don’t work at Adobe, so I can’t speak for them. They’ve done a lot of great last-minute work, but I would not expect Adobe to fix everything that has been found to date when 2.0 comes out, because of the effort involved and the need to stabilize the code.
The updates to the Beta program, while hardly frequent, have been reasonably often for a typical beta. I have been lucky to be participating in a private beta-within-a-beta program to review candidate releases, which have been coming out every few days lately. I wouldn’t bother getting the tree to work with Beta3, but would wait for the final release. Hopefully that’ll be soon :)
It would be a very unusual move for a large software company to make their repository available, and would expose Flex developers to all kinds of difficulties since brand new code often changes rapidly and unpredictably as it evolves.
What I would like to see, and think is realistic, is for Adobe to keep a stream of qualified, fully tested framework updates coming on a regular and frequent basis — say every 2-3 months.
Comment by joe — June 22, 2006 #
Hi Joe,
I post about the SVN idea in flexcoders and Matt Chotin responded that they were thinking in do something like that, but they want to think a little about how to manage the support.
I think that Adobe will surprise again to their developers if they show such innovative way to do their support and I don’t think it will be bad for them, it should be very positive and make people trust them and go the flex way.
I’m afraid that the conservative way (give full updates from time to time) will be very bad, since people stuck and frustrade will drop flex and go other ways they know better and have less surprises.
Comment by Carlos Rovira — June 22, 2006 #
Hi Joe,
thanks for this post! I fully understand the theory behind that, but I don’t really get it how for example rule 1 could be translated to code. That what you described is in my problem exactly the case. isBranch returns true and hasChildren returns false. How do I force the ITreeDataDescriptor to return true on hasChildren? Do I have to write my own custom datadescriptor? Sorry for that dumb question but I really don’t get it.. Maybe you can post a little code snippet?
Thanks in advance!
Comment by Marcel Fahle — August 23, 2006 #
Joe, you made my day!! The words “… to signal the children’s arrival” gave me the right direction to solve my tree problem.
My problem was this:
I built a “lazy tree” myself, but everytime I added new Data to the DataProvider (XMLListCollection), the nodes in my tree got totally messed when scrolling around.
So, after I read your post I tried to prepare each branchNode for the later arrival of new childNodes.
What I did was to add on every branch node a single empty node with a label “…loading”. And when the user clicks on the node to open it, the lonely loading-node gets replaced by the new real-nodes coming as xml from my streaming server. I don’t know if it is the most elegant way to do this, but it works like a charm! thanks for giving me the right direction! :)
Comment by Marcel Fahle — August 23, 2006 #
Glad that worked for you, Marcel. I think you understood the approach.
You are using XMLListCollection, and we are using an ArrayCollection (or something like it). Perhaps that explains why you need your “lonely node” for the tree to work correctly. We didn’t seem to need such a node.
There are still some bugs in Tree that can occur when scrolling around as data is added. Overall it is still in a delicate condition and I don’t think this control is completely solid yet.
Comment by joe — August 29, 2006 #
Many thanks to Marcel Fahle for the dummy “…loading” node technique - this did the trick for me.
I did have to add a call to invalidateList() to get the expanded branch to refresh the display, revealing the newly added node(s):
Tree(event.target).dataDescriptor.addChildAt(selectedResultNode,{label:”item1 child”},0);
Tree(event.target).invalidateList();
Comment by Benjamin Kurth — September 15, 2006 #
Hello all,
I am trying to create a dinamic tree, but i just can get it to work. Can someone please post the code here, not everything but just the way i can handle results and insert them to my tree component. Btw, i got it to work with addChildAt, but it works if my result only has one XML row like “”.
thx in advanced
Comment by Ozren — October 18, 2006 #
Hey Joe …
First off, thanks for posting info on this. I have been trying for 2 days create a tree that loads on demand. I am really struggling. I would really appreciate it if you would post your ITreeDataDescriptor Class. I am going to post mine below. The first time I open a dynamic branch it works perfectly. The 2nd through N times all open branches show the data retrieved on the last call.. make sence? Thanks for the time and attention!! I hope to return the favor one day!
public function getChildren(node:Object, model:Object=null):ICollectionView
{
if (node is EnvironmentVO)
return ICollectionView(new ArrayCollection(EnvironmentVO(node).circulars));
if (node is CircularVO)
return ICollectionView(new ArrayCollection(CircularVO(node).pages));
if (node is PageVO)
return ICollectionView(new ArrayCollection(PageVO(node).items));
return null;
}
public function hasChildren(node:Object, model:Object=null):Boolean
{
if (node is EnvironmentVO) return true;
if (node is CircularVO)
{
if (CircularVO(node).pages.length==0){
CircularVO(node).pages.push(new PageVO({name:’loading’}));
var cmd:CmdEvent = new CmdEvent(Controller.EVENT_DataLoadPageList);
cmd.data = CircularVO(node);
Controller.getInstance().executeCommand(cmd);
return false;
}else if (CircularVO(node).pages.length==1){
if (PageVO(CircularVO(node).pages[0]).name==”loading”)
return false;
}
return true;
}
if (node is PageVO) return true; // Got To Get The Top Level Working First!!
if (node is ItemVO) return false;
return false;
}
public function isBranch(node:Object, model:Object=null):Boolean
{
if (node is EnvironmentVO) return true;
if (node is CircularVO) return true;
if (node is PageVO) return true;
if (node is ItemVO) return false;
return false;
}
Comment by Michael — November 1, 2006 #
Michael, I can’t post our entire ITreeDataDescriptor class source because it has content that’s proprietary to Allurent. I wrote this article in order to convey the exact sense of what we were doing, without actually sharing the code.
One problem I see off the bat is that your code doesn’t apply “Rule 2: Always return the same identical object from a call to getChildren() for a given value of the node argument.” Because you’re calling new ArrayCollection() every time, this would tend to throw off the bookkeeping inside Tree.
It would be easier for you if the collections inside your nodes (.circulars, .pages, etc.) were already instances of ArrayCollection. Then you wouldn’t have to wrap them in anything.
You weren’t specific about what “previously loaded” data meant, but the other thing I’d do is make absolutely sure that the events that fire when your loading operation completes are passing back the correct, newly loaded data set. There might be a problem in there somewhere. That’s beyond the scope of what I could help with though.
Comment by joe — November 1, 2006 #
Thanks for you help. I took your advice and now it is working great. I made my collections inherit from ArrayCollections (rather than Array) and returned them directly. As I understand it the ArrayCollections are basically wrappers around Arrays that are given the additional responsibility of broadcating events when the underlying array is altered. The tree must be making use of these events for its internal bookkeeping. Thanks again!
I owe you one!
Comment by Michael — November 8, 2006 #
A good and clear post on dynamic tree loading. Though I see this has been written almost a year ago, I don’t find any official documentation from Adobe on dynamic tree loading even today. This gives me a feel that though all the videos and tutorials make flex development appear like a childs play, it will be sometime before we can build truly enterprise class applications using Flex.
Comment by SIyer — June 13, 2007 #