COMobjects.NET Deep Tree 

Overview

Treeview is one of the most popular way of navigating through the site. Probably, you have seen a great number of scripts for its implementation. Most of them requires roundtrip to expand or collapse nodes. Of course some of them use DHTML to expand new nodes dynamically. But what to do if there are thousands nodes in Treeview? You can't load all of them at once. So roundtrip required again. Have you ever seen tree of content at MSDN or navigation menu at our site? Probably, you have noticed that nodes of the tree are downloaded to the browser dynamically. You need not to reload whole page to show some new nodes when you expand it. 

For example, when you click on node "Languages" loading box is displayed and  couple seconds later subtree is displayed. But without full roundtrip! We download only additional nodes.

     

In this tutorial I try to show the way how to create such a treeview. Please draw attention that this script works only with Internet Explore 5.0 or higher. If you modify this script for working with other browsers, please let me know (mail to fed@comobjects.net ).

First download COMobjects.NET Deep Tree. When you unpack ZIP file you will find 2 folders: BasicDemo and FileSystemDemo. The first folder contains basic sample which we will discuss first. It have been written only as client-side script, so it even does not require web server. In the second folder, more complex sample of treeview is located: it is ASP script which reads file system from specified path and automatically generates result tree. The part of script which scans file system is available in two versions - ASP and ASP .NET.

Basic Demo

Creating Data Source

As I said before first I will tell you about BasicDemo. I'd like to describe how I have created this code.

First you should create data source, which describes navigation tree. I have chosen  XML as data source, because this format is very convenient for describing hierarchical data. More over, all the modern browsers (Internet Explorer 5.0 and higher, Netscape 6) support it. In spite of the fact that this script works only with IE 5.0 or higher, I have left possibility to change script for other browser support. Here is source of file Root.xml (in TOC folder):

<?xml version="1.0" encoding="Windows-1252"?> 
<Tree base="Help/">
	<TreeNode icon="item" name="Overview" href="Overview.htm" target="main"/> 
	<TreeNode icon="folder" name="Products" href="Products/overview.htm" target="main"> 
		<TreeNode icon="item" name="Colorizer" href="Products/Colorizer/overview.htm" target="main" />
		<TreeNode icon="item" name="Picture Processor" href="Products/PictureProcessor/overview.htm" target="main" /> 
		<TreeNode icon="item" name="Picture Gallery" href="Products/PictureGallery/overview.htm" target="main"/>
	</TreeNode>
	<TreeNode icon="folder" name="Reference" href="Help/Reference/Overview.htm" target="main">
		<TreeNode icon="folder" name="Languages" src="TOC/Languages.xml"/>
		<TreeNode icon="item" name="Specifications" href="Help/Reference/Specifications.htm" target="main"/>
	</TreeNode>	
</Tree>

I believe that this code is intuitively understandable. However, I would like to comment some spots:

<Tree base="Help/">

The attribute base specifies  the baseline URL on which relative links will be based. For example in tag:

<TreeNode icon="item" name="Overview" href="Overview.htm" target="main"/>

attribute href links to:

Help/Overview.htm
<TreeNode icon="item" name="Overview" href="Overview.htm" target="main"/>

Attribute icon specifies icon file without .gif extension

Attribute name specifies the name of the node

Attribute href specifies link as stated above

Attribute target specifies the window or frame where the contents is targeted.

Attributes href and target can be omitted. Node can have unlimited number of nested nodes. Notice that the following node:

<TreeNode icon="folder" name="Languages" src="TOC/Languages.xml"/>

has  src attribute in addition to the attributes, described above. It links to the TOC/Languages.xml file which subscribes subtree. Thus you can keep tree in several XML files. Here is its souce code:

<?xml version="1.0" encoding="Windows-1252"?>
<Tree base="Help/Reference/Languages/">
	<TreeNode icon="folder" name="C#" src="TOC/CSharp.xml" />
	<TreeNode icon="item" name="VB.NET" href="VB.NET/overview.htm" target="main" />
	<TreeNode icon="item" name="JScript .NET" href="JScript.NET/overview.htm" target="main" />
