Aug
16
Written by:
Michael Washington
8/16/2015 6:10 PM
We can create a database driven tree with unlimited levels of nesting of child nodes in the LightSwitch HTML Client.
A Tree With Unlimited Child Nodes
If you have an application that only needs one level of nesting of entities, the following article may provide what you need: JQuery Mobile Tree Using Collapsible Sections and Dynamic Views in LightSwitch.
However, as covered in Rethinking The Help Desk:
“Unlimited levels of nested categories allow for hundreds of categories to be easily grouped and understood.”
This example demonstrates how this can be achieved in LightSwitch.
Walk-thru
When you run the application, the collapsible nodes are displayed.
Clicking on a node allows you to edit that node.
Clicking the New Node button allows you to create a new node.
When you create a new node, if you do not select a parent, it will be a root node. Otherwise, you can click the “+” icon on the search box to select a parent node.
You can also change the parent node to automatically move the selected node and any child nodes that it has.
You can also delete the selected node, however you must first delete any child nodes that it has first.
The Data
The data structure is simple. We have a single table called Node.
It has a single self-referencing relationship that allows us to associate a node to a parent.
This is important to understand, we don’t associate child nodes to a parent node, we associate a node to a single parent. This results in a collection of nodes that may have child nodes.
We can then right-click on the Screens folder in the HTML Client project and select Add Screen…
We can create screens to allow us to edit the data.
We use the screens to add sample data.
jqTree
We will use the jQuery widget, jqTree to display the tree in the LightSwitch application.
To set it up, we add the jqtree.css and the tree.jquery.js to the LightSwitch HTML Client project, and add a reference to them in the default.htm file.
Providing Data to jqTree Using a Generic File Handler
To provide the data from the Node table to the jqTree control, we add a Generic File Handler to the Server project of the LightSwitch solution.
First we create two classes. DataNode to hold the data from the Node table, and TreeNode to contain the nested collection of nodes that will be provided to the jqTree control:
#region Classes
public class DataNode
{
public int Id { get; set; }
public string NameName { get; set; }
public int? NodeParentId { get; set; }
}
public class TreeNode
{
public int Id { get; set; }
public string label { get; set; }
public List<TreeNode> children { get; set; }
}
#endregion
ProcessRequest is the main method that gets the data and create the root nodes.
It calls the AddChildren method to populate the child nodes of each root node:
public void ProcessRequest(HttpContext context)
{
using (var serverContext = ServerApplicationContext.CreateContext())
{
// Collection to hold final TreeNodes
List<TreeNode> colTreeNodes = new List<TreeNode>();
// Get all the Nodes from the database
var colNodes = (from objNode in serverContext.DataWorkspace
.ApplicationData.Nodes
select new DataNode
{
Id = objNode.Id,
NameName = objNode.NameName,
NodeParentId = objNode.NodeParent.Id
}).Execute().ToList();
// Loop through Parent nodes
foreach (DataNode objNode in colNodes
.Where(x => x.NodeParentId == null))
{
// Create a new Node
TreeNode objNewNode = new TreeNode();
objNewNode.Id = objNode.Id;
objNewNode.label = objNode.NameName;
objNewNode.children = new List<TreeNode>();
colTreeNodes.Add(objNewNode);
// Add Child Nodes
AddChildren(colNodes, colTreeNodes, objNewNode);
}
// Create JavaScriptSerializer
JavaScriptSerializer jsonSerializer = new JavaScriptSerializer();
// Output as JSON
context.Response.Write(jsonSerializer.Serialize(colTreeNodes));
}
}
The AddChildren method is called recursively to populate the child nodes of each node being currently processed:
private void AddChildren(
List<DataNode> colNodeItemCollection,
List<TreeNode> colTreeNodeCollection,
TreeNode paramTreeNode)
{
// Get the children of the current item
// This method may be called from the top level
// or recuresively by one of the child items
var ChildResults = from objNode in colNodeItemCollection
where objNode.NodeParentId == paramTreeNode.Id
select objNode;
// Loop thru each Child of the current Node
foreach (var objChild in ChildResults)
{
// Create a new Node
var objNewNode = new TreeNode();
objNewNode.Id = objChild.Id;
objNewNode.label = objChild.NameName;
objNewNode.children = new List<TreeNode>();
// Search for the Node in colTreeNodeCollection
// By looping through each root Node
foreach (DataNode objNode in colNodeItemCollection
.Where(x => x.NodeParentId == null))
{
// See if Parent is in the colTreeNodeCollection
TreeNode objParent =
colTreeNodeCollection.Where(x => x.Id == objNode.Id).FirstOrDefault();
if (objParent != null) // Parent exists in the colTreeNodeCollection
{
// Get the Parent Node for the current Child Node
TreeNode objParentTreeNode = objParent.Descendants()
.Where(x => x.Id == paramTreeNode.Id).FirstOrDefault();
if (objParentTreeNode != null)
{
// Add the Child node to the Parent
objParentTreeNode.children.Add(objNewNode);
}
}
}
//Recursively call the AddChildren method adding all children
AddChildren(colNodeItemCollection, colTreeNodeCollection, objNewNode);
}
}
The Descendants linq extension allows for the deep searching of child nodes:
public static class Extensions
{
public static IEnumerable<TreeNode> Descendants(this TreeNode root)
{
var nodes = new Stack<TreeNode>(new[] { root });
while (nodes.Any())
{
TreeNode node = nodes.Pop();
yield return node;
foreach (var n in node.children) nodes.Push(n);
}
}
}
Display and Edit the Tree
The final step is to consume the data and bind it to the jqTree control.
First, we edit the JavaScript file that is attached to the screen:
// Create a Div to contain the tree
var TreeDiv = $("<div></div>");
var objScreen;
myapp.BrowseNodes.created = function (screen) {
// Set a global for screen
objScreen = screen;
};
In the screen designer, we add a Custom Control to the screen.
In the Properties for the control, we select it’s Render method.
We use the following code for the method. This will create an instance of the tree and create methods that will handle editing of the nodes.
The tree is filled with data in the UpdateTree method called at the end:
myapp.BrowseNodes.TreeDiv_render = function (element, contentItem) {
// clear the element
element.innerHTML = "";
// Create the element
var objElement = $(element);
// Append the tree to the element
TreeDiv.appendTo(objElement);
// Create the Tree
TreeDiv.tree({
autoOpen: true,
dragAndDrop: false
}).bind(
'tree.select',
function (event) {
if (event.node) {
// node was selected
var node = event.node;
var NodeId = node.Id;
// Get the Node
myapp.activeDataWorkspace.ApplicationData.Nodes_SingleOrDefault(NodeId)
.execute().then(function (result) {
// Set the selected Node
objScreen.Nodes.selectedItem = result.results[0];
EditSelectedNode();
});
}
else {
// event.node is null
// a node was deselected
// e.previous_node contains the deselected node
}
});
UpdateTree();
};
The UpdateTree method calls the Generic File Handler and populates the tree:
function UpdateTree() {
// Get the data for the Tree
$.getJSON(
'/Web/TreeData.ashx',
function (data) {
TreeDiv.tree('loadData', data);
});
}
When a node is being edited, the following method is called:
function EditSelectedNode() {
myapp.showAddEditNode(null, {
beforeShown: function (addEditScreen) {
addEditScreen.Node = objScreen.Nodes.selectedItem;
},
afterClosed: function (addEditScreen, navigationAction) {
// If the user commits the change, refresh the Tree
if (navigationAction === msls.NavigateBackAction.commit) {
UpdateTree();
}
}
});
}
When a new node is created, the following method is called:
myapp.BrowseNodes.NewNode_execute = function (screen) {
myapp.showAddEditNode(null, {
beforeShown: function (addEditScreen) {
// Create new Node here so that
// discard will work.
var newNode = new myapp.Node();
// If there is a currently selected Node
// set is as the parent node
if (screen.Nodes.selectedItem !== null) {
newNode.NodeParent = screen.Nodes.selectedItem;
}
addEditScreen.Node = newNode;
},
afterClosed: function (addEditScreen, navigationAction) {
// If the user commits the change, refresh the Tree
if (navigationAction === msls.NavigateBackAction.commit) {
UpdateTree();
}
}
});
};
Links
JQuery Mobile Tree Using Collapsible Sections and Dynamic Views in LightSwitch
Creating Advanced LightSwitch HTML Screen Templates
The LightSwitch Data Binding Framework Using EaselJS
How Does A LightSwitch HTML Client Application Work?
HUY Volume II - Visual Studio LightSwitch Advanced JavaScript Examples
Walk-thru Examples of Common Visual Studio LightSwitch JavaScript
Download Code
The LightSwitch project is available at http://lightswitchhelpwebsite.com/Downloads.aspx
You must have Visual Studio 2015 (or higher) with LightSwitch installed to run the code (if you have Visual Studio Community Edition see How To Get Visual Studio LightSwitch For Free)
3 comment(s) so far...
Brilliant!
Thanks Michael for pushing LS forward.
By Johan on
8/17/2015 3:41 AM
|
Thanks Michael, this looks so promising! However, I'm a novice user and have some difficulties converting the generic file handler from c# to vb. Would you have a vb version available too? Thanks!
By MarcPadros on
5/25/2016 4:19 AM
|
@MarcPadros - Sorry no VB samples :(
By Michael Washington on
5/25/2016 4:19 AM
|