Extending Flex 3 Components

If you’ve more than dabbled with Flex I’m sure you’ve come to a point where your excitement for all the whiz-bang awesomeness you can do so easily is replaced by dread when you realize a component is doing about 90% of what you want. Sometimes 90% just aint good enough though, right? Exactly, that’s why I figured I’d write a little tutorial on how to extend a Flex component based on real-life problem I had with the Flex 3.x Tree control. So without further ado…

The Flex 3 tree control has an issue with icons. The issue is you cannot define icons via the XML dataprovider for branch nodes (a branch is defined as non-leaf node). That sentence is a bit dense so let me explain with an example.

The following xml code is the dataprovider of a Flex tree:

<menu>
  <node label="Home" icon="appIcon"/>
  <node label="Channels" icon="channelsIcon">
    <channel label="Vimeo HD Channel" thumb="http://images.vimeo.com/channelbadge-113006734.jpg"/>
    <channel label="Vimeo Staff Picks" thumb="http://images.vimeo.com/channelbadge-112957409.jpg"/>
  </node>
</menu>

The flex team was nice enough to create an iconField attribute, which allows you to define an icon for any node in your tree. In our case we’ll use “@icon” like so:

  <mx:Tree dataProvider="myXMLListCollection" iconField="@icon"/>

You’d expect the icon attribute to work on every node right? Wrong:

The second node (label=”Channels”) renders with the default icon instead of the defined channelsIcon (this is defined as an embedded image which is not shown).

I’m actually not sure if it’s an issue, a design flaw, or if there’s another way to do what I want that I’m just not getting. Either way, I did the first thing I always do when a Flex component doesn’t behave the way I want… open the hood and start hacking.

If you’ve never taken a look at the source of Flex component it’s actually pretty easy. Just navigate to whichever Flex SDK your using and you’ll find Tree.as in [your-flex-directory]/frameworks/project/framework/src/mx/controls/Tree.as. The easiest way to get to this file if you’re using Flex Builder is to cmd+click (ctrl-click on windows I believe) the component in mxml or actionscript. If everythings linked correctly Tree.as

So now you’ve got the source, what the heck do you do with this 3452 line monster? Well, this changes a bit each time, but in our case with the Tree component I decided to just look through the methods of Tree and see if i could find a promising one. Low and behold on line 1169 there’s a public method named itemToIcon. So noq f we track back a bit we can see that itemToIcon is used in initListData (line 2659). Specifically we see that on line 2666 we have this:

treeListData.icon = itemToIcon(item);

Bingo! It look like itemToIcon might just be what we’re looking for. Unfortunately, there’s no documentation for this method, but if we just take a second to think about the method name, it’s inputs and outputs, we can quickly see that itemToIcon converts an item object into an icon class. Scanning a little further down we can start to see series of if / else if / else statements that define the priority in which an icon is returned:

if (iconClass)
{
    return iconClass;
}
else if (iconFunction != null)
{
    return iconFunction(item)
}
else if (branch)
{
    return getStyle(open ? "folderOpenIcon" : "folderClosedIcon");
}
else
//let's check the item itself
{
    if (item is XML)
    {
        try
        {
            if (item[iconField].length() != 0)
               icon = String(item[iconField]);
        }
        catch(e:Error)
        {
        }
    }
    else if (item is Object)
    {
        try
        {
            if (iconField && item[iconField])
                icon = item[iconField];
            else if (item.icon)
                icon = item.icon;
        }
        catch(e:Error)
        {
        }
    }
}

Basically, itemToIcon prioritizes what icon the item will use by the following conditionals:

  1. does iconClass exist? (this has to do with the itemIcons parameter existing)
  2. is an IconFunction defined?
  3. is it a branch?
  4. does iconField exist?

It seem like case 1 should be able to handle custom branch icons, but I could not figure out how to get that to work with XML (any enlightenment on this would be appreciated). It was easy enough to figure out the logic and rearrange it though.

