Jan 7, 2009

A-Better-Flex-Tree


A Better Flex Tree

Intro

    The Tree implementation that ships with Adobe Flex (at least as of version 3) has some very large shortcomings if you are trying to do anything beyond the basic examples. I have found other implementations of the tree that are not built on the existing tree implementation, but I am hesitant to stray away from the standard tree because Adobe will continue to add new features and improvements. Also I would really just be trading one set of headaches for another depending on what is missing/broken in those implementation. So I’ve put together a fairly simple extension to the Adobe tree to address some of the major problems, and am posting it up in the hopes that it may save someone else the same headaches.

    Please feel free to make recommendations, as this code is not perfect, it is simply “good-enough” for my current needs. I will continue to revise this design as my current project continues to progress. But at this point it works and is simple enough for people to understand and make their own changes.



A Simple Demo



Example Source



Vocabulary

Real This tree implementation wraps the data you want in something called a WrapperTreeNode or “wrapper” for short. Any mentions of the “real” data is referring to the original data that is being wrapped.

Wrapper The thin object that wraps each data object such that it will work with this extension of the tree.
 

The Problems


Problem: Objects Can Only Occur Once in a Tree

    The fact that objects can only occur once in a tree is of no consequence to folks rendering XML node data, but for those who are trying to use a tree as a means to visualize object data in a database, this can be a real problem. It is fairly common in an application to have an entity exist in multiple locations. For example, consider the iTunes application. A user might have the same song appear in many different playlists or folders.

    To make matters worse, the default tree implementation doesn’t expressly fail when the same object occurs in multiple locations. It simply behaves very strangely. For example, selecting an object can result in selecting a different instance of the same object, and so on. This makes it a bug that isn’t always found right away during testing.


Figure 1 - When the same object occurs in a tree more than once, using the mouse to select that object will often select the wrong tree node representation.
 


Problem: The Tree Doesn’t Resize Upon Opening a Folder

    This one really bothers me because Adobe doesn’t classify this as a bug. If you open a folder that contains items with long labels, the tree simply clips the items without giving you a scroll bar. It isn’t until the tree measures itself again that the tree discovers that the contents are wider than the display and adds the scroll bar.


Figure 2 - Opening folders doesn't resize the tree despite content changed
 

Problem: Trees Don’t Support Lazy Loading

    For many distributed applications lazy loading is a must. There is often too much data to ship it all across the wire when it isn’t needed. For example, in my current application, I don’t want the contents of a folder in the tree to be retrieved until the user opens the folder. The user will notice a small delay the first time they open a folder, but it is a small price compared to having the server ship the contents of 10,000 folders to the client at startup time.

 

Design


Figure 3 - Class Diagram of this Tree Implementation
 

WrapperNodeTree

    The WrapperNodeTree is just an extension of the normal flex tree with two distinct additions. First, the tree contains code to resize when nodes are expanded. This code was taken from http://frishy.blogspot.com/2007/09/autoscrolling-for-flex-tree.html. Unfortunately while this code works pretty well, it has a flaw of sometimes allowing text to go outside of the tree. I will look into fixing that fairly soon.

    Second, this tree uses a special data descriptor, a WrapperNodeTreeDataDescriptor, to navigate through the WrapperTreeNodes. You will still be able to use your own customized data descriptor to describe your own object hierarchy, but you will add it in a different place as you will see shortly.


Capabilities


Adjust size when opening a folder

When you open and close a folder, the tree will resize and add or adjust the scrollbars as necessary.

ToDo’s

  • The users own data descriptor should be moved from the node to here.


WrapperNodeTreeDataDescriptor

    Since this tree wraps each piece of data in a wrapper, we need to have a data descriptor that says it should look for data in the wrapped object, not in the node itself.

 

IWrapperTreeNodeDataDescriptor

    You implement the IWrapperTreeNodeDataDescriptor to describe how the tree can understand your object data. For reasons that escape me right now, I thought the ITreeDataDescriptor was lacking methods for name and icon and so I added them.


public interface IWrapperNodeTreeDataDescriptor extends ITreeDataDescriptor
{
function getLabel(node:Object, model:Object=null):String;
function getIcon(node:Object, model:Object=null):Class;
}
Figure 4 - IWrapperNodeTreeDataDescriptor Interface
 

