(更新:见帖子底部)我是 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);
}
这工作正常,并以 jstree 期望的形式返回具有指定 ID 的节点的子节点。
我的视图有一个用于树的 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 函数来检索选中的节点。问题是这里的节点是动态加载的,所以这只适用于当用户提交表单时页面中有效存在的节点。如果我展开一个分支,选中并取消选中一些节点,然后在提交之前收缩同一个分支,我将得到一个空的选中 ID 列表,因为代表树 UI 中节点的列表项元素不存在于页面中。
那么处理这个问题的最佳策略是什么?我正在考虑对选中/未选中事件做出反应,并跟踪用户所做的事情,这样我就可以知道他检查了哪些节点,而不管 UI 状态如何。所以我会像这样添加另一个绑定:
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,这取决于浏览器的功能。
请注意,在提交之前打开所有分支不是一个选项,因为这意味着逐个分支查询服务器分支,其中一些树很大,而我想保留带宽和响应性。
我的第一个问题涉及检查和取消检查节点事件:它们似乎没有记录(我发现它们在谷歌上搜索),而且似乎并非所有浏览器都正确处理它们。例如,在 IE9 中,该事件似乎根本不会触发,而在 FF 中会触发。无论如何,你能提出更有效的方法来解决这个问题吗?对于一个相对简单的任务来说,这似乎是很多代码,并且由于我将在不同的页面中使用多个树,我可能必须创建一个 HTML 帮助程序以避免在我的 MVC 项目中出现冗余。
更新#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 类的编辑似乎没有效果,并且我看到带有来自服务器的检查的分支。那么,假设没有其他可行的方法,您是否有任何暗示可以让这项工作按预期进行?