Building a class browser with Microsoft Ajax 4.0 Preview 5

(c) 2004 Bertrand Le Roy The Microsoft Ajax Library 4.0 Preview 5 is the first release of Microsoft Ajax that I didn’t participate in: I left the team a few months ago. But that doesn’t mean I don’t love what’s in there, and I really do. And by the way I’ve also seen what’s in Preview 6 too and man that will seriously rock.

So I thought I’d write a little something to celebrate the new preview. The new features include recursive templates, which is pretty much begging us to implement a treeview with it, and we’ll do just that in this post.

There is also an intriguing capability, which enables you to dynamically set what template to render for each data item, and where to render it. At first, this doesn’t look like the most useful thing in the world, but it actually opens up some very interesting possibilities, which we’ll also show in this post.

The sample code that I’m going to write for this post is a rudimentary class browser. It will render a treeview representing the hierarchical structure of namespaces and classes in Microsoft Ajax, and clicking one of the tree nodes will render a details view for it: a list of classes and subnamespaces for namespaces, and a grouped list of members for classes.

Let’s start with the tree. It will be rendered as nested unordered lists by a simple recursive DataView:

<ul id="tree" class="tree"
    sys:attach="dataview"
    dataview:data="{{ Type.getRootNamespaces() }}"
    dataview:itemtemplate="nodeTemplate"
    dataview:oncommand="{{ onCommand }}">
</ul>

<ul id="nodeTemplate" class="sys-template">
  <li>
    <a href="#" onclick="return false;"
       sys:command="select">
      {{ getSimpleName($dataItem.getName()) }}
    </a>
    <ul sys:attach="dataview"
        dataview:data="{{ getChildren($dataItem) }}"
        dataview:itemtemplate="nodeTemplate"
        dataview:oncommand="{{ onCommand }}"></ul>
    </li>
</ul>

On the first UL, which is the outer DataView for the tree, you can see that we set the data property to Type.getRootNamespaces(), which returns the set of root namespaces currently defined.

We also set the template to point to the “nodeTemplate” element, which has to be outside the DataView itself when doing recursive templates. Note that the outer node of the template, the UL, won’t actually get rendered into the target ul (tree). It is only a container.

The command event of the DataView is hooked to the onCommand function, and we’ll get back to that when we couple the tree with the detail view.

In the template itself, you can see we have a link with the select command so that clicking it will trigger the nearest onCommand event up the DOM.

The text of that link is the results of a call to getSimpleName, which will extract the last part of the fully-qualified name of the namespace or class.

After that link, we find another DataView control. The data property of that control points to an array of namespaces and classes under the current object. But the nice part here is that the template property points to “nodeTemplate”, its own parent, enabling the recursive nature of the tree.

In other words, we’ve morphed a simple DataView control into a tree, with minimal effort and code.

There is just one thing missing to the tree, and that is the +/- buttons that will collapse and expand the tree nodes. This is actually very easy to set-up using CSS and some simple script. First, let’s collapse the tree by default. This is done by defining the style of the tree as follows in our stylesheet:

.tree ul 
{
    padding: 0;
    display:none;
}

This has the effect of collapsing all unordered list nodes under the tree.

The +/- button is created by adding the following to the template, right before the existing link:

<a class="toggleButton" href="#"
   sys:if="Type.isNamespace($dataItem)" 
   onclick="return toggleVisibility(this);">+</a>

The button is a simple link whose rendering is conditioned by whether the current data item is a namespace: only namespaces can be expanded, classes are leaf nodes.

The toggling function itself is fairly simple:

function toggleVisibility(element) {
    var childList = element.parentNode
.getElementsByTagName("ul")[0], isClosed = element.innerHTML === "+"; childList.style.display =
isClosed ? "block" : "none"; element.innerHTML = isClosed ? "-" : "+"; return false; }

This just toggles the display style of the first child UL between none and block, and the text of the link between + and –.

So there it is, we have built a simple tree by simply making use of the recursive capabilities of DataView and some very simple script.

Before we look at the details view, let’s look at the code that gets called when the user selects a node in the tree:

function onCommand(sender, args) {
    var dataItem = sender.findContext(
args.get_commandSource()).dataItem; $find("details").set_data(dataItem); }

That code gets a reference to the data item for the selected node from the template context that we can get from the sender of the event (the inner DataView that contains the selected node) using the command source as provided by the event arguments (that source is the element that triggered the command). We can then set the data of the details DataView to that data item, which will trigger that view to re-render.

Now let’s build the details view. The details view will display the child namespaces and classes if a namespace is selected in the tree, and the properties, events and methods (instance and static) in the case of a class.

