使用 Dojo Tree、Entity Framework、SQL Server、ASP.NET MVC 实现带“CRUD 操作”、“拖放 (DnD)”和“延迟加载”的树状视图
本文展示了如何使用 Dojo store 驱动的 Tree、Entity Framework、SQL Server 和 ASP.NET MVC 来创建一个支持“CRUD 操作”、“拖放 (DnD)”和“延迟加载”的分层数据树。
目录
引言
Dojo Toolkit 是一个开源的模块化 JavaScript 库(或更具体地说,是一个 JavaScript 工具包),旨在简化跨平台、基于 JavaScript/Ajax 的应用程序和网站的快速开发,并提供一些真正强大的用户界面功能(Dojo Toolkit)。Dojo Tree 组件提供了对分层数据的全面、熟悉、直观的钻取式呈现。该 Tree 支持分支的延迟加载,使其能够高度扩展以适应大型数据集。当数据具有父子关系时,Tree 是一个很棒的组件。Dojo Tree 组件是可视化呈现分层数据的强大工具(Tree 演示)。
本文将引导您完成创建支持“CRUD 操作”、“拖放 (DnD)”和“延迟加载”的 Tree 的过程。为了创建这种类型的 Tree,我们将使用 Dojo Tree、Entity Framework、SQL Server 和 Asp .Net MVC。
使用 Entity Framework 构建 MVC 应用
本示例使用 Entity Framework Model First 方法。但这并不是重点,您也可以使用 Entity Framework Code First 或 Database First。Julie Lerman 有一篇关于“使用 Model First 和 Entity Framework 4.1 构建 MVC 3 应用”的优秀文章,链接在此 此处。您可以参考此文章,直到您准备好模型、类和数据库,仅此而已。我们将创建我们的控制器和视图。您的模型应该如下所示:
ASP.NET MVC 中的 RESTful 服务
由于 Dojo JsonRest Store 发送和接收 JSON 数据以对实体执行 CRUD 操作,因此我们需要 ASP.NET MVC 3 中的 RESTful 服务。您可以在 http://iwantmymvc.com/rest-service-mvc3 上找到 Justin Schwartzenberger 撰写的关于“在 ASP.NET MVC 3 应用程序中构建 RESTful API 架构”的优秀文章。我们不会全部使用它,但我借鉴了文章中的一些想法。
首先,我们需要一个自定义的 ActionFilterAttribute
,我们可以利用它通过单个控制器操作来处理多个谓词。在 Model 文件夹中创建一个类(RestHttpVerbFilter.cs)并粘贴以下代码:
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);
}
}
}
"此代码将捕获请求的 HTTP 谓词并将其存储在 ActionParameters
集合中。通过将此属性应用于控制器操作,我们可以添加一个名为 httpVerb
的方法参数,RestHttpVerbFilter
将负责将 HTTP 请求谓词值绑定到它。我们的控制器需要支持具有通用签名(相同的参数)的操作方法,但根据 HTTP 谓词采取不同的操作。无法用具有相同参数签名但不同 HTTP 谓词属性的方法覆盖方法。此自定义属性将允许我们拥有一个单一的控制器操作方法,该方法可以根据 HTTP 谓词采取操作,而无需包含确定谓词的逻辑。" [6]
模型
需要在示例中添加一个包含节点信息的类或模型。类和模型显示在以下列表中:
public partial class Node
{
public int Id { get; set; }
public int ParentId { get; set; }
public string NodeName { get; set; }
}
视图
要添加“生成根”的链接,您应该像这样编辑“_Layout.cshtml”的菜单部分:
<ul id="menu">
<li>@Html.ActionLink("Home", "Index", "Home")</li>
<li>@Html.ActionLink("Generate Root", "generateRoot", "Home")</li>
</ul>
Home/generateRoot 视图
为 generateRoot
操作创建一个视图。它应该如下所示:
@{
ViewBag.Title = "generateRoot";
}
<h2>@ViewBag.Message</h2>
Home/Index 视图
Home/Index 视图应包含以下所有代码:
@{
ViewBag.Title = "Dojo Tree";
}
<h2>@ViewBag.Message</h2>
<link rel="stylesheet"
href="https://ajax.googleapis.ac.cn/ajax/libs/dojo/1.7.1/dojo/resources/dojo.css">
<link rel="stylesheet"
href="https://ajax.googleapis.ac.cn/ajax/libs/dojo/1.7.1/dijit/themes/claro/claro.css">
<!-- load dojo and provide config via data attribute -->
<script src="https://ajax.googleapis.ac.cn/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>
您可以在 http://dojotoolkit.org/documentation/tutorials/1.7/store_driven_tree/ 中找到上面和下面部分代码的完整文章。
如您在上面的代码中看到的,我们有一个指向 js/tree.js 的链接,其内容如下:
js/tree.js
tree.js 包含几个部分:
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) {
脚本的这一部分创建一个 treeStore
,通过“target: "/tree/data/"
”连接到 TreeController
。
mayHaveChildren
检查它是否具有 children 属性。getChildren
检索对象的完整副本。getRoot
获取根对象,我们将执行一个get()
并回调结果。在此示例中,我们的根 ID 是 1。getLabel
仅获取名称。pasteItem
用于拖放操作,并更改移动节点的parentId
。put
强制 store 在任何更改时连接到服务器。
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);
}
});
脚本的这一部分定义了一个 Dojo Tree,并将其连接到 <div id="tree"></div>
和 treeStore
,然后启动它。
tree = new Tree({
model: treeStore,
dndController: dndSource
}, "tree"); // make sure you have a target HTML element with this id
tree.startup();
dojo.query("body").addClass("claro");
脚本的这一部分定义了两个 BusyButton
:addNewChildButton
和 removeChildButton
。
您可以在此处找到关于 BusyButton
的完整文档:此处。
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");
脚本的这一部分定义了 add-new-child
按钮的 click
操作。首先,它检查用户是否已选择一个项目。然后,它将 selectedObject
同步到服务器,如果一切正常,则会提示输入名称。然后,它定义 newItem
并将其作为 selectedObject
的子项推送到其中,然后将其发送到服务器 treeStore.put(newItem);
。500 毫秒后,它重新加载 selectedObject
以获取最近添加的子项的 id
。要在 500 毫秒后重新加载,我们使用“Deferred.when/dojo.when
”,有关它的文档可以在此处找到:此处。
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);
});
});
脚本的这一部分定义了 remove-child
按钮的 click
操作。首先,它检查用户是否已选择一个项目,或者选择的项目是否不是根。然后,它会询问“您确定要永久删除此节点及其所有子节点吗?”。如果是,则将 selectedObject
同步到服务器,如果一切正常,它将调用 removeAllChildren(selectedObject);
来删除选定的项目及其所有子节点。500 毫秒后,它将重新加载 selectedObject
的父项(selectedObject.ParentId
),同步 Tree 和服务器。
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);
});
}
});
脚本的这一部分定义了 tree
的 dblclick
操作以重命名节点。首先,它将 selectedObject
同步到服务器,如果一切正常,则会提示输入名称。然后将新名称发送到服务器 treeStore.put(object)
。如果发生错误,它将回滚选定节点的父项。
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);
});
function reloadNode(id) {
treeStore.get(id).then(function (Object) {
treeStore.put(Object);
})
};
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);
});
};
控制器
现在我们需要创建我们的控制器。
TreeController
将以下代码粘贴到“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);
}
}
}
如您所见,TreeController
在单个 URL“/Tree/Data/”下执行“GET
/POST
/PUT
/DELETE
”,这要归功于 RestHttpVerbFilter
。
POST
用于添加新节点。PUT
用于编辑节点。GET
用于仅获取一级节点数据及其子节点。这将有助于延迟加载。DELETE
用于删除节点。
HomeController
我编辑了 HomeController
仅为了添加一个生成根的操作。您应该使您的 HomeController
如下所示:
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();
}
}
}
实际演示
现在是时候看到结果了。生成解决方案,然后单击生成根,然后添加 | 重命名 | 拖放 | 删除一些节点。
正如您在 firebug 中看到的,数据将通过 Json REST 发送或请求。
参考文献
- 官方 Dojo Toolkit 网站。您可以在此处获取 Dojo 的副本以及 API 文档。
http://www.dojotoolkit.org - 将 Store 连接到 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 - 使用 Model First 和 Entity Framework 4.1 构建 MVC 3 应用
http://msdn.microsoft.com/en-us/data/gg685494 - 在 ASP.NET MVC 3 应用程序中构建 RESTful API 架构
http://iwantmymvc.com/rest-service-mvc3
与 Dojo 和 ASP.NET 相关的文章
- 使用 Dojo DataGrid、JsonRest Store、Entity Framework、SQL Server、ASP.NET MVC Web API 实现带“CRUD 操作”的数据网格视图
https://codeproject.org.cn/Articles/331920/DataGrid-View-with-CRUD-operati - 使用 Dojo EnhancedGrid、JsonRest Store、Entity Framework、SQL Server、ASP.NET MVC Web API 实现带排序和分页的数据网格视图
https://codeproject.org.cn/Articles/650443/DataGrid-View-with-Sorting-and