If you take a look at the priority of operations you should see the problem at case 3. If it finds a node that’s a branch it uses the default folder icon (either folderOpenIcon or folderClosedIcon). If you remember our example tree this is exactly our problem. Rather than the tree item rendering the default icon renders. Now if we looks at the 4th case, we see that the logic is there to use the iconField (@icon in our case), but it never gets to it on branch nodes! It just uses the default icons because of case 3.

Luckily we should be able to just create a new component that extends the Tree component and override the itemToIcon method with our new logic. Essentially all we have to do is take case 4, and make it the top priority, like so:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
public class IconTree extends Tree
{
override public function itemToIcon(item:Object):Class
{
    if (item == null)
    {
        return null;
    }

    var icon:*;
    var open:Boolean = isItemOpen(item);
    var branch:Boolean = isBranch(item);
    var uid:String = itemToUID(item);

    //first lets check the component
    var iconClass:Class =
            itemIcons && itemIcons[uid] ?
            itemIcons[uid][open ? "iconID2" : "iconID"] :
            null;
   
    // put precident of object and xml over
    if (item is XML){
      try{
         if(item[iconField].length() != 0){
           icon = String(item[iconField]);
          }
      } catch(e:Error) {}
    }else if (item is Object){
      try{
        if(iconField && item[iconField]){
          icon = item[iconField];
        }
        else if(item.icon){
          icon = item.icon;
        }
      }  catch(e:Error) {}
    }
   
    if(icon == null){
      if (iconClass){
        return iconClass;
      }else if (iconFunction != null){
        return iconFunction(item)
      }else if (branch){
        return getStyle(open ? "folderOpenIcon" : "folderClosedIcon");
      }
      else { /* xml object check used to bed here */ }
    }
   

    //set default leaf icon if nothing else was found
    if (icon == null)
      icon = getStyle("defaultLeafIcon");

    //convert to the correct type and class
    if (icon is Class){
        return icon;
    }
    else if (icon is String){
        iconClass = Class(systemManager.getDefinitionByName(String(icon)));
        if (iconClass)
            return iconClass;

        return document[icon];
    }
    else{
        return Class(icon);
    }

}
}

If you look at line 21 you’ll see that now we’re putting the logic to use the iconField as priority 1. All should be gravy right? Not so fast…

Attempted access of inaccessible property isBranch through a reference with static type cloudtv.components:IconTree

Unfortunately, isBranch (used on line 12) is private function in Tree, therefore we cannot use it in our custom component. We could if isBranch was protected (or public). In fact I don’t really see a reason for any method to ever be private, but dont get me started on that…

Anyway, there’s any easy, although not so elegant solution to this problem: copy & paste. If we just copy the method isBranch to our new component everything works just fine and we have exactly what we want by using our custom component (I call it IconTree). Take a look see:

That wasn’t that hard was it? Well it was kind of a pain, but at least we got the result we wanted =)

In case you find this particular case useful here is link to the entire componenent: IconTree.as

  • NotAGuru

    Why not just assign a class that extends the TreeItemRenderer as the itemRenderer of the Tree?

  • http://mixwit.com Michael Christoff

    I haven't had a chance to try that, but it seems like it could work. I'm not quite sure if the itemRenderer can effect change on the icon though. Have you given this a try to see if you can get the same result?

    Either way, I think fundamentally the default behavior of the Tree should behave like IconTree. I've ended up having to this kind of thing to various different flex components as well so I hope it serves as a good tutorial for anyone who's just getting their feat wet digging into the innards of the flex framework.

  • http://mixwit.com Michael Christoff

    I haven't had a chance to try that, but it seems like it could work. I'm not quite sure if the itemRenderer can effect change on the icon though. Have you given this a try to see if you can get the same result?

    Either way, I think fundamentally the default behavior of the Tree should behave like IconTree. I've ended up having to this kind of thing to various different flex components as well so I hope it serves as a good tutorial for anyone who's just getting their feat wet digging into the innards of the flex framework.