Saturday, March 20, 2010

Flex Tree control bug: selectedItem loses value and nothing is shown highlighted

One of the project I am working on uses a Tree control. Clicking on an item in the tree control selects that item and shows it highlighted but not in the application I was working on. Read about my 18 hour journey to fix this problem...

Clicking on a tree item would select the item but the selection would be lost in a few milliseconds.
A Tree extends List which extends ListBase and between them, there are about 15000 lines of code. Everything looked good, breakpoints did not reveal anything interesting. FlexSpy showed that the tree.selectedItem was null and it shouldn't have been because an item was selected by clicking on it. Breakpoint in the ListBase.selectedItem showed nothing unusual but soon I learnt that the ListBase class sets the private variable _selectedItem directly and that too in about 20+ places. Breakpoints in these 20+ places did not reveal anything and in debug mode I could not reproduce the bug. One of my colleague terms it as "Heisen Bug". A HeisenBug is a bug whose presence is affected by the act of observing it.

You cannot do much with the Flex framework code other than set breakpoints. You cannot add trace statements etc. So I decided to create my own Flex framework project and add it in the library path of my application project. I will write more about this in a different blog. I also have a script that adds trace statements in all the methods of all the classes inside a folder and its sub-folder(s). This script is also coming soon on one of my blog. The Tree/ List/ ListBase methods were modified to add the trace statements using the script I mentioned earlier. The traces were very helpful because in every method call, I would trace the selectedItem. I narrowed down the method where the selectedItem was an object and then suddenly became null. What was really happening?

The trace statements helped me narrow the problem down to a method called "collectionChangeHandler" which was being called because the dataProvider was changing. I was confident that the application was not changing the dataProvider. Then who was and why? The Tree.commitProperties() was changing the dataProvider. I am reproducing part of Tree.commitProperties() code below ...

if (dataProviderChanged || showRootChanged || openItemsChanged)
var tmpCollection:ICollectionView;
//reset flags

dataProviderChanged = false;
showRootChanged = false;

//we always reset the open and selected items on a dataprovider assignment
if (!openItemsChanged)
_openItems = {};

// are we swallowing the root?
if (_rootModel && !_showRoot && _hasRoot)
var rootItem:* = _rootModel.createCursor().current;
if (rootItem != null &&
_dataDescriptor.isBranch(rootItem, _rootModel) &&
_dataDescriptor.hasChildren(rootItem, _rootModel))
// then get rootItem children
tmpCollection = getChildren(rootItem, _rootModel);

// at this point _rootModel may be null so we dont need to continue
if (_rootModel)
//wrap userdata in a TreeCollection and pass that collection to the List
super.dataProvider = wrappedCollection = (_dataDescriptor is ITreeDataDescriptor2) ?
ITreeDataDescriptor2(_dataDescriptor).getHierarchicalCollectionAdaptor(tmpCollection != null ? tmpCollection : _rootModel, itemToUID, _openItems) :
new HierarchicalCollectionView(tmpCollection != null ? tmpCollection : _rootModel, _dataDescriptor, itemToUID, _openItems);

The condition check "if (dataProviderChanged || showRootChanged || openItemsChanged)" on line 1 passed because the "openItemsChanged" variable was true. This creates a new collection and assigns it to the dataProvider. This flag is set to true if you have set the openItems property of the Tree. All dirty flags have to be set to false at some point BUT THIS ONE WAS NOT and in every Tree commitProperties() call, a new collection was created and assigned to dataProvider which in turn was setting the selectedItem to null. Searching flex bugs revealed that I was not the first guy to have come across this bug.
I had searched flex bugs but this bug did not show up in the search results because it didnt have the TREE keyword in the title.

Somebody else had a similar dataProvider problem but I am guessing he didnt get to the root of the problem.

My solution ...
- Extend the Tree class and create a new class MyTree.
- override the set openItems method and in the method open the items individually one by one using expandItem() method.
- The solution could depend on your needs. What I mentioned above works for me. You might be adding and dynamically removing items hence you might have to do something more then what I did.

The problem I faced was kind of intermittent. I also noticed a race condition which I will explain below ...
- User clicks an item. The clicked item is selected and shown highlighted.
- Tree.commitProperties is called which creates a new dataProvider (if you set the openItems property on the Tree) is created because openItemsChanged is always true.
- dataProvider is set which nulls the selectedItem and selectedIndex.
- You probably have a model where you are tracking the selectedItem.
- You also have a binding Tree.selectedItem = model.selectedItem because the Tree.selectedItem can change for reasons other than tree clicks.
- The binding fires and tries to assign the model.selectedItem to Tree.selectedItem.

There are race conditions happening here. If the model tries to set the selectedItem after the Tree.selectedItem becomes null, you will see something highlighted. Nothing will be shown highlighted if the model sets the selectedItem before the Tree.selectedItem becomes null. callLater can help in these circumstances. I did that and solved the problem but a change somewhere else in the application broke the callLater solution.

With this ended my 18 hour journey to fix a Flex bug that was reported to Adobe 5-6 months before I encountered it. With this ends my first blog too.

Labels: , , ,


At April 22, 2010 at 7:47 AM , Blogger said...

Thanks for this post, it helped me. I have confronted with exactly the same problem, but in my case It was easily reproducible in debug mode.

At August 10, 2010 at 12:26 PM , Anonymous daniel said...

My implementation is probably a little different, but I overcame this issue without too much trouble. Though its possible to use the tree control as a model, I find it much more reliable to use a data structure such as the one contributed by Polygonal

Implementing the interface ITreeDataDescriptor gives the tree control a way to interact with the data provider. Perhaps by using my own dataprovider, the tree control won't refresh the provider on its own? Once in awhile I needed to reassign the dataprovider myself. So I saved the open items and selectedItem via tree.openItems and tree.selectedItem properties. It was necessary to call tree.validateNow() just before trying to restore tree.selectedItem.



Post a Comment

Subscribe to Post Comments [Atom]

<< Home