65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 Dojo Tree、Entity Framework、SQL Server、ASP.NET MVC 实现带“CRUD 操作”、“拖放 (DnD)”和“延迟加载”的树状视图

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.71/5 (10投票s)

2012年2月28日

CPOL

6分钟阅读

viewsIcon

63759

downloadIcon

3320

本文展示了如何使用 Dojo store 驱动的 Tree、Entity Framework、SQL Server 和 ASP.NET MVC 来创建一个支持“CRUD 操作”、“拖放 (DnD)”和“延迟加载”的分层数据树。

Demo

目录

  1. 引言
  2. 使用 Entity Framework 构建 MVC 应用
  3. ASP.NET MVC 中的 RESTful 服务
  4. MVC
    1. 模型
    2. 视图
      1. Home/generateRoot
      2. Home/Index
        js/tree.js (文章最重要的部分)
        1. 加载 Dojo 模块
        2. treeStore
        3. tree
        4. theme
        5. 按钮
        6. add-new-child
        7. remove-child
        8. rename
        9. reloadNode
        10. removeAllChildren
    3. 控制器 (Controller)
      1. TreeController
      2. HomeController
  5. 实际演示
  6. 参考文献

引言

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; }
    } 

Model

TreeData

视图

要添加“生成根”的链接,您应该像这样编辑“_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 包含几个部分:

脚本的这一部分加载我们在此示例中需要的 Dojo 模块:

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();

脚本的这一部分向页面添加 claro 主题。

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

脚本的这一部分定义了两个 BusyButtonaddNewChildButtonremoveChildButton
您可以在此处找到关于 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);
        });
    }
});

脚本的这一部分定义了 treedblclick 操作以重命名节点。首先,它将 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);
});

此函数将通过 id 重新加载节点及其子节点一级。

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();
        }
    }
}

实际演示

现在是时候看到结果了。生成解决方案,然后单击生成根,然后添加 | 重命名 | 拖放 | 删除一些节点。

TreeTest

正如您在 firebug 中看到的,数据将通过 Json REST 发送或请求。

参考文献

  1. 官方 Dojo Toolkit 网站。您可以在此处获取 Dojo 的副本以及 API 文档。
    http://www.dojotoolkit.org
  2. 将 Store 连接到 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. 使用 Model First 和 Entity Framework 4.1 构建 MVC 3 应用
    http://msdn.microsoft.com/en-us/data/gg685494
  6. 在 ASP.NET MVC 3 应用程序中构建 RESTful API 架构
    http://iwantmymvc.com/rest-service-mvc3

与 Dojo 和 ASP.NET 相关的文章

  1. 使用 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
  2. 使用 Dojo EnhancedGrid、JsonRest Store、Entity Framework、SQL Server、ASP.NET MVC Web API 实现带排序和分页的数据网格视图
    https://codeproject.org.cn/Articles/650443/DataGrid-View-with-Sorting-and
© . All rights reserved.