March 22, 2008

XUL Trees and Objects: ClassTreeView

I love XUL trees. I even smoke them from time to time. But what I don't like is trying to build a hierarchy of objects in them - even though that's probably the best use for them.

Imagine that you want to show this tree of objects, with properties of each object horizontally, and the objects themselves laid out vertically, indented and illustrated to show which objects have which parent objects. DOM Inspector does this with DOM nodes all the time. My chrome registry viewer code does something similar for files (file systems are tree-like), and when you want to see the properties of an object, JS object inspection is usually through a tree. Even Venkman uses trees to show you functions in a file or webpage.

Still, for every different object tree I've come across, there's a different view that has to be built. Usually it's custom-built for that tree. So you've got two options: build your own view, from scratch, every time... or build a XUL tree DOM and let Gecko's own tree utilities show it to you.

Believe it or not, I've tried both approaches... and finally decided to roll my own baseline solution. (If someone else has done this before, please let me know. It's best to have this in a common place.) More details in the extended section.

nsITreeView

Here's the kind of image that comes to mind every time I've looked at the nsITreeView interface:

nsITreeView's heart

Seriously, what in the name of (it's Easter weekend, I don't care to push my luck) what in the world is this? At least half the methods on it deal with row-specific or cell-specific details. Couldn't the people who work with this just given me a nsITreeRow interface and a getRow() method on the tree view?!?

I mean, it feels so dirty. It feels... procedural. Not object-oriented at all. How do you map a row in this tree to an object? You don't through this interface - because there's no row to grab.

It's also an interface with a lot of methods on it to implement. Yeeeuck. It's not immediately obvious which ones you need, and when. I've never truly understood this tree view stuff, even with XULPlanet's tutorial lending a hand.

<xul:treeitem/> and friends

I do understand treeitems, treechildren, treerow, and the like, though. It's DOM! It's something we already have! It's easy to inspect, to debug! It's familiar!

This was my preferred method of building trees for five years. Never mind the various people who kept telling me that tree views were so much better, ignoring me when I said, "No, make this a DOM set of trees, that doesn't need a lot to understand." It's simple. I like simple.

Until two or three months ago, when I discovered nsTreeContentView. For those of you who don't know about it, this is what takes your pretty DOM-based XUL tree and converts it into a nsITreeView object.

So here's what happens: My app spends a lot of time crafting this oh-so-nice, fifteen-levels-deep, memory-crushing, CPU-melting DOM fragment. My program then appends it, forcing nsTreeContentView to go to work, ripping that fragment apart and creating... a nsITreeView that I did everything to avoid dealing with, and which you just waited far too long for. Not to mention the bootstrapping I have to put on top of that DOM tree to bind each row to some object.

There are a few things that are more efficient than this approach...

Starting over

So finally I said, "enough of this." If I'm going to build a tree for objects, I'm going to do it right. I'm going to create a generalized tree view component, reading from both the tree and from objects, to show the object hierarchy. I'm going to define a very simple, and very flexible, API that my component's users can build in and make it all work.

The first concept is that tree columns define what the cells show. So let's just go ahead and define a propertyname attribute to stick on each <xul:treecol/>. element. We still need those, anyway. For more complex properties, define a fallback by allowing the tree's author to set a function on the column element. I do this through the DOM 3 UserData API.

The second concept is that there's a 1:1 mapping between rows and objects. That is, for each row, there should be exactly one object, and vice versa. So, at least internally, we need a TreeRow class to store a reference to the original objects.

The third concept is that the tree view needs to know how to get child objects of a given object. To do that, the tree view requires you to pass in a function which takes an object and returns an array of objects, which you say are children of that object.

The fourth concept is to provide a way to add top-level objects to this tree. Every DOM tree has a root node, every file system has a few items at the top. A generic tree view class can't know about them beforehand, so you have to tell it about them.

Put these four together, and you have enough to build a generic algorithm, a generic "class tree viewer".

ClassTreeView

Source Code

Sample chrome code (copied, altered from the XUL Tutorial on developer.mozilla.org)

testTreeView.xul

Element and MatterState represent (in this case) two similar classes which live in the same tree - so really, you'd only need one of them. One column has a propertyname attribute on it. The other one (further down in the init() function) has a cellGetter function on it. The getObjectChildren() function tells the tree view how to go from one object to its children. Everything else is just raw data, initializing the tree and the tree view, and adding top-level objects to the tree. It's really just that simple, and pretty light-weight.

Now, I will admit this ClassTreeView is not complete. For instance, it's read-only, and doesn't yet support progress meters, check boxes, etc. I don't need those at this point, and to me it's just more complexity (see nsITreeView for details). As a starting point for showing a true object hierarchy, though, it's good enough. If you know tree views and want to finish this, patches accepted!

API-wise, it's a "best guess". I designed this to work based on my needs and understanding. Maybe there are better approaches - but for this particular problem, I think it's a good start.

The one true weakness to ClassTreeView is that content web pages cannot use it. The reason for that is buried at the end of the nsITreeView.idl file: the nsINativeTreeView interface. So perhaps I will someday rewrite this in C++ code - after I figure out C++-to-JavaScript interfaces, and with a lot of reviews to make sure I get it right - and I'll make it available to the Web. (Then again, you don't see a whole lot of XUL on the Web... maybe for reasons like this.)

Posted by WeirdAl at March 22, 2008 7:24 PM
Comments

Yea, I've done this before. Unfortunately, it turned out to be about a million times more tricky than I realized going in, or I never would have bothered. Code that seems to work pefectly on simple examples utterly fails to capture the rich complexity that is actually required. Frankly, the nsITreeView interface was never meant to be implemented by mere mortals, or more than once.

Jan Varga set me straight, however, and ripped out all of that code and replaced it with a template that pulls data from the sqlite query processor. That made it dead simple. I believe he maintains (though that may be too strong a word) a patch to backport the feature to the 1.8 branch, if that's what you need.

Posted by: db48x at March 22, 2008 10:10 PM

This sounds like a possible good candidate for FUEL perhaps?

Posted by: Shawn Wilsher at March 23, 2008 4:02 AM

I wrote the original hierarchical nsITreeView example submitted to XULplanet and still available at http://neil.rashbrook.org/primary.xul although you'll need to enable privileges to demonstrate it.

What's the deal with your propertyname attribute?

(From Alex: It's really just a convenience so that one can map a property of an object to a column in that row. The fallback is the cellGetter user data.)

Posted by: Neil at March 23, 2008 12:17 PM

The tree-utils script that is a part of venkman take a little getting used to but is quite powerful when you need it:

http://mxr.mozilla.org/seamonkey/source/extensions/venkman/resources/content/tree-utils.js

Posted by: Mossop at March 23, 2008 2:28 PM
Post a comment









Remember personal info?