Flex 2.0: Reopening a Tree after updating the dataProvider

So, I’m completely stoked! I’ve figured out something that I think might actually be useful to the world in Flex! If you’ve worked with trees before, they’re fun except when you have to update their dataProvider. Then it’s not so fun because it collapses the tree and does not reopen it. Well, my friends, now it will. Check out this lil snippet of code. Copy it, paste it into FlexBuilder and press “Run”.

The two key elements of the tree are the openItems property and the render event. When used in conjuction with a boolean flag, you’re all set for keeping your tree open. Enjoy!

To see how trees normally act when changing dataProviders, delete the render event from the tree tag.

<?xml version=”1.0″ encoding=”utf-8″?>
<mx:Application xmlns:mx=”http://www.adobe.com/2006/mxml&#8221; layout=”vertical” creationComplete=”initFunc()”>
<mx:Script>
<![CDATA[
[Bindable]
public var tempOpen:Object = new Object();
[Bindable]
public var refreshSO:Boolean = false;

[Bindable]
public var secondObject:Object = new Object();
[Bindable]
public var tempObject:Object = new Object();
[Bindable]
public var tempObject1:Object = new Object();
[Bindable]
public var tempObject2:Object = new Object();
[Bindable]
public var currentDP:String = “tempObject”;

private function initFunc():void
{

tempObject = new Object();
tempObject.path = “somepathvalue”+2;
tempObject.label = “Node”+2;
tempObject.componentType = “Type”+2;
tempObject.children = new Array();
tempObject1.label = “test3”;
tempObject1.children = new Array();
tempObject2.label = “test4”;
tempObject1.children[0] = tempObject2;
tempObject.children[0] = tempObject1;
secondObject = tempObject;

}

public function switchDP():void
{
tempOpen = testTree.openItems;
refreshSO = true;
if (currentDP == “tempObject”)
{
currentDP = “secondObject”;
testTree.dataProvider = secondObject;
}
else
{
currentDP = “tempObject”;
testTree.dataProvider = tempObject;
}
}

public function renderFunc():void
{
if (refreshSO)
{
refreshSO = false;
testTree.openItems = tempOpen;
}
}
]]>
</mx:Script>
<mx:Tree id=”testTree” render=”renderFunc()” width=”250″ dataProvider=”{tempObject}” labelField=”label” />
<mx:Button label=”Switch DP” click=”switchDP()”/>
</mx:Application>

