Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Tree View with CRUD operations, drag and drop (DnD) and Lazy Loading using Dojo Tree, Entity Framework, SQL Server, ASP.NET MVC

0.00/5 (No votes)
5 Mar 2012 1  
This article shows how you can make a Tree with hierarchical data supporting CRUD operations, drag and drop (DnD) and Lazy Loading using Dojo store driven Tree, Entity Framework, SQL Server, ASP.NET MVC.
Demo

Table of contents

  1. Introduction
  2. Building an MVC App using Entity Framework
  3. RESTful service within an ASP.NET MVC
  4. MVC
    1. Model
    2. View
      1. Home/generateRoot
      2. Home/Index
        js/tree.js (Most important part of the article)
        1. load Dojo module
        2. treeStore
        3. tree
        4. theme
        5. Buttons
        6. add-new-child
        7. remove-child
        8. rename
        9. reloadNode
        10. removeAllChildren
    3. Controller
      1. TreeController
      2. HomeController
  5. See in action
  6. References

Introduction

Dojo Toolkit is an open source modular JavaScript library (or more specifically JavaScript toolkit) designed to ease the rapid development of cross-platform, JavaScript/Ajax-based applications and web sites and provides some really powerful user interface features (Dojo Toolkit). The Dojo Tree component provides a comprehensive, familiar, intuitive drill-down presentation of hierarchical data. The Tree supports lazy loading of branches, making it highly scalable for large data sets. The Tree is a great widget to use when data has parent-child relationships. The Dojo Tree component is a powerful tool for visual presentation of hierarchical data (Tree Demo).

This article walks-through the process of creating a Tree supporting "CRUD operations" , "drag and drop (DnD)" and "Lazy Loading". For making this kind of tree, we will use Dojo Tree, Entity Framework, SQL Server and Asp .Net MVC.

Building an MVC App using Entity Framework

This example uses Entity Framework Model First approach. But this isn't the point, you could also use Entity Framework Code First or Database First. Julie Lerman has a good article about "Building an MVC 3 App with Model First and Entity Framework 4.1" here. You could use the article until you’ve got your model, your class and your database in place, nothing more. We will make our controllers and views. Your model should be something like this:

RESTful Service within an ASP.NET MVC

As Dojo JsonRest Store sends and receives JSON data to perform CRUD operations on the entities so we need RESTful service within an ASP.NET MVC 3. You could find a good article about "Build a RESTful API architecture within an ASP.NET MVC 3 application" wrote by Justin Schwartzenberger at http://iwantmymvc.com/rest-service-mvc3. We won't use all of it, but I used parts of article ideas.

First we need a custom ActionFilterAttribute that we can craft to help us handle multiple verbs through a single controller action. Make a class (RestHttpVerbFilter.cs) at Model folder with code below:

using System.Web.Mvc;

namespace DojoTree.Models
{
    public class RestHttpVerbFilter : ActionFilterAttribute
    {
        public override void OnActionExecuting(ActionExecutingContext filterContext)
        {
            var httpMethod = filterContext.HttpContext.Request.HttpMethod;
            filterContext.ActionParameters["httpVerb"] = httpMethod;
            base.OnActionExecuting(filterContext);
        }
    }
}

"This code will capture the HTTP verb of the request and store it in the ActionParameters collection. By applying this attribute to a controller action, we can add a method parameter named httpVerb and the RestHttpVerbFilter will handle binding the HTTP request verb value to it. Our controller needs to support an action method with a common signature (the same parameters) but take different actions based on the HTTP verb. It is not possible to override a method with the same parameter signature but a different HTTP verb attribute. This custom attribute will allow us to have a single controller action method that can take action based on the HTTP verb without having to contain the logic to determine the verb." [6]

Model

A class or Model that contains information about Nodes needs to be added in the example. The class and the model are shown in the following listing:

   public partial class Node
    {
        public int Id { get; set; }
        public int ParentId { get; set; }
        public string NodeName { get; set; }
    } 

Model

TreeData

View

For adding a link to Generate Root, you should edit menu part of "_Layout.cshtml" like this:

	<ul id="menu">
		<li>@Html.ActionLink("Home", "Index", "Home")</li>
		<li>@Html.ActionLink("Generate Root", "generateRoot", "Home")</li>
	</ul>

Home/generateRoot View

Make a view for generateRoot action. It should be like this:

@{
    ViewBag.Title = "generateRoot";
}
<h2>@ViewBag.Message</h2>

Home/Index View

Home/Index View should contain all the codes below:

@{
    ViewBag.Title = "Dojo Tree";
}
<h2>@ViewBag.Message</h2>