</Tree>

The following tag:

<TreeNode icon="folder" name="C#" src="TOC/CSharp.xml" /> 

has  src attribute too, which links to another subtree and so on. So you can create series of subtree in such a way. But there is another thing to do: you should also provide client-side JScript, which will render data on browser.

Rendering data on browser

Open file TOC.htm , find the following line and set sRootTree variable to root xml file link.

//Root Tree
var sRootTree = "TOC/Root.xml"

Add the following code to the body of HTML document:

<body onload="initPage();">
	<div id="Tree" style="margin-left:-20px;">
	</div>
</body>

When you load TOC.htm, onload event fires. You must handle it. Use following code:

function initPage(){
	oSelNode = null;
	//set loadingNode to the tree container
	loadingNode = document.all("Tree");
	oXml = new ActiveXObject("Microsoft.XMLDOM");
	//Change this property to false if you are working in synchronous mode
	oXml.async = true;
	//set handler for xml data loading event
	oXml.onreadystatechange = onLoadXml;
	//load root xml data source
	oXml.load(sRootTree);
}

When xml data have been loaded, the following code runs:

function onLoadXml(){
	//test if xml was fully loaded (COMPLETED(4))
	if (oXml.readyState==4){
		hideLoadingNode (loadingNode);		
		//test if xml is well formed and contains other than documentElement nodes
		if ((oXml.parseError.reason=="") && (oXml.documentElement.childNodes.length>0)){		
			if (loadingNode!=document.all("Tree")){
				loadingNode.childNodes(0).childNodes(0).src = sImageMinus;
			}
			//Set sBaseHref which specifies the baseline URL
			sBaseHref = oXml.documentElement.attributes.getNamedItem("base").value;
			//Building treeview starts with documentElement of XML and loading node
			//We expand only one level of deep treeview, so we just pass "true" value in this call						
			buildTree(oXml.documentElement, loadingNode, "true");		
		}
		else
		{	
			//If subtree loading have been failed, change action image to none and remove event handler		
			loadingNode.childNodes(0).childNodes(0).src = sImageNone;
			loadingNode.childNodes(0).childNodes(0).onclick = null; 
		}
		//Actions with loading node are finished, so set it to null
		loadingNode = null;
	}	
}

If XML data is well formed, buildTree method is called. Parameter sourceNode is a XML source node, resultNode is a produced HTML node, expanded specifies whether display resultNode  expanded or not. To comment this code I would like to show you the structure of assemblied HTML tree:

