The article submission wizard is messing around with my Code Dowload link, so here is the code in GitHub:
https://github.com/bradykelly/bootstrap-folder-browser.git
Please ignore the FancyTree project in the source. That is an article in early progress and will feature here very soon.
Introduction
A short while ago, I had a coding requirement that my usual box of tricks couldn’t meet: a file system folder browser for a web application. The user needs to select a folder for backup to another location.
I looked around at several file/folder browser widgets I have used, or at least seen, before. JQuery and family have plenty, and one that deserves honourable mention is jQuery FancyTree
. It’s almost a pity I won’t be covering that one yet, but for now, I will focus on my Bootstrap based folder browser. I chose the Bootstrap Treeview product because it is simple and easy to use, as well as it already being styled like the rest of my project, the “Twitter Bootstrap default look”. It is, of course, also free and open source.
My adaptation of this widget for my folder browser is far from feature perfect and has some drawbacks, but it was very quick and easy to implement, it works, and it allows me to choose a folder, the only real requirement here. This part of the article covers setting up a treeview
to show file system data. At the end of this part, you should be able to create a web page on which people can browse folders. Once the important stuff is out of the way, in Part 2, I will show you how to package up all this cool into a new Folder Browser widget, with more features and more reusability.
Figure 1 - Example Folder Browsing
First Steps in Setting Up the Bootstrap Treeview
Include the Treeview in the Project
This is as simple as downloading or installing Bootstrap Treeview and referencing scripts and styles in your view (or layout page). The package is available via npm and bower, or you can clone or download the whole repo from github. My problem with npm is that it just downloads the package and adds it to the solution’s node_modules folder, and doesn’t install any files into the project itself. If I must go and pick and copy files from node_modules, I may as well download the whole repo and pick and choose my files from there. The files I do choose and copy, I place, ready for serving, in the wwwroot\lib\bootstrap-treeview folder.
Style and Script References
My style references for this project, for the Development environment, look like below. I always try and use non-minified files for development, but there is no non-minified file is this case:
<environment include="Development">
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.css" />
<link rel="stylesheet" href="~/css/site.css" />
<link href="~/lib/bootstrap-treeview/bootstrap-treeview.min.css" rel="stylesheet" />
</environment>
In a non-development environment, I use the minified versions of all sheets, where available.
My development script
references look like this:
<environment include="Development">
<script src="~/lib/jquery/dist/jquery.js"></script>
<script src="~/lib/bootstrap/dist/js/bootstrap.js"></script>
<script src="~/lib/bootstrap-treeview/bootstrap-treeview.min.js"></script>
</environment>
I also only normally use non-minified scripts for development, except when not available, like here.
The Container View and Script
Place a container and script on your view, ideally a div
, in which the Treeview
must appear:
<div id="tree"></div>
function getTree() {
// Some logic to retrieve, or generate tree structure
return data;
}
$('#tree').treeview({data: getTree()});
Getting Data
The Bootstrap Treeview is very simplistic but tricky. It requires the tree graph data to be ready before you initialize the Treeview
. The most basic data structure for one node has only two properties: Text is the name of a folder, and Nodes is a collection of node structures for the folder’s children.
The fact that we must have the data before initializing the widget means we can do the initialization of the treeview
in the done
jQuery Ajax method (or equivalent):
The Ajax Callback
$.ajax("@Url.Action("TreeData","TreeView")")
.done(function(resp) {
$("#bsTree").treeview({
data: resp
});
})
.fail(function(error) {
console.log(error);
});
The Structure
My simplified node class, TreeNode
, is designed to represent a tree node, and is a very small subset of the full specification for a node
object, as seen on the home page for the widget, under the heading Data Structure. There are properties for icons, but I was not able to get anything but ‘+’ and ‘-‘ on expanded and collapsed nodes. No other icons are visible on the online examples either, so I chose to omit the icon properties totally, to keep things as small and tidy as possible.
TreeNode Class
public class TreeNode
{
[JsonProperty("text")]
public string Text { get; set; }
[JsonProperty("nodes")]
public List<TreeNode> Nodes { get; set; } = new List<TreeNode>();
}
Using this node structure, a JSON reply to the Treeview
might look something like this:
JSON Example
[
{
"text": "Folder1",
"nodes": []
},
{
"text": "Folder2",
"nodes": [
{
"text": "FolderB",
"nodes": [
{
"text": "FolderOne",
"nodes": []
}
]
}
]
},
{
"text": "Logs",
"nodes": []
}
]
Recursion
This treeview
requires a recursive data structure (JSON) to represent a whole folder tree as the widget does not support lazy loading, or any Ajax beyond the initial load. It, therefore, needs all the data at once. I find this unnerving, because nesting is rife on a filesystem
, to who knows what depth, and building and transferring huge JSON documents only hurts performance.
I achieved the required recursion using the following controller code:
The Controller
public class TreeViewController : Controller
{
private FileTreeConfig _config;
public TreeViewController(IOptions<FileTreeConfig> config)
{
_config = config.Value;
}
public IActionResult TreeData(string dir = "")
{
var browsingRoot = Path.Combine(_config.BaseDir, dir);
var nodes = new List<TreeNode>();
nodes.AddRange(RecurseDirectory(browsingRoot));
return Json(nodes);
}
private List<TreeNode> RecurseDirectory(string directory)
{
var ret = new List<TreeNode>();
var dirInfo = new DirectoryInfo(directory);
try
{
var directories = dirInfo.GetDirectories("*", SearchOption.TopDirectoryOnly);
foreach (var dir in directories)
{
if (dir.FullName.ToLower() == dirInfo.FullName)
{
continue;
}
var thisNode = TreeNode.FromDirInfo(dir);
thisNode.Nodes.AddRange(RecurseDirectory(dir.FullName));
ret.Add(thisNode);
}
}
catch (UnauthorizedAccessException ux)
{
}
return ret;
}
}
I think the above code is readable, given we know its exact purpose, so no more explanation is required.
The Browsing Root
Especially with this treeview
, that only works on recursion, we don’t want the user navigating to the file system root, and have our poor code recurse all that. It seems ideal to have a browsing root, set to a file system folder, that the user cannot browse beyond. They can, using relative paths, but the complexity of checking these paths is beyond the scope of this simple exercise. We can only feel sorry for the poor soul that dreams up a real string
of “..\..\..” type paths.
I have a config setting called BaseDir
, which is the path to where the root of the treeview
must begin. The Path.Combine
in the controller code helps establish this as the root for the treeview
.
Conclusion
If you have followed along this article together with the official documentation for the Bootstrap Treeview
, you should easily be able to put together, if not a real browser widget, at least a page (e.g. Figure 1- Example Folder Browsing) that can browse folders. In the second and last part of this article, I will show you how to use your code to build a proper, reusable widget that can be plugged in anywhere you need folder picking or browsing.
History
This is the first public draft for Part 1 of 2.