Table of contents
- Introduction
- Building an MVC App using Entity Framework
- RESTful service within an ASP.NET MVC
- MVC
- Model
- View
- Home/generateRoot
- Home/Index
js/tree.js (Most important part of the article)
- load Dojo module
- treeStore
- tree
- theme
- Buttons
- add-new-child
- remove-child
- rename
- reloadNode
- removeAllChildren
- Controller
- TreeController
- HomeController
- See in action
- 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; }
}
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">
<!---->
<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) {
return "children" in object;
},
getChildren: function (object, onComplete, onError) {
this.get(object.id).then(function (fullObject) {
object.children = fullObject.children;
onComplete(fullObject.children);
}, function (error) {
console.error(error);
onComplete([]);
});
},
getRoot: function (onItem, onError) {
this.get("1").then(onItem, function (error) {
alert("Error loading Root");
});
},
getLabel: function (object) {
return object.NodeName;
},
pasteItem: function (child, oldParent, newParent, bCopy, insertIndex) {
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; }
});
store.put(oldParent);
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");
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");
}
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);
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);
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) {
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();
[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 =
"" }
};
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)
{
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.
As you could see in fireBug data will send or request throw Json REST.
References
- The official Dojo Toolkit site. You can get a copy of Dojo as well as API documentation here:
http://www.dojotoolkit.org
- Connecting a Store to a Tree:
http://dojotoolkit.org/documentation/tutorials/1.7/store_driven_tree/
- Deferred.when/dojo.when:
http://dojotoolkit.org/reference-guide/dojo/when.html
- dojox.form.BusyButton:
http://dojotoolkit.org/reference-guide/dojox/form/BusyButton.html
- Building an MVC 3 App with Model First and Entity Framework 4.1:
http://msdn.microsoft.com/en-us/data/gg685494
- Build a RESTful API architecture within an ASP.NET MVC 3 application:
http://iwantmymvc.com/rest-service-mvc3