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.
- Namespaces
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.
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.
- JavaScript classes
DE.util.DOM = function() {
this.addClass = function() {
.......
}
this.anotherMethod = function() {...........}
};
DE.util.DOM.prototype.$ = function() {
using the above class:
var varName = new DE.util.DOM();
- Logging
This is very helpful and needed. Alerts get old very fast!
var log = new DE.util.Logger();
log.show( true );
log.showTimeStamp(true);
Now, logging is a simple as:
log.Log("log this now");
You also need a div
on your page with id="Log"
.
<div id="Log"></div>
- Simplistic AJAX calls
var _cb = new DE.util.HierFilterCB('filterdata' );
ajax1.setAsyncCall(false); or not Async jax
ajax1.request('GET',"HierFilterPage.aspx?rnd="+ Math.random(), _cb );
- Server-side code
HierFilter hier = null;
String parent = Request.QueryString["parent"];
if (!String.IsNullOrEmpty(parent))
{
hier = new HierFilter(@"C:/Downloads/CssFriendly" +
@"AjaxTreeCtrl/App_Data/hierdata.xml", Int32.Parse(parent));
Response.Write( hier.ToString() );
}
In short
The client makes an initial call to the server for XML data at depth 0.
var ajax1 = new DE.util.Ajax();
var _cb = new DE.util.HierFilterCB('filterdata' );
ajax1.setAsyncCall(false);
ajax1.request('GET',"HierFilterPage.aspx?rnd="+ 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).
hier = new HierFilter(@"C:/Downloads/" +
@"CssFriendlyAjaxTreeCtrl/App_Data/hierdata.xml");
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
public HierFilter( String file) : base()
{
selectedIndicies = "";
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.
public XmlWriter getXmlWriter()
{
XmlWriterSettings settings = new XmlWriterSettings();
XmlWriter w;
settings.ConformanceLevel = ConformanceLevel.Fragment;
settings.Indent = true;
settings.OmitXmlDeclaration = true;
settings.CloseOutput = false;
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.
<ul id="mktree">
<li>......</li>
<li>......</li>
</ul>
The code:
private void CreateTree(XmlWriter w, XmlDocument doc )
{
w.WriteStartElement("ul");
w.WriteStartAttribute("class");
w.WriteValue("mktree");
w.WriteStartAttribute("id");
w.WriteValue("hierfilter");
GoDeeper(w , doc , 0 );
w.WriteEndElement();
w.Flush();
}
private void GoDeeper(XmlWriter w , XmlDocument doc, int level )
{
bool state =false;
int index = 0;
bool hasChildren = false;
XmlNodeList nodes = doc.SelectNodes("
foreach (XmlNode node in nodes)
{
if (node.Attributes.Count == 4)
{
if (node.Attributes[3].Value == "0")
state = false;
else
state = true;
}
index = Int32.Parse(node.Attributes[0].Value);
hasChildren = Boolean.Parse(node.Attributes[2].Value);
w.WriteStartElement("li");
if (hasChildren)
{
w.WriteStartAttribute("class");
w.WriteValue("liClosed");
}
w.WriteStartElement("span");
w.WriteStartAttribute("class");
w.WriteValue("index");
w.WriteEndAttribute();
w.WriteValue(index.ToString());
w.WriteEndElement();
w.WriteStartElement("span");
w.WriteStartAttribute("class");
w.WriteValue("bullet");
w.WriteEndAttribute();
w.WriteValue( node.InnerText );
w.WriteEndElement();
w.WriteEndElement();
}
}
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.
public void GetChildren( XmlWriter w, XmlDocument doc, int parent )
{
bool state = false;
int index = 0;
bool hasChildren = false;
XmlNodeList nodes = doc.SelectNodes("
foreach (XmlNode node in nodes)
{
if (node.Attributes.Count == 4)
{
if (node.Attributes[3].Value == "0")
state = false;
else
state = true;
}
index = Int32.Parse(node.Attributes[0].Value);
hasChildren = Boolean.Parse(node.Attributes[3].Value);
w.WriteStartElement("li");
if (hasChildren)
{
w.WriteStartAttribute("class");
w.WriteValue("liClosed");
}
else
{
w.WriteStartAttribute("class");
w.WriteValue("liBullet");
}
w.WriteStartElement("span");
w.WriteStartAttribute("class");
w.WriteValue("index");
w.WriteEndAttribute();
w.WriteValue(index.ToString());
w.WriteEndElement();
w.WriteStartElement("span");
w.WriteStartAttribute("class");
w.WriteValue("bullet");
w.WriteEndAttribute();
w.WriteValue(node.InnerText);
w.WriteEndElement();
w.WriteEndElement();
}
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.
<ul>
<li>
<ul>
<li>....../<li>
</ul>
</li>
</ul>
The code:
if ( n.className == 'liClosed')
{
n.onclick = function() {
log.Log("classname inside HierFilterCB " + n.className );
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');
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("spans " + 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.
DE.util.ChildItemCB = function( child ) {
this.success = function(val){
log.Log("child className " + 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.
if ( n.className == 'liClosed')
{
log.Log("n "+ n.innerText);
log.Log("n.className " + 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:
<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.