(更新:投稿の下部を参照)私はjstree(およびJS)が初めてで、ノードをいくつかのリポジトリから動的にロードする必要があるMVC4シナリオで使用しようとしています。私はこれを機能させることができましたが、UI の状態に依存するため、どのノードがユーザーによって効果的に選択されたかを判断する際にいくつかの問題に直面しています。ノードを返し、JS によって呼び出される MVC コントローラーのメソッドから始めましょう。そのコードは次のようになります。
public JsonResult GetTreeNodes(string id)
{
// selectedIds contains some pre-selected IDs...
IEnumerable nodes = _repository.GetChildNodes(id);
return Json(
(from n in nodes
select new
{
id = n.Id,
data = n.Value,
attr = new
{
id = n.Id,
selected = selectedIds.Any(s => s == n.Id)
},
state = "closed"
}).ToArray(), JsonRequestBehavior.AllowGet);
}
これは正常に機能し、指定された ID を持つノードの子ノードを、jstree が期待する形式で返します。
私のビューには、ツリーの div と、チェックされたノードを含めるための隠しフィールドがあります。フォームが送信されると、JS 関数がチェックされたノードを取得し、非表示フィールドに格納して、MVC アクションがそれらを受け取ることができるようにします。ビューは次のようになります。
...
<section>
@using (Html.BeginForm())
{
@Html.Hidden("selectedNodes")
<div id="tree" style="max-width: 70%"></div>
<input id="submit" type="submit" value="Submit" />
}
</section>
<script type="text/javascript">
$(function () {
fillTree();
$("form:first").submit(function(e) {
storeSelected();
});
});
function fillTree() {
$("#tree").jstree({
checkbox: {
real_checkboxes: true,
checked_parent_open: true
},
plugins: ["themes", "json_data", "ui", "checkbox"],
json_data: {
"ajax": {
"type": 'GET',
"url": function(node) {
var url;
if (node == -1) {
url = "@Url.Action("GetTreeNodes", "Home")";
} else {
var nodeId = node.attr('id');
url = "@Url.Action("GetTreeNodes", "Home")" + "?id=" + nodeId;
}
return url;
},
"success": function (newData) {
return newData;
}
}
},
animation: 200
}).bind("open_node.jstree", function(event, data) {
$("#tree").jstree("check_node", "li[selected=selected]");
});
}
function storeSelected() {
var checkedIds = [];
$("#tree").jstree("get_checked", null, true).each(function() {
checkedIds.push(this.id);
});
$("#selectedNodes").val(checkedIds.join(","));
}
</script>
ご覧のとおり、ユーザーがフォームを送信すると、storeSelected 関数は jstree get_checked 関数を使用して、チェックされたノードを取得します。問題は、ここのノードが動的に読み込まれることです。そのため、これは、ユーザーがフォームを送信したときにページに実際に存在するノードを参照する場合にのみ機能します。ブランチを展開し、いくつかのノードをチェックおよびチェック解除し、送信前に同じブランチを縮小すると、ツリー UI のノードを表すリスト項目要素がページに存在しないという理由だけで、チェック済み ID の空のリストが取得されます。
では、これに対処するための最善の戦略は何でしょうか? UI の状態に関係なく、ユーザーがチェックしたノードを知ることができるように、checked/unchecked イベントに反応し、ユーザーの行動を追跡することを考えていました。したがって、次のような別のバインドを追加します。
bind("check_node.jstree uncheck_node.jstree", function(event, data) {
var id = data.rslt.obj[0].id;
addToCheckedOrUnchecked(id, event.type == "check_node"); });
そして addToCheckedOrUnchecked 関数を実装して、ユーザーがチェックまたはチェック解除した ID のリストを保存できるようにします。もちろん、ID をチェック済みリストに追加するときは、未チェック リストが存在する場合はそれを削除する必要があり、その逆も同様です。また、さまざまな AJAX 呼び出し間でこれらのリストを保持するために、いくつかの永続化メカニズムを使用する必要があります。ブラウザーの機能に応じて、セッション データまたは Cookie を想定しています。
送信する前にすべてのブランチを開くことはオプションではないことに注意してください。これは、サーバー ブランチごとにクエリを実行することを意味し、帯域幅と応答性を維持したい一方で、これらのツリーの一部は大きいためです。
私の最初の問題は、check および uncheck node イベントに関するものです。これらは文書化されていないようで (Google で調べているとわかりました)、すべてのブラウザーがそれらを正しく処理するわけではないようです。たとえば、IE9 ではイベントはまったく発生していないように見えますが、FF では発生します。いずれにせよ、この問題に対するより効率的なアプローチを提案していただけますか? これは比較的単純なタスクのコードが多いようです。異なるページでいくつかのツリーを使用するので、おそらく MVC プロジェクトでの冗長性を避けるために HTML ヘルパーを作成する必要があります。
更新 #1
試行錯誤により、JS コードをさらに強化しましたが、まだいくつかの問題に直面しています。受信したブランチをまだ送信されていないユーザー編集と同期する必要があることを追加する必要があります。ブランチを開いた場合、(事前に選択された) アイテムのチェックを外して別のアイテムをチェックし、ブランチを閉じて再度開いても、これはトリガーされません。新しい ajax 呼び出しなので、open_node イベントを処理して、ノードのチェックボックスを表示する前に変更できるようにする必要があります。とにかく、ツリーは私の同期コードを無視しているようです。まず、ノード ID をチェック済みまたは未チェックの ID リストに追加するために使用される関数は、セッション ストレージ (または Cookie) で維持されます。
function addToCheckedOrUnchecked(id, checked) {
// get arrays of IDs from storage
var listChecked = getFromStore("checkedNodes");
if (listChecked === null) {
listChecked = [];
}
var listUnchecked = getFromStore("uncheckedNodes");
if (listUnchecked === null) {
listUnchecked = [];
}
// if checking, add ID to checked and remove from unchecked if present
if (checked) {
if ($.inArray(id, listChecked) == -1) {
listChecked.push(id);
}
var i = $.inArray(id, listUnchecked);
if (i > -1) {
listUnchecked.splice(i, 1);
}
// else add ID to unchecked and remove from checked if present
} else {
if ($.inArray(id, listUnchecked) == -1) {
listUnchecked.push(id);
}
var j = $.inArray(id, listChecked);
if (j > -1) {
listChecked.splice(j, 1);
}
}
// update storage
putInStore("checkedNodes", listChecked);
putInStore("uncheckedNodes", listUnchecked);
}
この関数は単にストレージからリストを読み取り、受け取った ID と ID がチェックされているかチェックされていないかに従ってリストを更新します。
次に、この関数を追加しました:
function synchChecks(data) {
var listChecked = getFromStore("checkedNodes");
var listUnchecked = getFromStore("uncheckedNodes");
$(data).each(function () {
var id = $(this).id;
if ($.inArray(id, listChecked) > -1) {
$(this).attr.selected = true;
console.log("overridden=1: " + id);
} else if ($.inArray(id, listUnchecked) > -1) {
$(this).attr.selected = false;
console.log("overridden=0: " + id);
}
});
}
これは、ストレージからリストを取得し、(データで) 受信した各ノードの attr.selected 値をオーバーライドして、ユーザーの編集と同期するようにします。最後に、jstree 呼び出しですべてを結合します。
function fillTree() {
$("#tree").jstree({
checkbox: {
real_checkboxes: true,
checked_parent_open: true
},
plugins: ["themes", "json_data", "ui", "checkbox"],
json_data: {
"ajax": {
"type": 'GET',
"url": function(node) {
var url;
if (node == -1) {
url = "@Url.Action("GetTreeNodes", "Home")";
} else {
var nodeId = node.attr('id');
url = "@Url.Action("GetTreeNodes", "Home")" + "?id=" + nodeId;
}
return url;
},
"success": function (newData) {
synchChecks(newData);
return newData;
}
}
},
animation: 200
}).bind("open_node.jstree", function (event, data) {
openingNode = true;
var tree = $("#tree");
tree.jstree("check_node", "li[selected=selected]");
// get and synch children
var listChecked = getFromStore("checkedNodes");
var listUnchecked = getFromStore("uncheckedNodes");
$(data.inst._get_children(data.rslt.obj[0])).each(function () {
var id = $(this).attr("id");
if ($.inArray(id, listChecked) > -1) {
$(this).addClass("jstree-checked");
$(this).removeClass("jstree-unchecked");
} else if ($.inArray(id, listUnchecked) > -1) {
$(this).addClass("jstree-unchecked");
$(this).removeClass("jstree-checked");
}
});
openingNode = false;
}).bind("check_node.jstree uncheck_node.jstree", function (event, data) {
if (!openingNode) {
var id = data.rslt.obj[0].id;
addToCheckedOrUnchecked(id, event.type == "check_node");
}
});
}
ajax 呼び出しが成功したら、synchChecks を呼び出して、受信したノード (サーバーに何も投稿されていないため、まだ更新されていません) をユーザーの編集と同期させます。また、open_node では、チェック済みおよび未チェックの ID リストを取得し、それらを使用して、開かれているノードの各子ノードのチェック状態をオーバーライドします。最後に、ユーザーがノードをチェックまたはチェック解除すると、addToCheckedOrUnchecked を呼び出して ID のリストを更新します。
とにかく、これはうまくいかないようです。たとえば、ブランチを開き、事前に選択されたノードのチェックを外し、それを閉じて、再度開くと、デバッガーから、リスト ID が期待どおりであることを確認できます (チェックを外したリストには、チェックを外したノードの ID が含まれています)。ループ内の li から取得された ID は問題ありませんが、li クラスの編集は効果がないようで、サーバーからのチェックがあるブランチが表示されます。それで、他に実行可能なアプローチがないと仮定すると、これを期待どおりに機能させるためのヒントはありますか?