ToDo’s
  • Probably get rid of this interface. The tree really should use the label/icon property or function of the tree, and be the same as the normal tree.


WrapperTreeNode

    The WrapperTreeNode is a tree node that will wrap every one of the “real” objects you want to display in the tree. The reason we wrap each object is to overcome the problem that objects can only occur once in a tree. By wrapping each object we can guarantee that each object in the tree will be a unique object in memory and will avoid that problem.


Member Attributes of Interest


wrappedNodeData_               This is the real object that the node is wrapping, and the tree is meant to display.

wrappedDataDescriptor_      This is the data descriptor you want to use to describe how the tree should traverse your real object data. In other words, if you were migrating from the regular tree to this tutorial’s tree, and your data had a data descriptor, this is where you would set that same data descriptor. If none is set, it will use the standard DefaultDataDescriptor.

children_                                 This array collection is a collection of WrapperTreeNode objects that represent the children of this node. This variable will be initialized by iterating through the list of “real” children and wrapping each object in a WrapperTreeNode.


wrappedChildren_                 This is the collection of “real” children from your original data. We keep this in order to add a CollectionEvent.COLLECTION_CHANGE event listener, so every time the real children data changes, we can update the wrapped nodes.

Capabilities


Handling of Changes to Data

    The WrapperTreeNode has a few interesting capabilities worth noting. First, if the real data has children, the wrapper node will attach a listener to the real object’s children. The node can then rebuild the list of children wrapper nodes whenever the real children change. For example, if a real data object has 3 children, and something adds another child to the real data, the wrapper node will rebuild its list of children so that the tree is kept up to date.


privatefunction onChildrenChange(evt:CollectionEvent) : void
{
trace("onChildrenChange - Node: "+ this.label +" - "+evt.toString());
//TODO: This probably should not remove all and actually handle the various types of collection events (add, remove, etc.)
children_.disableAutoUpdate();
children_.removeAll();
var origChildren:ICollectionView =
wrappedDataDescriptor_.getChildren(wrappedNodeData_);
foreach (var wrapChild:Object in origChildren)
{
addWrappableObjectAsChildNode(wrapChild);
}
children_.enableAutoUpdate();
}
Figure 5 - Change handler for when the children of the "real" change

ToDo’s
  • Currently the node just completely rebuilds all the wrapper children which is somewhat inefficient. It would be more efficient to individually handle adds, removes, etc.


Handling of Lazy Loading

    The WrapperTreeNode also handles lazy loading of the node children. When the user tries to access a WrapperTreeNode’s children, the node will either return the list of wrapper nodes that exist, or if the node children have not yet been initialized, it will build a new list of node children by wrapping each of the “real” children. However, in the case of lazy loaded data, the “real” children will not yet exist and the data services provider will throw an ItemPendingError.

    Handling lazily loaded children is very simple because we already handle updating the tree when the children data changes. All we have to do is catch the ItemPendingError so no error is propagated up, and then rely on the change handling logic (described earlier) to handle the fact the real object now has children loaded from the network. In a nutshell the data service provider will plug in the lazily loaded data as it comes in, cause a collection change event, and our existing change logic will update the tree.

publicfunctionget children():ArrayCollection {
if(wrappedDataDescriptor_.hasChildren(wrappedNodeData_)) {
if(wrappedChildren_.length > 0) {
try {
if(!childrenInitialized_) {
foreach (var origChild:Object in wrappedChildren_) {
addWrappableObjectAsChildNode(origChild);
}
childrenInitialized_ = true;
}
}
catch(ex:ItemPendingError) {
trace("Item pending for children of: " + this.label);
if(children_.length == 0) {
//Add a node that says we are loading data.
var infoObject:Object = new Object();
infoObject.label = "Loading Data...";
addWrappableObjectAsChildNode(infoObject);
}
ex.addResponder(new ItemResponder(
function (result:Object, token:Object=null) : void
{
//Do nothing because the children change event handler will build the tree nodes.
},
function (fault:Object, token:Object=null) : void
{
//TODO: Log this error
trace('Error while loading children');
}
));
childrenInitialized_ = true;
}
}
return children_;
}
else
returnnull;
}
Figure 6 - How wrapper handles children and lazy loading
 

