Получение проверенных узлов из динамически заполняемого jsTree в MVC

(Обновлено: см. внизу сообщения) Я новичок в jstree (и JS) и пытаюсь использовать его в сценарии MVC4, где узлы должны динамически загружаться из некоторого репозитория. Мне удалось заставить это работать, но я столкнулся с некоторыми трудностями при определении того, какие узлы были эффективно выбраны пользователем, так как это зависит от статуса пользовательского интерфейса. Начнем с метода контроллера MVC, который возвращает узлы и вызывается JS. Его код выглядит так:


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.

В моем представлении есть 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 для получения проверенных узлов. Проблема в том, что узлы здесь загружаются динамически, поэтому это работает только со ссылкой на узлы, которые фактически присутствуют на странице, когда пользователь отправляет форму. Если я разверну ветвь, проверю и сниму отметку с некоторых узлов, а затем свяжу ту же ветвь перед отправкой, я получу пустой список проверенных идентификаторов только потому, что элементы списка, представляющие узлы в пользовательском интерфейсе дерева, отсутствуют на странице.

Итак, какова наилучшая стратегия для решения этой проблемы? Я думал о том, чтобы отреагировать на событие 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, чтобы она могла хранить список идентификаторов, которые пользователи проверяют или снимают. Конечно, при добавлении идентификатора в проверенный список мне придется удалить его из непроверенного списка, если он есть, и наоборот. Я также должен использовать некоторый механизм сохранения для сохранения этих списков между различными вызовами AJAX: я предполагаю, что данные сеанса или файлы cookie, в зависимости от возможностей браузера.

Обратите внимание, что открывать все ветки перед отправкой не вариант, так как это подразумевало бы запрос ветки сервера за веткой, а некоторые из этих веток большие, а я хочу сохранить пропускную способность и отзывчивость.

Моя первая проблема касается событий Check и Uncheck node: они, кажется, не задокументированы (я нашел их погуглить), и кажется, что не все браузеры обрабатывают их правильно. Например, в IE9 событие вообще не срабатывает, а в FF — срабатывает. И, во всяком случае, не могли бы вы предложить более эффективные подходы к этой проблеме? Кажется, это много кода для относительно простой задачи, и поскольку я собираюсь использовать несколько деревьев на разных страницах, мне, вероятно, придется создать помощник HTML, чтобы избежать избыточности в моем проекте MVC.

Обновление №1

Методом проб и ошибок я улучшил код JS, но все еще сталкиваюсь с некоторыми проблемами. Я должен добавить, что мне нужно синхронизировать полученную ветку с еще не отправленными пользовательскими правками: если я открываю ветку, снимаю (предварительно выбранный) элемент и отмечаю другой, затем закрываю ветку и снова открываю ее, это не вызывает новый вызов ajax, поэтому мне нужно обработать событие open_node, чтобы иметь возможность изменять флажки узлов перед их отображением. Во всяком случае, дерево, кажется, игнорирует мой код синхронизации. Прежде всего, функция, используемая для добавления идентификатора узла в список проверенных или непроверенных идентификаторов, поддерживаемых в хранилище сеансов (или в файле 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);
}

Эта функция просто считывает из хранилища списки и обновляет их в соответствии с полученным идентификатором и независимо от того, проверяется ли идентификатор или нет.

Затем я добавил эту функцию:


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 я получаю списки проверенных и непроверенных идентификаторов и использую их для переопределения состояния проверки каждого дочернего узла открываемого узла. Наконец, когда пользователь проверяет или снимает отметку с узла, я вызываю addToCheckedOrUnchecked, чтобы обновить свои списки идентификаторов.

В любом случае, похоже, это не работает. Например, если я открою ветку, сниму отметку с предварительно выбранного узла, закрою его и снова открою, отладчик увидит, что идентификаторы списка соответствуют ожидаемым (неотмеченный список имеет идентификатор узла, который я не проверил), и что идентификатор, полученный из li в цикле, в порядке, но мои правки в классах li, похоже, не действуют, и я вижу ветку с проверками, поступающими с сервера. Итак, предполагая, что другого жизнеспособного подхода нет, есть ли у вас какие-либо намеки на то, чтобы позволить этому работать так, как ожидалось?


person Naftis    schedule 08.10.2012    source источник


Ответы (1)


Кажется, мне удалось заставить его работать, по крайней мере, в FF, но я открыт для предложений. Я отвечаю сам себе, так как это может быть полезно другим читателям, я потратил много времени на гугление, чтобы узнать все особенности jsTree. Следующее относится к версии 1.0 и MVC4.

Прежде всего, серверная часть: у меня есть контроллер с действием Tree, которое просто возвращается с View() и его аналогом post:


[HttpPost]
public ActionResult Tree(string checkedNodes, string uncheckedNodes)
{
    TempData["message"] = String.Format(CultureInfo.InvariantCulture,
                                        "c=[{0}] | u=[{1}]", 
                                        checkedNodes, uncheckedNodes);
    return RedirectToAction("Index");
}