21 thoughts on “Flex 2.0: Reopening a Tree after updating the dataProvider

  1. Good on figuring this all out, now even better:

    If you haven’t noticed, it jerks on the update request. BUT, if you add testTree.invalidateList() before you update openItems and then, after you update openItems add testTree.validateNow();, the jerk is gone. So, your renderFunc would look like:

    public function renderFunc():void{
    if(refreshSO){
    testTree.invalidateList();
    refreshSO = false;
    testTree.openItems = tempOpen;
    testTree.validateNow();
    }
    }

    Then, you’re jerk free!

    – Taka

  2. Does not work. I have a remoteObject connecting to a database that gets the updated data to the dataprovider. On refresh of the remoteObject method that reinits the dataprovider, the tree just closes, nothing else. I put the switcDP stuff in the remoteObject result function, the last function or action to run before the tree redraws, nothing.

    Figure out how to make it work with remoteObjects and I’ll call you a Flex developer. For now, your just lucky.

  3. Tom this seems to only work for non XML dataproviders.. I have tested this with XMLListCollection as a DP on the tree and this technique does not work for that.. any pointers?

  4. My mistake.. it does work for XMLListCollection also.. I was makiing a copy of the DP and as it compare the uuid in the tree class for the open items it was not finding the same and hence not opening the tree at the correct nodes…

  5. Try this

    // TestTreeApp.mxml

    Lvl2Branch->Lvl3Leaf )
    */
    private function findItem(item:TreeNode, startIndex:Number):Object{

    for( ; startIndex

    package
    {
    import mx.collections.ArrayCollection;

    [Bindable]
    public class TreeNode
    {
    public static const LVL1_TYPE:int = 0;
    public static const LVL2_TYPE:int = 1;
    public static const LVL3_TYPE:int = 2;

    // These two elements are primary keys ie, no two
    // TreeNodes will have the same uniqueID for a given type
    //
    public var uniqueId:int;
    public var type:int;

    // Tree Label and children elements
    //
    public var label:String;
    public var children:ArrayCollection;

    public function equals(node2:TreeNode):Boolean{
    return (this.uniqueId == node2.uniqueId && this.type == node2.type);
    }
    }
    }

  6. txt2html

    // TestTreeApp.mxml
    <?xml version=”1.0″ encoding=”utf-8″?>
    <mx:Application

    xmlns:mx=”http://www.adobe.com/2006/mxml
    layout=”vertical”>
    <mx:Script>

    <![CDATA[
    import mx.events.FlexEvent;
    import mx.controls.listClasses.ListBase;
    import mx.controls.List;
    import mx.collections.ArrayCollection;

    // Change these to increase/decrease the number of elements
    // per Tree Node
    //
    private const LVL1_COUNT:int = 7;
    private const LVL2_COUNT:int = 2;
    private const LVL3_COUNT:int = 50;
    [Bindable]
    private var selectedIndexStore:int;

    [Bindable]
    private var selectedItemStore:TreeNode;

    [Bindable]
    private var openItemsStore:Array;

    [Bindable]
    private var refreshData:Boolean = false;

    [Bindable]
    private var provider:ArrayCollection = createTreeData();

    [Bindable]
    private var debugTxt:String = new String();

    private function debug(msg:String):void{
    debugTxt += msg + “\n”;
    }

    /*
    Create a nested ‘complex’ object that will be used as Tree Nodes
    */
    private function createTreeData():ArrayCollection{
    var root:ArrayCollection = new ArrayCollection();

    var currLvl1Node:TreeNode;
    var currLvl2Node:TreeNode;
    var currLvl3Node:TreeNode;

    var lvl1UniqueId:int = 0;
    var lvl2UniqueId:int = 0;
    var lvl3UniqueId:int = 0;
    for(var ii:int = 0; ii < LVL1_COUNT; ii++ ){

    currLvl1Node = new TreeNode();
    currLvl1Node.uniqueId = lvl1UniqueId++;
    currLvl1Node.type = TreeNode.LVL1_TYPE;
    currLvl1Node.label = new String(ii+1);
    currLvl1Node.children = new ArrayCollection();
    root.addItem(currLvl1Node);

    for( var jj:int = 0; jj < LVL2_COUNT; jj++ ){

    currLvl2Node = new TreeNode();
    currLvl2Node.uniqueId = lvl2UniqueId++;
    currLvl2Node.type = TreeNode.LVL2_TYPE;
    currLvl2Node.label = currLvl1Node.label + new String(jj+1);
    currLvl2Node.children = new ArrayCollection();

    currLvl1Node.children.addItem(currLvl2Node);

    for( var kk:int = 0; kk < LVL3_COUNT; kk++ ){

    currLvl3Node = new TreeNode();

    currLvl3Node.uniqueId = lvl3UniqueId++;
    currLvl3Node.type = TreeNode.LVL3_TYPE;
    currLvl3Node.label = currLvl2Node.label + new String(kk+1);
    currLvl3Node.children = null;

    currLvl2Node.children.addItem(currLvl3Node);
    }
    }
    }

    return root;
    }
    /*
    This is a simplistic search example used to find an object in my
    new dataProvider that matches the previous object in my openItemsStore.
    Since the object references changed I must do this.
    This search method only works for a Tree with 3 levels:
    ( RootBranch->Lvl2Branch->Lvl3Leaf )
    */
    private function findItem(item:TreeNode, startIndex:Number):Object{
    for( ; startIndex < provider.length; startIndex++ ){
    var currLvl1Node:TreeNode = provider.getItemAt(startIndex) as TreeNode;
    if( currLvl1Node.equals(item) ){
    debug(“Found ” + currLvl1Node.label);
    return currLvl1Node;
    }else if( item.type == currLvl1Node.type ){
    if( item.uniqueId < currLvl1Node.uniqueId ){ // Assuming they are sorted by id
    debug(“Break – Item must have been deleted.”);
    break;
    }else{
    debug(“continue”);
    continue;
    }
    }
    for( var jj:int = 0; jj < currLvl1Node.children.length; jj++ ){
    var currLvl2Node:TreeNode = currLvl1Node.children.getItemAt(jj) as TreeNode;
    if( currLvl2Node.equals(item) ){
    debug(“Found ” + currLvl2Node.label);
    return currLvl2Node;
    }
    }
    }

    return null;
    }
    /*
    Returns the Tree to its previous state
    */
    private function assignTreeStore():void{
    debug(“Assigning Tree Store”);
    SampleTree.invalidateList();

    // Expand the Tree to the previous state
    //
    debug(“Assigning openItems”);

    var startIndex:Number = 0;

    for( var ii:int = 0; ii < openItemsStore.length; ii++ ){
    // Find the ‘matching’ object.
    // NOTE: References will be different in case of Remote Call,
    // so use ‘TreeNode.equals’ to compare equality.
    // NOTE: This is simple search version … obviously can be improved
    //
    var item:Object = findItem(openItemsStore[ii] as TreeNode, startIndex);

    if( item ){
    SampleTree.expandItem(item,true);
    }
    }

    // Following the expansion, set the previous selectedIndex
    // and adjust the verticalScrollPosition
    //
    SampleTree.selectedIndex = selectedIndexStore;
    selectedIndexStore = SampleTree.selectedIndex;
    selectedItemStore = SampleTree.selectedItem as TreeNode;
    SampleTree.verticalScrollPosition = Math.max( selectedIndexStore – 3, 0 );
    SampleTree.validateNow();
    }
    public function renderTree():void{
    if(refreshData){
    refreshData = false;
    assignTreeStore();
    }
    }

    /*
    Label function for the Tree
    */
    private function getTreeNodeLabel(item:Object):String{
    return (item as TreeNode).label;
    }

    /*
    Whenever the user selects an item, store it so we can return to this state
    once the tree is refreshed
    */
    private function itemClickHandler(event:Event):void{
    debug( ‘Selected Item Changed: ‘ + SampleTree.selectedItem.label );
    selectedItemStore = SampleTree.selectedItem as TreeNode;
    selectedIndexStore = SampleTree.selectedIndex;
    debug( ‘Selected Index: ‘ + selectedIndexStore );
    // perform some logic now that the selectedIndex and selectedItem are correct
    //
    }
    ]]>

    </mx:Script>

    <mx:Label text=”Current Selected Item: “/>

    <mx:Label text=”{SampleTree.selectedItem.label}”/>

    <mx:Tree id=”SampleTree”

    render=”renderTree()”
    width=”250″
    height=”150″
    dataProvider=”{provider}”
    labelFunction=”getTreeNodeLabel”
    showRoot=”false”
    itemClick=”itemClickHandler(event)”/>
    <mx:Button

    label=”Simulate Remote Call”
    click=”{
    /*
    Store the openItems before refreshing
    */
    debug( ‘\nSimulating Remote Call’ );
    openItemsStore=SampleTree.openItems as Array;
    refreshData = true;
    provider=createTreeData();
    }”/>
    <mx:Button

    label=”Signal Refresh of Data Provider”
    click=”{
    /*
    Store the openItems before refreshing
    */
    debug( ‘\nSimulating dp refresh’);
    openItemsStore=SampleTree.openItems as Array;
    refreshData = true;
    provider=provider;
    }”/>

    <mx:TextArea id=”debugTA” text=”{debugTxt}” height=”100%” width=”100%” liveScrolling=”true” />

    <mx:Button label=”Clear Text Area” click=”debugTA.text=””/>

    </mx:Application>

    // TreeNode.as
    package
    {
    import mx.collections.ArrayCollection;

    [Bindable]
    public class TreeNode
    {
    public static const LVL1_TYPE:int = 0;
    public static const LVL2_TYPE:int = 1;
    public static const LVL3_TYPE:int = 2;

    // These two elements are primary keys ie, no two
    // TreeNodes will have the same uniqueID for a given type
    //
    public var uniqueId:int;
    public var type:int;

    // Tree Label and children elements
    //
    public var label:String;
    public var children:ArrayCollection;

    public function equals(node2:TreeNode):Boolean{
    return (this.uniqueId == node2.uniqueId && this.type == node2.type);
    }
    }

    }

  7. It “Works” with XML data, but my issue is that it doesn’t keep open folders according to their XML properties, rather, their indexes within the Tree object itself. In my application, I’m moving around nodes, then hoping that I can keep the same folders open that were previously.

    My workaround is to go through the tmpOpen array and manually open each folder or something to that effect.

  8. VM, I have the same problem as you, Flex don’t find the same UUID for the XML branch i would like to open. How did you do the make this appened.

  9. I made it like this :

    After I modified the DP I call my function expandNode :

    My dataprovider is a xml file and each node has an unique attribute “id”…

    call :
    […]
    updating DP
    expandNode(nodeToShow.@id, DP);
    tree.scrollToIndex (index+1);
    tree.selectedIndex = index + 1 + count;
    […]

    public function expandNode(id:int, xmllist:XML):void
    {
    var childlist:XMLList = xmllist.children();

    for each (var item:XML in xmllist)
    {
    if (id == item.@id)
    {
    while (item.parent())
    {
    tree.expandItem(item.parent(), true, true);
    item = item.parent();
    }
    }

    for each (var child:XML in childlist)
    {
    expandNode(id,child);
    }
    }
    }

  10. I try to refresh and expand my tree after getting dataprovider through a webservice call and it’s not working…
    It tried the recursive method in debug mode and i see that the correct nodes are supposed to be expanded but at the end, the tree is totally collapsed.
    Any idea?

  11. Hi Tom,

    Great post.

    For a Project with BlazeDS I had to work with updating and reloading Tree View Data without breaking the User Experience (all nodes closed when data reloaded).
    Instead of keeping tabs on “which node was opened before?” and “what was the scroll-position?” I found a way to inject the new state of the Tree View data into the existing data provider.

    See article here if you are interested.
    BlazeDS and Smooth Data Injection – Reloading the Tree View Data Provider without breaking the User Experience – http://bit.ly/92h7on

    Hope this is of help as well.

  12. Hmm, doesn’t secondObject=tempObject in initFunc mean that seconObject is just a reference of tempObject and you are switching a same object?
    If you create 2 separate objects as:

    firstObject = {label:”Node1-1″,children:[{label:”Node1-2″,children:[{label:”Node1-3″}]}]};
    secondObject = {label:”Node2-1″,children:[{label:”Node2-2″,children:[{label:”Node2-3″}]}]};

    whole thing will break 🙂

Leave a Reply to lordbron Cancel reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s