function buildTree(sourceNode, resultNode, expanded){
	var oLI, oUL, oIMG, oA, oNOBR, i, oNodeAttributes;
	var sIcon, sName, sHref, sTarget, sSrc;
	//Create container for child nodes	
	oUL = document.createElement("<ul>");
	oUL = resultNode.appendChild(oUL);
	//if node expanded then display container
	if (expanded=="true"){
		oUL.style.display="block";
	}
	else{
		oUL.style.display="none";
	}
	//Run over nodes in XML source
	for (i=0;i<sourceNode.childNodes.length;i++){
		//Set obligatory values
		oNodeAttributes = sourceNode.childNodes(i).attributes;
		sIcon = oNodeAttributes.getNamedItem("icon").value;
		sName =	oNodeAttributes.getNamedItem("name").value;		
		//Set optional values
		if (oNodeAttributes.getNamedItem("href")!= null){
			sHref =	oNodeAttributes.getNamedItem("href").value;
		}
		else{
			sHref = "";
		}
		if (oNodeAttributes.getNamedItem("target")!=null){
			sTarget = oNodeAttributes.getNamedItem("target").value;
		}
		else{
			sTarget = ""
		}
		//Create node
		oLI = document.createElement("<li>");	
		oLI = oUL.appendChild(oLI);
		oNOBR = document.createElement("<nobr>");	
		oNOBR = oLI.appendChild(oNOBR);
		//Create action image
		oIMG = document.createElement("<img>");
		oIMG.border = 0;
		//If src attribute is not empty, add custom attribute to the result node
		if (oNodeAttributes.getNamedItem("src")!=null){
			sSrc = oNodeAttributes.getNamedItem("src").value;			
		}
		else{
			sSrc = "";
		}		
		oLI.setAttribute("src", sSrc);
		//If src attribute is not empty or amount of the child nodes is not equals zero		
		if (sSrc!="" || sourceNode.childNodes(i).childNodes.length>0){
			//If sub nodes was not loaded
			if (sSrc!=""){
				oLI.setAttribute("LoadedChildren", "false");				
			}
			//If sub nodes are already loaded
			else{
				oLI.setAttribute("LoadedChildren", "true");				
			}	
			oIMG.src = sImagePlus;
			oLI.setAttribute("Expanded", "false");	
			//Set action image event handler		
			oIMG.onclick = actionOnClick;	
		}		
		else{
			oIMG.src = sImageNone;
		}
		//Create icon image
		oIMG = oNOBR.appendChild(oIMG);
		oIMG = document.createElement("<img>");
		oIMG.border = 0;
		oIMG.src = sImagesPath + sIcon + ".gif";
		oIMG = oNOBR.appendChild(oIMG);
		oA = document.createElement("<a>");
		if (sHref!=""){
			oA.href = sBaseHref + sHref;
			//Set event handler for node		
			oA.onclick = nodeOnClick;
			oA.onmouseover = nodeOnMouseOver;
			oA.onmouseout = nodeOnMouseOut;			
		}		
		if (sTarget!=""){
			oA.target = sTarget;		
		}
		oA.innerText = sName;
		oA.title = sName;		
		oA.className = "treenode";
		oA = oNOBR.appendChild(oA);
		if (sourceNode.childNodes(i).childNodes.length>0)
		{ 
			//Assembly subtree
			buildTree(sourceNode.childNodes(i), oLI, "false")
		}
	}
	//Add custom attributes to resultNode which specifies whether node loaded children or not and 
	//whether is was expanded or not
	resultNode.setAttribute("LoadedChildren", "true")
	resultNode.setAttribute ("Expanded", expanded);	
}

We have attached OnClick event handler to action (plus/minus) image.  I'd like to discuss it in detail

function actionOnClick(){
	var oNode
	oNode = event.srcElement;
	event.cancelBubble = true;
	//If node is already expanded we collapse it
	if (oNode.parentNode.parentNode.getAttribute ("Expanded")=="true"){
		oNode.parentNode.parentNode.childNodes(1).style.display = "none";
		oNode.parentNode.parentNode.childNodes(0).childNodes(0).src = sImagePlus;
		oNode.parentNode.parentNode.setAttribute ("Expanded", "false")
	}
	//If node is collapsed expand it
	else{
		//If children nodes are already loaded, just expand it
		if (oNode.parentNode.parentNode.getAttribute("LoadedChildren")=="true"){
			oNode.parentNode.parentNode.childNodes(1).style.display = "block";
			oNode.parentNode.parentNode.childNodes(0).childNodes(0).src = sImageMinus;		
			oNode.parentNode.parentNode.setAttribute ("Expanded", "true")
		}
		 //Children nodes are not loaded yet, we need to load it
		else{
			//If the other nodes are loaded at the current time, we should stop it
			if (loadingNode!=null){
				hideLoadingNode(loadingNode);
			}
			//Set new node
			loadingNode = oNode.parentNode.parentNode;
			//Show loading node
			showLoadingNode(loadingNode);		
			//Assembly subtree	
			oXml.load(loadingNode.getAttribute("src"));
		}
	}
}

Of course we have implemented some other auxiliary methods in the script, but I guess you won't have any problems to understand how do they work. All of them you can find in the source code.

File System Demo

It is located in FileSystemDemo folder. The main idea of this script is creating XML source dynamically. I think that this script isn't very difficult for understanding if you have worked with XML DOM, however I would like comment it a bit. Note, we used XML assembling  instead of direct using Response.Write method for creating XML, because it allows us to get well formed XML easily.

To get it working you need only to change strRootPath which maps to the RootFolder from which you'd like to read the structure of your website.

