Asynchronous TreeView in ASP.NET MVC

Today I'm going to show how to create a completely asynchronous TreeView in ASP.NET MVC. To achieve this I will use jQuery TreeView plugin from bassistance.de. General purpose of this plugin is converting unordered list into an expandable and collapsible tree. I'm not going to write about converting static list, because that is really straightforward. I will focus on asynchronous features of this plugin.
We will start by referencing necessary files:
<head>
  <link href="../../Content/Site.css" rel="stylesheet" type="text/css" />
  <link href="../../Content/jquery.treeview.css" rel="stylesheet" type="text/css" />
  <script src="/Scripts/jquery-1.3.2.min.js" type="text/javascript"></script>
  <script src="/Scripts/jquery.treeview.min.js" type="text/javascript"></script>
  <script src="/Scripts/jquery.treeview.async.js" type="text/javascript"></script>
</head>

Now we need to add placeholder (an empty unordered list) for TreeView:
<ul id="trFileBrowser"></ul>
After that we can write initialization javascript:
<script type="text/javascript">
  $(document).ready(function() {
    $('#trFileBrowser').treeview({
      url: '/Home/FileBrowserData/'
    })
  });
</script>

Let's move on to controller action. TreeView makes a GET request with one parameter: root.
public ActionResult FileBrowserData(string root)
{
}

If the request comes from empty TreeView root has a constant value 'source', otherwise it contains id of expanding node:
public ActionResult FileBrowserData(string root)
{
  DirectoryInfo rootDirectory = null;
  if (root == "source")
    rootDirectory = new DirectoryInfo(@"D:\Books\");
  else
    rootDirectory = new DirectoryInfo(root);
  var directoryChildren = from child in rootDirectory.GetFileSystemInfos()
                          orderby child is DirectoryInfo descending
                          select child;
}

In the response we should return an array of objects representing TreeView nodes. I have prepared a TreeViewNode class (it serves better for presentation purposes) but you are free to use an anonymous type with only those properties which you need (basically only the text property is required):
/// <summary>
/// jQuery TreeView plugin node
/// </summary>
public class TreeViewNode
{
  ...

  #region Properties
  /// <summary>
  /// Node identifier
  /// </summary>
  public string id
  {
    ...
  }

  /// <summary>
  /// Node text
  /// </summary>
  public string text
  {
    ...
  }

  /// <summary>
  /// Whether node is expanded
  /// </summary>
  public bool expanded
  {
    ...
  }

  /// <summary>
  /// Whether node has children
  /// </summary>
  public bool hasChildren
  {
    ...
  }

  /// <summary>
  /// CSS classes for node
  /// </summary>
  public string classes
  {
    ...
  }

  /// <summary>
  /// Node children's
  /// </summary>
  public TreeViewNode[] children
  {
    ...
  }
  #endregion
}

We can finish our controller action:
public ActionResult FileBrowserData(string root)
{
  DirectoryInfo rootDirectory = null;
  if (root == "source")
    rootDirectory = new DirectoryInfo(@"D:\Books\");
  else
    rootDirectory = new DirectoryInfo(root);
  var directoryChildren = from child in rootDirectory.GetFileSystemInfos()
                          orderby child is DirectoryInfo descending
                          select child;

  List nodes = new List();
  foreach (FileSystemInfo directoryChild in directoryChildren)
  {
    bool isDirectory = directoryChild is DirectoryInfo;
    nodes.Add(new TreeViewNode() {
      id = directoryChild.FullName,
      text = directoryChild.Name,
      classes = isDirectory ? "folder" : "file",
      hasChildren = isDirectory
    });
  }

  return Json(nodes.ToArray());
}

Once again you can take a look at my Books directory:

Source code can be downloaded here

12 comments:

Anonymous said...

I am reading this article second time today, you have to be more careful with content leakers. If I will fount it again I will send you a link

Bengt said...

This is bloody awesome - thanks, dude! :-)

ritesh said...

Hi all, i require a little help. After the async tree is loaded when i click a node to expand i find that the request to the server goes for all the nodes equivalent to that and the children of all the parallel nodes are populated.Like if i have 5 nodes in parallel and i click 1 node then the request hits the server 5 times with different id and populates the data for all nodes at once which creates performance issue
is there any way that the node i click the request goes to the server only for that node?
any help appreciated...

Tomasz Pęczek said...

Hi, I'm away at them moment (participating in MIX10 to be exact), but I will be happy to help when I'm back. Can you provide your markup and action method for testing?

ritesh said...

Hi Tomasz,
Following is the action that i call for treeview. After the condition "if(root=="source")" suppose there are five expandable nodes populated, then clicking on any of them hits the condition "else if(jrnlname.Contains(root))" five times.
Further investigating i found that this problem arises when i set the tree property "unique" = true
so is there a way that i keep unique = true and the hit to the server goes only for that node...

public ActionResult TreeData(string root)
{
if (root == "source")
{
var jrnlcontent = from s in jrnlmth.getjournals()
select s;
foreach (var item in jrnlcontent)
{
nodes.Add(new TreeViewNode()
{
text = item.jname,
id = item.jname,
hasChildren = item.yearslist == null ? false : true,

});
}

}
else if (jrnlname.Contains(root))
{
var decadecontent = from s in jrnlmth.getYears()
select s;
foreach (var item in decadecontent)
{
nodes.Add(new TreeViewNode()
{
text = item.range,
id = item.range,
hasChildren = item.yrlist == null ? false : true

});
}
}
else if (decadename.Contains(root))
{
var yrcontent = from y in jrnlmth.getYear()
select y;
foreach (var item in yrcontent)
{
nodes.Add(new TreeViewNode()
{
text = item.name,
id = item.name,
hasChildren = item.sercodes == null ? false : true
});
}
}
else if (yearname.Contains(root))
{
var sercodecontent = from ser in jrnlmth.getSercode()
select ser;
foreach (var item in sercodecontent)
{
nodes.Add(new TreeViewNode()
{
text = item.name,
id = item.text,
hasChildren = false
});
}
}

return Json(nodes.ToArray());

}

Tomasz Pęczek said...

I have investigated your case and I have two informations for you: a good one and a bad one. The good is that your code is perfectly fine. The bad one is that the problem lies in jQuery TreeView plugin. When you use 'unique:true', plugin performs requests for sub nodes of all nodes on current level... The problem appears in any technology (PHP, ASP.NET etc.) and I wasn't able to find a solution for it (as many others). I must apologize to you, but I can't help you with this one.

Anonymous said...

I love this game :)

J. Boers said...

Thank you very much; this example is exactly what I was looking for.

Telvin said...

Thanks you so much. Your solution is helpful for me.

sheir said...

Hello,
I am new to MVC/jQuery and thought your sample is awesome!

I would like to know how in MVC3, can I have your treeview popup in a dialog when user clicks on a button (ie "Browse") and when they say double-click on a leaf, I get that leaf value in a textbox and the dialog goes away.

Please understand that I am a complete newbie to MVC/jQuery.

Thanks.

kiran said...

can u provide me crud treeview menu code using mvctree?

Masud Parvez said...

Dear Brother,

Could you please provide me some help on how to click event will work on tree view