Summary

    So that is all there is to it. It’s not perfect, and I still have some TODOs left to make the design a little cleaner. But this works pretty well, and you have the benefit of still using the Adobe tree.

 

Any questions or comments?!  Post a comment or email me at plummeronsoftware at gmail dot com

15 comments:

chack said...

Hi Jeff,

this is great stuff...
I'm very new to adobe and I having big problems with the tree control. It would be great if you could post your wrapper classes, I'm going round in circles with this. Thanks

Anonymous said...

Very much a prompt reply :)

David Sissoko said...

Great job. I don't understand why Adobe did not yet release a patch for their tree component: we are november 2010 !!
anyway, thanks for this code

Unknown said...

Do you perhaps have an example that implements Lazy Loading? I am bumped up against that issue right now, and your Tree looks like it could solve it!
Cheers,
Fred

Anonymous said...

Helpful info. Fortunate me I discovered your site
accidentally, and I am surprised why this twist of fate did not happened in advance!

I bookmarked it.

My homepage :: Suggested Resource site

Hua Cai said...

ray-ban sunglasses
louis vuitton bags
michael kors outlet online
lululemon uk
louis vuitton outlet
swarovski outlet
louis vuitton outlet stores
michael kors factory outlet
burberry outlet
mulberry outlet
hermes outlet store
ray ban sunglasses
michael kors handbags
cheap oakley sunglasses
hermes outlet
air max 2015
fitflops outlet sale
louis vuitton handbags
ferragamo outlet
tiffany and co
nike tn pas cher
rolex uk
mulberry sale
replica watches
bottega veneta outlet online
cheap michael kors handbags
fitflops clearance
toms shoes
christian louboutin outlet
chaussure louboutin
celine outlet online
toms outlet
cheap jordan shoes
nike blazer pas cher
ferragamo outlet
20160528caihuali

xjd7410@gmail.com said...

discount jordans
air jordans
coach factory outlet
retro 11
louboutin shoes
ray ban sunglasses
kobe 8
rolex watches
giuseppe zanotti
oakley sunglasses
adidas running shoes
tiffany jewelry
adidas superstar trainers
gucci handbags
louis vuitton purses
lebron 12
louis vuitton outlet
burberry handbags
air jordan femme
timberland outlet
michael kors outlet online
michael kors outlet
nike store
coach outlet online
ray ban sunglasses
fit flops
coach factory outlet online
louis vuitton handbags
supra for sale
coach outlet online
chenyingying712

raybanoutlet001 said...

cheap basketball shoes
michael kors handbags wholesale
new balance shoes
cheap nfl jerseys
christian louboutin outlet
jimmy choo
jordan shoes
cheap jordans
salvatore ferragamo
cheap nfl jerseys wholesale

raybanoutlet001 said...

nike blazer
basketball shoes
ecco shoes outlet
yeezy boost 350 black
michael kors outlet online
chicago bulls jersey
michael kors handbags wholesale
adidas nmd runner
nhl jerseys
tiffany and co

dada24 Xu said...

louis vuitton pas cher
hermes bags
pandora charms sale clearance
cheap jordans for sale
longchamp handbags
uggs
moncler outlet online
canada goose
adidas nmd r1
kd shoes
zhi20161209

dong dong23 said...

gucci outlet
louis vuitton handbags
prada outlet
pandora uk
ugg outlet
timberland boots
ralph lauren outlet
celine outlet
nike air max pas cher
ralph lauren
201612.27wengdongdong

Meiqing Xu said...

moncler outlet
michael kors outlet
beats by dr dre
coach factory outlet
adidas shoes
longchamp outlet
gucci purses
omega replica watches
beats earbuds
the north face
20170209CAIYAN

John said...

longchamp handbags
gucci borse
mbt
adidas
red bottom
adidas yeezy boost
yeezy boost 350 v2
cheap nike sneakers
oakley vault
ralph lauren sale clearance uk
20170707yuanyuan

adidas nmd said...

ugg outlet
true religion outlet store
oakley sunglasses
kobe 9
michael kors handbags
mont blanc outlet
ugg outlet
nike blazer pas cher
ugg boots
michael kors handbags

aaa kitty20101122 said...

nmd
kobe shoes
michael kors handbags
nmd
adidas tubular
nike air zoom
fitflops
adidas ultra
prada sunglasses
longchamp bags