はじめに
Dojo Toolkitは、クロスプラットフォームJavaScript / Ajax指向のアプリケーションおよびWebサイトの迅速な開発を促進するために設計されたオープンソースのモジュール式JavaScriptライブラリであり、非常に強力なユーザーインターフェイス機能を提供します。 Dojo Treeコンポーネントは、階層データの完全で使い慣れた、直感的で拡張可能なプレゼンテーションを提供します。 このコンポーネントは遅延ブランチロードをサポートしているため、大量のデータに対して非常にスケーラブルです。 Dojo Treeは、親子関係でデータを表示するための優れたウィジェットです。
この記事では、CRUD操作、ドラッグアンドドロップ(DnD)、遅延ロードをサポートするツリーを作成するプロセスを示します。 このようなツリーを作成するには、Dojoツリー、Entity Framework、SQL Server、およびAsp .Net MVCを使用します。
Entity Frameworkを使用したMVCアプリケーションの作成
この例では、Entity Frameworkの「モデルファースト」アプローチを使用しています。 しかし、これは、「コードファースト」や「データベースファースト」など、他のアプローチを使用できないことを意味するものではありません。 ジュリーラーマンの優れた記事「「モデルファースト」とEntity Framework 4.1アプローチによるMVC 3アプリケーションの作成」を参照してください 。 この記事を使用して、モデル、クラス、およびデータベースを作成できます。 ここでは、コントローラーとビューの作成に専念します。
ASP.NET MVCのRESTfulサービス
Dojo JsonRest StoreはJSONデータを送受信してエンティティにCRUD操作を提供するため、ASP.NET MVC 3でRESTfulサービスが必要です。JustinSchwartzenbergerが書いた「ASP.NET MVC 3アプリケーションでRESTfulアーキテクチャAPIを構築する」という良い記事があります 。 すべてを使用するわけではありませんが、この記事のアイデアの一部を使用しました。
最初に、1つのコントローラーアクションを使用して複数の操作(動詞)の管理を容易にするために作成する
ActionFilterAttribute
が必要です。 コードを使用して、モデルフォルダーにクラス(
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動詞)をインターセプトし、
ActionParameters
コレクションに格納します。 この属性をコントローラーのアクションに適用すると、パラメーター
httpVerb
を追加できます
httpVerb
は、HTTP要求操作の値の添付を制御します。 コントローラーは、同じパラメーターでアクションメソッドをサポートする必要がありますが、HTTP操作に基づいてさまざまなアクションを受け入れます。 HTTP操作の属性が同じでもパラメーターが同じメソッドをオーバーライドすることはできません。 このカスタム属性により、操作を決定するロジックを気にすることなく、HTTP操作に応じて機能する1つのコントローラーアクションメソッドを持つことができます。」[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(" ", "generateRoot", "Home")</li> </ul>
ホーム/ generateRootビュー
次のようにgenerateRootアクションのビューを作成します。
@{ ViewBag.Title = "generateRoot"; } <h2>@ViewBag.Message</h2>
ホーム/インデックスビュー
このビューのコード:
@{ 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) {
この部分は、
"target: "/tree/data/""
を使用して
TreeController
への
treeStore
接続を作成します。
- ノードに子があるかどうかを示す
mayHaveChildren
プロパティ -
getChildren
は、オブジェクトのすべての子孫のコピーを返します -
getRoot
はルート要素を返し、get()を実行して結果をコールバックします。 この例では、ルートID = 1 -
getLabel
は名前を返します -
pasteItem
ドラッグアンドドロップアクションに使用され、子孫のIDを変更します - 変更のためにデータベースに接続するように
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ツリーを定義し、それを
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
定義します。
このアイテムに関する詳細なドキュメントはこちらで読むことができます。
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
要素が
newItem
され、選択されたselectedObject要素の子として追加され、
treeStore.put(newItem);
サーバーに送信されます
treeStore.put(newItem);
。 500ミリ秒後に、選択した
selectedObject
再ロードさ
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
の
remove-child
ボタンのクリックを定義します。 最初に、ユーザーが要素を選択したかどうか、および選択した要素がツリーのルートではないことを確認します。 次に、アクションの実行に関する確認メッセージ:「このノードとそのすべての子孫を完全に削除してもよろしいですか?」 答えが「はい」の場合、selectedObjectはサーバーと同期され、すべてが正常な場合、メソッドはすべての子孫と選択されたノード自体を削除します
removeAllChildren(selectedObject);
。 500ミリ秒後に、選択した要素
selectedObject.ParentId
親
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.cs":
内の以下のコードをコピーします
"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); } } }
ご覧のとおり、コントローラーは単一のURL
"/Tree/Data/"
で「GET / POST / PUT / DELETE」操作を
RestHttpVerbFilter
ます。これは
RestHttpVerbFilter
。
- POSTは新しいノードを追加します
- PUTはノードを編集します
- GETは、同じレベルのノードとその子孫に関するデータを返します。 これは、遅延読み込みをサポートするのに役立ちます。
- DELETEはノードを削除します
ホームコントローラー
ツリールート生成メソッドを追加するためだけに
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を介して送受信されます。
参照資料
この資料は、「CRUD操作を使用したツリービュー」、「ドラッグアンドドロップ(DnD)」、および「Dojoツリー、Entity Framework、SQL Server、ASP.NET MVCを使用した遅延読み込み」の記事の翻訳です 。 そこで、この例のソースコードをダウンロードできます。