Это действие просто устанавливает сообщение в TempData для отображения полученных узлов на странице индекса. Обратите внимание, что, будучи динамически загружаемым деревом, действие получает не список выбранных идентификаторов узлов, а скорее список действий пользователя: список идентификаторов, которые необходимо выбрать (checkedNodes), и список идентификаторов, которые необходимо отменить. (непроверенные узлы). Выполнение этих действий на стороне сервера для вашего репозитория синхронизирует его с тем, что пользователь сделал в jsTree, все сразу.

Ядром серверной части является действие, возвращающее узлы по запросу (вызываемое ajax), используя репозиторий для их извлечения из некоторого хранилища:


public JsonResult GetTreeNodes(string id)
{
    // some sample categories to be preselected
    string[] aSampleCategories = new[] {"categories-fun", "languages-ita"};

    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 = aSampleCategories.Any(s => s == n.Id)
                                   },
                        state = "closed"
                    }).ToArray(), JsonRequestBehavior.AllowGet);
}

Теперь на стороне клиента. Вот форма представления:

@using (Html.BeginForm())
{
    @Html.Hidden("checkedNodes")
    @Html.Hidden("uncheckedNodes")
    <div id="tree"></div>
    <input id="submit" type="submit" value="Submit" />
}

Ниже приведен код JS (я использовал несколько вызовов журнала, чтобы проверить, что происходит): storeSelected сохраняет идентификаторы узлов, которые нужно выбрать или отменить выбор, извлекая их из временного хранилища JS. addToCheckedOrUnchecked получает идентификатор узла и логическое значение (указывающее, следует ли проверять узел или нет) и соответствующим образом обновляет это хранилище.

Затем выполняются функции синхронизации: synchChecks используется при успешном завершении вызова ajax и переопределяет проверки, полученные с сервера, пользовательскими правками; обработчик open_node используется при открытии узла, и его потомки должны быть синхронизированы аналогичным образом. Вот полный код, за исключением некоторых служебных функций, связанных с хранилищем JS (использующих sessionStorage или возвращающихся к куки, когда они недоступны). Надеюсь, это может помочь кому-то, если кто-нибудь предложит лучший подход/реализацию, я буду рад узнать. Спасибо!

<script type="text/javascript">
    var openingNode = false;
    function storeSelected() {
        $("#checkedNodes").val(getFromStore("checkedNodes").join(","));
        $("#uncheckedNodes").val(getFromStore("uncheckedNodes").join(","));
    }

    function addToCheckedOrUnchecked(id, checked) {
        console.log("[BEG addToCheckedOrUnchecked]");

        // 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);
            }
        }
        console.log("checked: " + listChecked + " | unchecked: " + listUnchecked);

        // update storage
        putInStore("checkedNodes", listChecked);
        putInStore("uncheckedNodes", listUnchecked);
        console.log("[END addToCheckedOrUnchecked]");
    }

    function synchChecks(data) {
        console.log("[BEG synchChecks]");

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

        console.log("checked: " + listChecked + " | unchecked: " + listUnchecked);
        console.log("[END synchChecks]");
    }

    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) {
            console.log("[BEG open_node]");
            openingNode = true;

            var tree = $("#tree");
            tree.jstree("check_node", "li[selected=selected]");

            // get and synch children
            var listChecked = getFromStore("checkedNodes");
            var listUnchecked = getFromStore("uncheckedNodes");
            console.log("checked: " + listChecked + " | unchecked: " + listUnchecked);

            $(data.rslt.obj[0]).find("li").each(function () {
                var id = $(this).attr("id");
                if ($.inArray(id, listChecked) > -1) {
                    $(this).addClass("jstree-checked");
                    $(this).removeClass("jstree-unchecked");
                    console.log("overridden=1: " + id);
                } else if ($.inArray(id, listUnchecked) > -1) {
                    $(this).addClass("jstree-unchecked");
                    $(this).removeClass("jstree-checked");
                    console.log("overridden=0: " + id);
                }
            });

            openingNode = false;
            console.log("[END open_node]");
        }).bind("check_node.jstree uncheck_node.jstree", function (event, data) {
            console.log("[BEG " + event.type + "]");
            if (!openingNode) {

                var id = data.rslt.obj[0].id;
                addToCheckedOrUnchecked(id, event.type == "check_node");
            }
            console.log("[END " + event.type + "]");
        });
    }

    $(function () {
        // clear storage used for keeping track of checked nodes
        removeFromStore("checkedNodes");
        removeFromStore("uncheckedNodes");

        // build tree
        fillTree();

        // attach store logic to submit
        $("form:first").submit(function (e) {
            storeSelected();
        });
    });
</script>
person Naftis    schedule 09.10.2012