For each case, we’ll use a different template: “namespaceTemplate” for namespaces, and “classTemplate” for classes,  but we’ll do so from the same DataView. This dynamic template switching is done by handling the onItemRendering event of the DataView:

function onDetailsRendering(sender, args) {
    var dataItem = args.get_dataItem();
    args.set_itemTemplate(Type.isNamespace(dataItem) ?
        "namespaceTemplate" : "classTemplate");
}

This code gets the data item from the event arguments and sets the itemTemplate property depending on its type.

Each of these two templates will have to display the contents of the selected object. But, and that will be the tricky part, we want all those to be neatly grouped into separated lists.

One way to do that would be to have one DataView per list but where would the fun be in that? Here, we are going to enumerate only once through the data items to display and dispatch them dynamically to this or that placeholder depending on their nature.

Once more, the key to doing that will be handling the onItemRendering event:

function onNamespaceChildRendering(sender, args) {
    if (Type.isClass(args.get_dataItem())) {
        args.set_itemPlaceholder("classPlaceHolder");
    }
}

This code is simply changing the rendering place holder for the curent item from the default (the DataView’s element) to “classPlaceHolder” if the current data item is a class (instead of a namespace). The template itself looks like this:

<div id="namespaceTemplate" class="sys-template">
   <h1>{{ $dataItem.getName() }}</h1>
   <div class="column">
     <h2>Namespaces:</h2>
     <ul sys:id="namespacePlaceHolder"
         sys:attach="dataview"
         dataview:data="{{ getChildren($dataItem) }}"
         dataview:itemtemplate="namespaceChildTemplate"
         dataview:onitemrendering=
"{{ onNamespaceChildRendering }}"> </
ul> </div> <div class="column"> <h2>Classes:</h2> <ul><li sys:id="classPlaceHolder"></li></ul> </div> </div> <ul id="namespaceChildTemplate" class="sys-template"> <li>{{ $dataItem.getName() }}</li> </ul>

As you can see, there really is only one DataView in there, and thanks to the code above, it can dispatch its rendering to different places if necessary. The template for the items of that DataView happens to be the same in all cases (namespaceChildTemplate) but it could be easily different, as it was for the parent details view.

The template for displaying classes is essentially the same thing, but with four placeholders instead of two.

So here’s what it looks like in the end:

Class Browser

Key takeaways of this post are that it’s now super-easy to render hierarchical data structures with DataView, and that you can do some interesting grouping of data on the fly by handling the item rendering event.

You can play with the class browser live here:
http://boudin.vndv.com/AjaxPreview5Tree/default.htm

And you can download the code here:
http://weblogs.asp.net/blogs/bleroy/Samples/AjaxPreview5Tree.zip

Microsoft Ajax 4.0 Preview 5:
http://aspnet.codeplex.com/Release/ProjectReleases.aspx?ReleaseId=32770

Jim and Dave’s posts on Preview 5:
http://weblogs.asp.net/jimwang/archive/2009/09/11/asp-net-ajax-preview-5-and-updatepanel.aspx
http://weblogs.asp.net/infinitiesloop/archive/2009/09/10/microsoft-ajax-4-preview-5-the-dataview-control.aspx

9 Comments

  • "Note that the outer node of the template, the UL, won’t actually get rendered into the target ul (tree). It is only a container."

    To me that seems counter-intuitive.

  • @Mike: that enables you to have more than one top-level element in your template. It's also easier to write valid markup this way.

  • I gotta ask -- what's your take on how the picture is related to the article? :) Previous ones I can theorize, but on this one I'm stumped. :)

  • lots of preview is given, i do hope the beta and release version will have less difference

  • Hi Bertrand, do you know when the final release of Ajax 4 will be out? How many more previews are left?

  • For one-way binding, is there a way to re-render an item if *any* property changes, instead of only re-rendering a property that has been bound? The problem I'm running into is when I have a code-if statement, where a property in the code-if statement has changed. I can't force an update to the rendered output. Right now I'm actually removing and re-adding the object at the same index to force it to be re-rendered.

    Does this make sense?

  • John, you can call dataview.refresh() to force a re-rendering. If you'd rather that only the individual thing updates, I'd consider creating an object that represents it in the view, and using a {binding}. Then whenever the conditions change, you simply use Sys.Observer to modify that object.

  • I have been programatically instantiating the templates using $create(Sys.UI.DataView, ... etc. is there a way to do this with the recursive templates e.g. not decaring them inline e.g. sys:attach="dataview" dataview:data="{{ getChildren($dataItem) }}"

  • @bodar77: you can probably handle onItemRendering and instantiate the inner dataview from there.

Comments have been disabled for this content.