Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web / CSS

CSS Friendly AJAX Tree Control

2.20/5 (2 votes)
13 Aug 2008GPL34 min read 1   126  
A very simple and lightweight AJAX hierarchical tree control.

Introduction

I am developing a WebPart and I needed a client-side hierarchical tree control. I also wanted a CSS-friendly specific markup. I then found this was great, and got me started. I have this setup now as is, and it works great. The only thing was that some datasets that I may possibly be dealing with will be very large. This script does some initial setup that is costly with large data. Therefore, I spawned this bit of code. I do not use the tree control JavaScript anymore, but I do still use the CSS and the code was the basis of my final code.

Background

I just want to point out a few things that I think could be helpful in all aspects of web development.

  1. Namespaces
  2. Please see this link for a more elaborate discussion on why this works. Dustin also has a really helpful book that is on my desk right now.

    JavaScript
    var DE = {
        data : {},
        net : {},
        client : {},
        util : { 
            coll: {}
            },
        widget : { },
        example : { }
    };

    Usage: With today's code being subsets of frameworks, it is essential to use namespaces in your code.

  3. JavaScript classes
  4. JavaScript
    ////DOM class
    DE.util.DOM = function() {
           
       //create private member vars here.
       //Only protected methods have access to these vars 
       //this all falls in the realm of js classes and is not 
       //discussed fully in this article but 
       //can be found all over the web.
    
       //protected method with access to private member vars 
       this.addClass = function() {
            .......
       }
       this.anotherMethod = function() {...........}
    
    }; 
    
    //public method    
    DE.util.DOM.prototype.$ = function() {
    
    using the above class:
    var varName = new DE.util.DOM();
  5. Logging
  6. This is very helpful and needed. Alerts get old very fast!

    JavaScript
    var log = new DE.util.Logger(); 
    log.show( true );
    log.showTimeStamp(true);

    Now, logging is a simple as:

    JavaScript
    log.Log("log this now");

    You also need a div on your page with id="Log".

    HTML
    <div id=&quot;Log&quot;></div>
  7. Simplistic AJAX calls
  8. JavaScript
    var _cb = new DE.util.HierFilterCB('filterdata' );
    ajax1.setAsyncCall(false); or not Async jax
    ajax1.request('GET',&quot;HierFilterPage.aspx?rnd=&quot;+ Math.random(), _cb );
  9. Server-side code
  10. C#
    HierFilter hier = null;
    String parent = Request.QueryString[&quot;parent&quot;];
    if (!String.IsNullOrEmpty(parent))
    {
       //if index is not null then a specific request for a node has been made 
       //index is null only on intial load
       hier = new HierFilter(@&quot;C:/Downloads/CssFriendly&quot; + 
                  @&quot;AjaxTreeCtrl/App_Data/hierdata.xml&quot;, Int32.Parse(parent));
       Response.Write( hier.ToString() );
    }

In short

The client makes an initial call to the server for XML data at depth 0.

JavaScript
var ajax1 = new DE.util.Ajax();
var _cb = new DE.util.HierFilterCB('filterdata' );
ajax1.setAsyncCall(false);
ajax1.request('GET',&quot;HierFilterPage.aspx?rnd=&quot;+ Math.random(), _cb );

The code above is not async because we need to ensure that all data is returned because the callback is responsible for parsing the data returned by the server. The client calls the following code block. The server instantiates a HierFilter type by passing it the location of the data (XML).

C#
//intial load 
hier = new HierFilter(@&quot;C:/Downloads/&quot; + 
           @&quot;CssFriendlyAjaxTreeCtrl/App_Data/hierdata.xml&quot;);
Response.Write(hier.ToString());

The HierFilter class inherits from FilterBase. In this example, FilterBase is responsible for formatting the processed data back to the client.

HierFilter.cs

C#
public HierFilter( String file) : base()
{
    selectedIndicies = &quot;&quot;;
    XmlWriter w = base.getXmlWriter();
    XmlDocument doc = new XmlDocument();
    doc.Load( file );
    CreateTree(w, doc);
    
}

FilterBase.cs

The following code block will help create well-formed XHTML data to send back to the client.

C#
public XmlWriter getXmlWriter()
{
     //uses class variable sb
     XmlWriterSettings settings = new XmlWriterSettings();
     XmlWriter w;
    //start first <ul> tag 
    //tell the xml parser that i am not abiding by strigent xml rules 
    settings.ConformanceLevel = ConformanceLevel.Fragment;
    //pretty print - not necessary but good for debugging 
    settings.Indent = true;
    //_settings.Encoding = Encoding.Unicode;
    //dont show xml info in header 
    settings.OmitXmlDeclaration = true;
    settings.CloseOutput = false;
    //XmlWriter factory push xml to string builder with these settings 
    w = XmlWriter.Create(sb, settings);
    return w;
}

HierFilter.cs

The following creates the tree data at level 0. The CSS-friendly tree markup is a pattern of nested unordered list elements.

XML
<ul id=&quot;mktree&quot;> 
    <li>......</li> 
    <li>......</li>
</ul>

The code:

C#
private void CreateTree(XmlWriter w, XmlDocument doc )
{
    //get node list 
   // InitSettings();
    w.WriteStartElement(&quot;ul&quot;); // beginning <ul> tag 
    w.WriteStartAttribute(&quot;class&quot;);
    w.WriteValue(&quot;mktree&quot;);
    w.WriteStartAttribute(&quot;id&quot;);
    w.WriteValue(&quot;hierfilter&quot;);
    GoDeeper(w , doc , 0 );
    w.WriteEndElement(); //</ul> outermost ul tag 
    w.Flush();
}
private void GoDeeper(XmlWriter w , XmlDocument doc, int level )
{
    bool state =false;
    int index = 0;
    bool hasChildren = false;
    //create a tree based on the level provided
    XmlNodeList nodes = doc.SelectNodes(&quot;//n[@d='&quot;+ level +&quot;']&quot;);           
    foreach (XmlNode node in nodes)
    {
        if (node.Attributes.Count == 4)
        {
            //get state from xml 
            if (node.Attributes[3].Value == &quot;0&quot;)
                state = false;
            else
                state = true;
        }
        index = Int32.Parse(node.Attributes[0].Value);
        hasChildren = Boolean.Parse(node.Attributes[2].Value);

        w.WriteStartElement(&quot;li&quot;); // beginning <ul> tag 
        if (hasChildren)
        {
            //if the node has children place a plus gif next to it using css 
            w.WriteStartAttribute(&quot;class&quot;);
            w.WriteValue(&quot;liClosed&quot;);
        }
        w.WriteStartElement(&quot;span&quot;);
        w.WriteStartAttribute(&quot;class&quot;);
        w.WriteValue(&quot;index&quot;);
        w.WriteEndAttribute();
        w.WriteValue(index.ToString());
        w.WriteEndElement();      
        w.WriteStartElement(&quot;span&quot;);
        w.WriteStartAttribute(&quot;class&quot;);
        w.WriteValue(&quot;bullet&quot;);
        w.WriteEndAttribute();
        w.WriteValue( node.InnerText );
        w.WriteEndElement(); 

        //w.WriteValue(node.InnerText );
        w.WriteEndElement();
    }
    //close all oustanding xml tags 
    //CloseOut(w, elementStack);
}

The following is called when a node is clicked. This passes in the parent ID and returns all of its children in well formatted XHTML.

C#
public void GetChildren( XmlWriter w, XmlDocument doc, int parent )
{
    bool state = false;
    int index = 0;
    bool hasChildren = false;
   
    //w.WriteStartElement(&quot;ul&quot;); // beginning <ul> tag 
    //w.WriteStartAttribute(&quot;class&quot;);
    //w.WriteValue(&quot;liOpen&quot;);
    //create a tree based on the level provided
    XmlNodeList nodes = doc.SelectNodes(&quot;//n[@p='&quot; + parent + &quot;']&quot;);
    foreach (XmlNode node in nodes)
    {

        if (node.Attributes.Count == 4)
        {
            //get state from xml 
            if (node.Attributes[3].Value == &quot;0&quot;)
                state = false;
            else
                state = true;
        }
        index = Int32.Parse(node.Attributes[0].Value);
        hasChildren = Boolean.Parse(node.Attributes[3].Value);

        w.WriteStartElement(&quot;li&quot;); // beginning <ul> tag 
        if (hasChildren)
        {
            //if the node has children place a plus gif next to it using css 
            w.WriteStartAttribute(&quot;class&quot;);
            w.WriteValue(&quot;liClosed&quot;);
        }
        else
        {
            w.WriteStartAttribute(&quot;class&quot;);
            w.WriteValue(&quot;liBullet&quot;);
        }
        w.WriteStartElement(&quot;span&quot;);
        w.WriteStartAttribute(&quot;class&quot;);
        w.WriteValue(&quot;index&quot;);
        w.WriteEndAttribute();
        w.WriteValue(index.ToString());
        w.WriteEndElement();

        w.WriteStartElement(&quot;span&quot;);
        w.WriteStartAttribute(&quot;class&quot;);
        w.WriteValue(&quot;bullet&quot;);
        w.WriteEndAttribute();
        w.WriteValue(node.InnerText);
        w.WriteEndElement();

        //w.WriteValue(node.InnerText );
        w.WriteEndElement();

    }
   // w.WriteEndElement(); //end of ul of liOpen
    w.Flush();
}

Back to the client callback that started it all. The following is in the success method. This method will be called if the AJAX call completed without error. The data returned is well formed, so we parse it and look for certain criteria that will allow the CSS to style our growing tree. If the element has a class name liClosed, then we want to attach an OnClick event to it. This translates to a node having children or not. If a node does not have children, then there is no need for us to make a call to the server. How do we know if the node has children? The XML has an attribute 'hc', and this is processed on the server and delivered to the client.

As the tree expands by level, the pattern grows as nested unordered lists.

HTML
<ul>
     <li> 
        <ul> 
            <li>....../<li>
        </ul>
    </li>
</ul>

The code:

C#
if ( n.className == 'liClosed')
{
   n.onclick = function() {
   log.Log(&quot;classname inside HierFilterCB &quot; + n.className );
  //check to see if node has been loaded 
   var ul = n.getElementsByTagName('ul');
   if ( ul.length > 0 ) 
   {
     if ( n.className == 'liClosed') 
     {
       n.className = 'liOpen';
     }else{n.className = 'liClosed';}
   }else
    {                                                     
      var spans = n.getElementsByTagName('span');
      //get the index from here 
      for ( var j =0; j<spans.length;j++)
      {
       if ( spans[j].className == 'index')
       {
         var index = spans[j].innerText;
         var child = new DE.util.ChildItemCB( n );
         var childrenAjax = new DE.util.Ajax();
         childrenAjax.request('GET', 
           'HierFilterPage.aspx?parent='+index,child);
       }
         log.Log(&quot;spans &quot; + spans[j] );
      }
    }   
 }  
}

When a node is clicked, the following code is called, if it has not already made a trip to the server. If it has, then there is some code above that will just modify the CSS to expand or collapse the tree instead.

JavaScript
//callback method called when 
//a node is clicked after intial loading is complete. 
//see also  DE.util.HierFilterCB for more info
//parsing is better understood by inspecting hierdata.xml
DE.util.ChildItemCB = function( child ) {        
    this.success = function(val){           
       log.Log(&quot;child className &quot; + child.className ) ;           
       var children = document.createElement('ul')           
       children.innerHTML = val;           
          var li = children.getElementsByTagName('li');
             for ( var i=0; i < li.length; i++ )
                 parseTree(li[i]);

The above parse tree is called on every <li> item returned by the server. Again, the pattern is, if a node has childrenn then we need to apply an OnClick event to it and modify its class name, therefore allowing the CSS to properly style our tree with the appropriate plus/minus signs.

C#
if ( n.className == 'liClosed')
{ 
  log.Log(&quot;n &quot;+ n.innerText);
  log.Log(&quot;n.className &quot; + n.className );
  n.onclick = function(e) {
    e = e || (window.event || {});
    e.cancelBubble = true;
    ...................

The above code is similar to the first call to the server. Accept must cancel any event bubbling that was inherited from its parent nodes.

Here is a sample XML dataset used:

XML
<n i='0' d='0' hc='true' s='0'>1  -  Scope</n>
<n i='1' p='0' d='1' hc='false' s='0'>1.1  -  Determine project scope</n>
<n i='2' p='0' d='1' hc='false' s='0'>1.2  -  Secure project sponsorship</n>
<n i='3' p='0' d='1' hc='false' s='0'>1.3  -  Define preliminary resources</n>
<n i='4' p='0' d='1' hc='false' s='0'>1.4  -  Secure core resources</n>
<n i='5' p='0' d='1' hc='false' s='0'>1.5  -  Scope complete</n> 
<n i='6' p='0' d='1' hc='true' s='0'>1.6  -  Program Group</n>
<n i='7' p='6' d='2' hc='false' s='0'>1.6.1  -  Milestone 1</n>
<n i='8' p='6' d='2' hc='false' s='0'>1.6.2  -  Milestone 2</n>
<n i='9' p='6' d='2' hc='false' s='0'>1.6.3  -  Milestone 3</n>

Using the code

Download the code and change the path to the XML file inside HierFilterPage.aspx and then run from Default.aspx.

Points of interest

This only works in IE6, not sure about IE7, but I would like feedback on how to make it work with Firefox. So, please, if anyone gets this to work, I would like to see it. Cheers!

This was developed in WebDeveloper 2008. However, using any Visual Studio based IDE should be as easy as eliminating the solution file and just opening the project from the IDE of your choice.

History

First release - Aug 13, 2008.

License

This article, along with any associated source code and files, is licensed under The GNU General Public License (GPLv3)