<link rel="stylesheet" 
href="http://ajax.googleapis.com/ajax/libs/dojo/1.7.1/dojo/resources/dojo.css">
<link rel="stylesheet" 
href="http://ajax.googleapis.com/ajax/libs/dojo/1.7.1/dijit/themes/claro/claro.css">
<!-- load dojo and provide config via data attribute -->
<script src="http://ajax.googleapis.com/ajax/libs/dojo/1.7.1/dojo/dojo.js"
           data-dojo-config="async: true, isDebug: true, parseOnLoad: true"></script>

<script src="/js/tree.js" type="text/javascript"></script>

<div style=" width: 400px; margin: 10px;">
    <div id="tree"></div>
</div>
<div id="add-new-child"></div>
<div id="remove-child"></div>

You could see a complete article about a part of the code above and below in http://dojotoolkit.org/documentation/tutorials/1.7/store_driven_tree/ .

As you can see in the code above, we have a link to js/tree.js that its content comes in code below.

js/tree.js

tree.js contains some section:

This part of script loads Dojo modules that we need in this sample:
require(["dojo/store/JsonRest",
            "dojo/store/Observable",
            "dojo/_base/Deferred",
            "dijit/Tree",
            "dijit/tree/dndSource",
            "dojox/form/BusyButton",
            "dojo/query",
            "dojo/domReady!"], function 
		(JsonRest, Observable, Deferred, Tree, dndSource, BusyButton, query) {

This part of script makes a treeStore to connect TreeController by "target: "/tree/data/"".

  • mayHaveChildren sees if it has a children property
  • getChildren retrieves the full copy of the object
  • getRoot gets the root object, we will do a get() and callback the result. In this example, our root id is 1
  • getLabel just gets the name
  • pasteItem is used for drag and drop action and changes parentId of moved node
  • put forces store to connect to server on any changes
treeStore = JsonRest({
    target: "/tree/data/",
    mayHaveChildren: function (object) {
        // see if it has a children property
        return "children" in object;
    },
    getChildren: function (object, onComplete, onError) {
        // retrieve the full copy of the object
        this.get(object.id).then(function (fullObject) {
            // copy to the original object so it has the children array as well.
            object.children = fullObject.children;
            // now that full object, we should have an array of children
            onComplete(fullObject.children);
        }, function (error) {
            // an error occurred, log it, and indicate no children
            console.error(error);
            onComplete([]);
        });
    },
    getRoot: function (onItem, onError) {
        // get the root object, we will do a get() and callback the result
        this.get("1").then(onItem, function (error) {
            alert("Error loading Root");
        });
    },
    getLabel: function (object) {
        // just get the name
        return object.NodeName;
    },
    pasteItem: function (child, oldParent, newParent, bCopy, insertIndex) {
    
        // This will prevent to add a child to its parent again.
        if (child.ParentId == newParent.id) { return false; }
        
        var store = this;
        store.get(oldParent.id).then(function (oldParent) {
            store.get(newParent.id).then(function (newParent) {
                store.get(child.id).then(function (child) {
                    var oldChildren = oldParent.children;
                    dojo.some(oldChildren, function (oldChild, i) {
                        if (oldChild.id == child.id) {
                            oldChildren.splice(i, 1);
                            return true; // done
                        }
                    });
                    
                    store.put(oldParent);
                    
                    //This will change the parent of the moved Node
                    child.ParentId = newParent.id;
                    store.put(child);
                    
                    newParent.children.splice(insertIndex || 0, 0, child);
                    
                    store.put(newParent);
                    
                }, function (error) {
                    alert("Error loading " + child.NodeName);
                });
            }, function (error) {
                alert("Error loading " + newParent.NodeName);
            });
        }, function (error) {
            alert("Error loading " + oldParent.NodeName);
        });
    },
    put: function (object, options) {
        this.onChildrenChange(object, object.children);
        this.onChange(object);
        return JsonRest.prototype.put.apply(this, arguments);
    }
});

This part of script define a Dojo Tree and connects it to <div id="tree"></div> and treeStore then starts it up.

tree = new Tree({
    model: treeStore,
    dndController: dndSource
}, "tree"); // make sure you have a target HTML element with this id

tree.startup();

This part of script adds claro theme to page.

dojo.query("body").addClass("claro");

This part of script defines two BusyButton: addNewChildButton and removeChildButton.
You could find a complete document about BusyButton here.

var addNewChildButton = new BusyButton({
    id: "add-new-child",
    busyLabel: "Wait a moment...",
    label: "Add new child to selected item",
    timeout: 500
}, "add-new-child");

var removeChildButton = new BusyButton({
    id: "remove-child",
    busyLabel: "Wait a moment...",
    label: "Remove selected item",
    timeout: 500
}, "remove-child");

This part of script defines click action for add-new-child button. First, it checks whether the user has selected an item or not. Then it syncs selectedObject with server and if everything was fine, it prompts for a name. Then it defines newItem and pushes it as selectedObject children then sends it to server treeStore.put(newItem); After 500 ms, it reloads the selectedObject to getting the id of recently added child. For reloading after 500 ms, we use "Deferred.when/dojo.when" a document about it can be found here.

query("#add-new-child").on("click", function () {

    var selectedObject = tree.get("selectedItems")[0];

    if (!selectedObject) {
        return alert("No object selected");
    }

    //Sync selectedObject with server
    treeStore.get(selectedObject.id).then(function (selectedObject) {
        var name = prompt("Enter a name for new node");
        if (name != null && name != "") {

            var newItem = { NodeName: name, ParentId: selectedObject.id, children: "" };

            selectedObject.children.push(newItem);

            treeStore.put(newItem);

            //Loading recently added node 500ms after puting it
            var nodeId = new Deferred();
            Deferred.when(nodeId, reloadNode);
            setTimeout(function () {
                nodeId.resolve(selectedObject.id);
            }, 500);

        } else { return alert("Name can not be empty."); }

    }, function (error) {
        alert("Error loading " + selectedObject.NodeName);
    });
});

This part of script defines click action for remove-child button. First it checks that user has selected an item or not or selected item isn't root. Then it asks "Are you sure you want to permanently delete this node and all its children?". If yes, then sync selectedObject with server and if everything was fine, it will call removeAllChildren(selectedObject); that removes the selected item and all of its children. After 500 ms, it reloads the parent of selectedObject (selectedObject.ParentId) syncing the tree and server.

query("#remove-child").on("click", function () {

    var selectedObject = tree.get("selectedItems")[0];

    if (!selectedObject) {
        return alert("No object selected");
    }
    if (selectedObject.id == 1) {
        return alert("Can not remove Root Node");
    }

    var answer = confirm("Are you sure you want to permanently delete 
		this node and all its children?")
    if (answer) {

        treeStore.get(selectedObject.id).then(function (selectedObject) {

            removeAllChildren(selectedObject);

            //Reloading the parent of recently removed node 500ms after removing it
            var ParentId = new Deferred();
            Deferred.when(ParentId, reloadNode);
            setTimeout(function () {
                ParentId.resolve(selectedObject.ParentId);
            }, 500);

        }, function (error) {
            alert("Error loading " + selectedObject.NodeName);
        });
    }
});

This part of script defines dblclick action for tree for renaming a node. First it syncs selectedObject with server and if everything was fine, it prompts for a name. Then sends new name to server treeStore.put(object). If an error occurred, it will revert the parent of selected node.

    tree.on("dblclick", function (object) {

        treeStore.get(object.id).then(function (object) {

            var name = prompt("Enter a new name for the object");
            if (name != null && name != "") {
                object.NodeName = name;

                treeStore.put(object).then(function () {
                }, function (error) {
                    // On Error revert Value
                    reloadNode(object.ParentId);

                    alert("Error renaming " + object.NodeName);
                });

            } else { return alert("Name can not be empty."); }
        }, function (error) {
            alert("Error loading " + object.NodeName);
        });
    }, true);
});

This function will reload the node by id and its children by one level.

function reloadNode(id) {
    treeStore.get(id).then(function (Object) {
        treeStore.put(Object);
    })
};

This function will remove all children of the node recursively.

function removeAllChildren(node) {
    treeStore.get(node.id).then(function (node) {

        var nodeChildren = node.children;
        for (n in nodeChildren) {
            removeAllChildren(nodeChildren[n]);
        }
        treeStore.remove(node.id);
    }, function (error) {
        alert(error);
    });
};

Controller

Now we need to create our controller.

TreeController

Paste the code below in "TreeController.cs":

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Data.Entity;
using DojoTree.Models;
using System.Data;
using System.Net;

namespace DojoTree.Controllers
{
    public class TreeController : Controller
    {
        private TreeModelContainer db = new TreeModelContainer();

        // GET     /Tree/Data/3
        // POST    /Tree/Data
        // PUT     /Tree/Data/3
        // DELETE  /Tree/Data/3
        [RestHttpVerbFilter]
        public JsonResult Data(Node node, string httpVerb, int id = 0)
        {
            switch (httpVerb)
            {
                case "POST":
                    if (ModelState.IsValid)
                    {
                        db.Entry(node).State = EntityState.Added;
                        db.SaveChanges();
                        return Json(node, JsonRequestBehavior.AllowGet);
                    }
                    else
                    {
                        Response.TrySkipIisCustomErrors = true;
                        Response.StatusCode = (int)HttpStatusCode.NotAcceptable;
                        return Json(new { Message = "Data is not Valid." },
                        JsonRequestBehavior.AllowGet);
                    }
                case "PUT":
                    if (ModelState.IsValid)
                    {
                        db.Entry(node).State = EntityState.Modified;
                        db.SaveChanges();
                        return Json(node, JsonRequestBehavior.AllowGet);
                    }
                    else
                    {
                        Response.TrySkipIisCustomErrors = true;
                        Response.StatusCode = (int)HttpStatusCode.NotAcceptable;
                        return Json(new { Message = "Node " + id + "
                        Data is not Valid." }, JsonRequestBehavior.AllowGet);
                    }
                case "GET":
                    try
                    {
                        var node_ = from entity in db.Nodes.Where(x => x.Id.Equals(id))
                                select new
                                       {
                                           id = entity.Id,
                                           NodeName = entity.NodeName,
                                           ParentId = entity.ParentId,
                                           children = from entity1 in db.Nodes.Where
                                           (y => y.ParentId.Equals(entity.Id))
                                                      select new
                                                      {
                                                          id = entity1.Id,
                                                          NodeName = entity1.NodeName,
                                                          ParentId = entity1.ParentId,
                                                          children =
                                                          "" // it calls checking children 
                                                             // whenever needed
                                                      }
                                       };

                        var r = node_.First();
                        return Json(r, JsonRequestBehavior.AllowGet);
                    }
                    catch
                    {
                        Response.TrySkipIisCustomErrors = true;
                        Response.StatusCode = (int)HttpStatusCode.NotAcceptable;
                        return Json(new { Message = "Node " + id +
                        " does not exist." }, JsonRequestBehavior.AllowGet);
                    }
                case "DELETE":
                    try
                    {
                        node = db.Nodes.Single(x => x.Id == id);
                        db.Nodes.Remove(node);
                        db.SaveChanges();
                        return Json(node, JsonRequestBehavior.AllowGet);
                    }
                    catch
                    {
                        Response.TrySkipIisCustomErrors = true;
                        Response.StatusCode = (int)HttpStatusCode.NotAcceptable;
                        return Json(new { Message =
                        "Could not delete Node " + id }, JsonRequestBehavior.AllowGet);
                    }
            }
            return Json(new { Error = true,
            Message = "Unknown HTTP verb" }, JsonRequestBehavior.AllowGet);
        }
    }
}

As you can see, the TreeController performs "GET/POST/PUT/DELETE" in a single URL "/Tree/Data/" and it could do that because of RestHttpVerbFilter.

  • POST used to add new node
  • PUT used to edit a node
  • GET used to get node data and its children by just one level. This will help lazy loading
  • DELETE used to delete a node

HomeController

I edited the HomeController just for adding an action to generate Root. You should make your HomeController like this:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using DojoTree.Models;

namespace DojoTree.Controllers
{
    public class HomeController : Controller
    {
        public ActionResult Index()
        {
            ViewBag.Message = "Tree supporting CRUD operations Using Dojo Tree,
            Entity Framework, Asp .Net MVC";
            return View();
        }

        public ActionResult generateRoot()
        {
            try
            {
                TreeModelContainer db = new TreeModelContainer();
                Node node = new Node();

                node= db.Nodes.Find(1);
                if (node == null)
                {
                    //If you deleted Root manually, this couldn't make Root again
                    //because Root Id must be "1", so you must drop the 
                    //Tree table and rebuild it
                    //or change the Root Id in "tree.js"

                    Node rootNode = new Node();
                    rootNode.NodeName = "Root";
                    rootNode.ParentId = 0;
                    db.Nodes.Add(rootNode);

                    db.SaveChanges();
                    ViewBag.Message = "Some Nodes have been generated";
                }
                else { ViewBag.Message = "Root Exists."; }
            }
            catch { ViewBag.Message = "An Error occurred"; }
            return View();
        }
    }
}

See in Action

Now it's time to see the result. Build the solution and Click on Generate Root then Add | Rename | Drag and Drop | Remove some nodes.

TreeTest

As you could see in fireBug data will send or request throw Json REST.

References

  1. The official Dojo Toolkit site. You can get a copy of Dojo as well as API documentation here:
    http://www.dojotoolkit.org
  2. Connecting a Store to a Tree:
    http://dojotoolkit.org/documentation/tutorials/1.7/store_driven_tree/
  3. Deferred.when/dojo.when:
    http://dojotoolkit.org/reference-guide/dojo/when.html
  4. dojox.form.BusyButton:
    http://dojotoolkit.org/reference-guide/dojox/form/BusyButton.html
  5. Building an MVC 3 App with Model First and Entity Framework 4.1:
    http://msdn.microsoft.com/en-us/data/gg685494
  6. Build a RESTful API architecture within an ASP.NET MVC 3 application:
    http://iwantmymvc.com/rest-service-mvc3

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here