Как стать автором
Обновить

Tree View с «CRUD операциями», «drag and drop (DnD)» и «отложенной загрузкой» с использованием Dojo Tree, Entity Framework, SQL Server и ASP.NET MVC

Время на прочтение 11 мин
Количество просмотров 12K
Автор оригинала: Nikfazan
image

Введение

Dojo Toolkit это модульная JavaScript библиотека с открытым исходным кодом, предназначенная для облегчения быстрой разработки кросс-платформенных JavaScript/Ajax-ориентированных приложений и веб-сайтов, которая предоставляет некоторые действительно мощные возможности для пользовательского интерфейса. Компонент Dojo Tree обеспечивает полное, привычное, интуитивно понятное, развертываемое представление иерархических данных. Этот компонент поддерживает отложенную загрузку веток, что делает его хорошо масштабируемым для больших объемов данных. Dojo Tree отличный виджет для представления данных с отношениями «предок-потомок».

Эта статья показывает процесс создания дерева, поддерживающего «CRUD операции», «drag and drop (DnD)» и «отложенную загрузку». Чтобы создать такое дерево мы будем использовать Dojo Tree, Entity Framework, SQL Server и Asp .Net MVC.

Создание MVC приложения используя Entity Framework

Этот пример использует подход Entity Framework «сначала модель». Но это не значит, что вы не можете использовать и другие подходы, как «сначала код» или «сначала база данных». У Julie Lerman есть отличная статья «Создание MVC 3 приложения с подходом „сначала модель“ и Entity Framework 4.1» здесь. Вы можете использовать эту статью для создания вашей модели, класса и базы данных. Создание же контроллеров и представлений будет освящено здесь.

RESTful сервис в ASP.NET MVC

Так как Dojo JsonRest Store посылает и получает JSON данные для предоставления CRUD операций над сущностями, нам нужен RESTful сервис с ASP.NET MVC 3. Вы можете найти хорошую статью «Построение RESTful API архитектуры в ASP.NET MVC 3 приложениях» написанную Justin Schwartzenberger здесь. Мы не будем использовать её всю, но я использовал часть идей из этой статьи.

Для начала нам нужен свой ActionFilterAttribute, который мы создадим для облегчения управления несколькими операциями (verb) с использованием одного действия контроллера. Создайте класс (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 операции (HTTP verb) запроса и сохранять их в коллекции 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; }
    } 

image

Представление

Для добавления ссылки генерации корня, необходимо изменить часть меню в файле "_Layout.cshtml" как показано ниже:
	<ul id="menu">
		<li>@Html.ActionLink("Home", "Index", "Home")</li>
		<li>@Html.ActionLink("Генерация корня", "generateRoot", "Home")</li>
	</ul>

Представление Home/generateRoot

Создайте представление для действия «generateRoot» как показано ниже:
@{
    ViewBag.Title = "generateRoot";
}
<h2>@ViewBag.Message</h2>

Представление Home/Index

Код для данного представления:
@{
    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>

Полную статью о части кода выше и ниже вы можете посмотреть здесь.

Как видно из кода выше, у нас есть ссылка на 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 к TreeController с помощью "target: "/tree/data/"".

  • mayHaveChildren свойство, которое показывает есть ли у узла потомки
  • getChildren возвращает копию всех потомков объекта
  • getRootвозвращает корневой элемент, we will do a get() and callback the result. В этом примере у корня id = 1
  • getLabel возвращает имя
  • pasteItem используется для drag and drop действия, изменяет Id потомка перемещаемого элемента
  • 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 и присоединяет его к и 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");


Данная часть скрипта определяет BusyButton: addNewChildButton и removeChildButton.
Подробную документацию об этом элементе вы можете прочитать здесь
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. Во-первых, определяется выбрал ли пользователь элемент дерева. Затем выбранный элемент 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. Сначала проверяется выбрал ли пользователь какой-либо элемент, и что выбранный элемент не является корнем дерева. Далее происходит запрос подтверждения о выполнении действия: «Вы уверены, что хотите навсегда удалить этот узел и все его потомки?». Если ответ да, тогда происходит синхронизация выбранного объекта selectedObject с сервером и если все в порядке, то будет вызван метод удаления всех потомков и самого выбранного узла removeAllChildren(selectedObject);. По прошествии 500 мс, родитель выбранного элемента selectedObject.ParentId перезагружается.
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);
        });
    }
});


Эта часть скрипта определяет двойной клик dblclick для переименования узла дерева. Для начала производится синхронизация выбранного элемента с сервером и если все в порядке, запрашивается новое имя для узла. Затем новое имя передается серверу 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);
        }
    }
}


Как вы видите, контроллер выполняет «GET/POST/PUT/DELETE» операции в едином URL "/Tree/Data/", это возможно благодаря 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();
        }
    }
}


Наглядная демонстрация

Теперь пора посмотреть на результат. Постройте решение и нажмите на генерацию корня, потом добавьте | переименуйте | перетащите и вставьте | удалите какие-нибудь узлы дерева.
image
Как вы видите в fireBug данные посылаются и принимаются через Json REST.

Ссылки

Данный материал является переводом статьи «Tree View with „CRUD operations“, „drag and drop (DnD)“ and „Lazy Loading“ using Dojo Tree, Entity Framework, SQL Server, ASP.NET MVC» отсюда. Там же можно скачать исходный код данного примера.
Теги:
Хабы:
+4
Комментарии 3
Комментарии Комментарии 3

Публикации

Истории

Работа

Ближайшие события

Московский туристический хакатон
Дата 23 марта – 7 апреля
Место
Москва Онлайн
Геймтон «DatsEdenSpace» от DatsTeam
Дата 5 – 6 апреля
Время 17:00 – 20:00
Место
Онлайн