<%
Option Explicit
Response.ContentType = "text/xml"
'Change this variable to specify root folder
'Should be empty or ended to slash
Dim strRootPath
strRootPath = "/"
'Get relative path
Dim strPath
strPath = Request.QueryString("path")
'Prevent using path ../../ which allows somebody to go higher that root folder
If InStr(1, Server.MapPath(strRootPath & strPath), Server.MapPath(strRootPath), 1) <> 1 Then
	Err.Raise 7101, "COMobjectsNET.DeepTree", "Security Error"
	Response.End
End If
'Creating xml document
Dim xmlDoc
Set xmlDoc = Server.CreateObject("Msxml2.DOMDocument.3.0")
Dim xmlPI, xmlElement, xmlAtt
'Set encoding of XML document
Set xmlPI = xmlDoc.appendChild(xmlDoc.createProcessingInstruction("xml", "version='1.0' encoding='Windows-1252'"))
'Create root element and set base attribute
Set xmlDoc.documentElement = xmlDoc.createElement("Tree")
Set xmlAtt = xmlDoc.documentElement.Attributes.setNamedItem(xmlDoc.createAttribute("base"))
xmlAtt.Value = strRootPath & strPath
'Reading subfolder and files
Dim objFileSystemObject, objDirectory, objSubDirectory, objFile
Set objFileSystemObject = Server.CreateObject("Scripting.FileSystemObject")
Set objDirectory = objFileSystemObject.GetFolder(Server.MapPath(strRootPath & strPath))
For Each objSubDirectory In objDirectory.SubFolders
	'Create tree node
	Set xmlElement = xmlDoc.documentElement.appendChild(xmlDoc.createElement("TreeNode"))
	'Set attribute "icon" to value "folder"
	Set xmlAtt = xmlElement.Attributes.setNamedItem(xmlDoc.createAttribute("icon"))
	xmlAtt.value = "folder"
	'Make attribute "name" equal to folder name	
	Set xmlAtt = xmlElement.Attributes.setNamedItem(xmlDoc.createAttribute("name"))
	xmlAtt.value = objSubDirectory.Name
	 'Using attribute "src" for linking it to subtree	
	Set xmlAtt = xmlElement.Attributes.setNamedItem(xmlDoc.createAttribute("src"))
	xmlAtt.value = "Files.asp?path=" & Server.URLEncode(strPath & objSubDirectory.Name & "/")
Next
For Each objFile In objDirectory.Files
	'Create tree node
	Set xmlElement = xmlDoc.documentElement.appendChild(xmlDoc.createElement("TreeNode"))
	'Set attribute "icon" to value "item"    
	Set xmlAtt = xmlElement.Attributes.setNamedItem(xmlDoc.createAttribute("icon"))
	xmlAtt.value = "item"
	'Make attribute "name" equal to file name
	Set xmlAtt = xmlElement.Attributes.setNamedItem(xmlDoc.createAttribute("name"))
	xmlAtt.value = objFile.Name	
	'Set attribute "target"	
	Set xmlAtt = xmlElement.Attributes.setNamedItem(xmlDoc.createAttribute("target"))
	xmlAtt.value = "main"
	 'Combine attributes "href" and "base" of root element to get link to the file		
	Set xmlAtt = xmlElement.Attributes.setNamedItem(xmlDoc.createAttribute("href"))
	xmlAtt.value = objFile.Name
Next
'Write directly to response stream
xmlDoc.save Response
%>

The ASP.NET code differs from ASP only in detais. We use objects from namespace System.IO instead of FileSystemObjects and XML.NET instead of MSXML 3.0. Probably, you have noticed that we use in client-side script first version of Microsoft XML and in server-side scripts we use MSXML 3.0. The reason of such choose is that Internet Explorer 5.0 shipped with first version of Microsoft XML. So it would be more wise decision to choose it for providing the compatibility. However, MSXML 3.0 is much more productive, so it is better to use it instead the first version.

I hope that I helped you with this script. If you have any question feel free to contact me to fed@comobjects.net.

COMobjects.NET,  18 November 2001