1

更新:投稿の下部を参照)私は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 クラスの編集は効果がないようで、サーバーからのチェックがあるブランチが表示されます。それで、他に実行可能なアプローチがないと仮定すると、これを期待どおりに機能させるためのヒントはありますか?

4

1 に答える 1

0

少なくとも FF で動作させることができたようですが、提案は受け付けています。これは他の読者に役立つかもしれないので、私は自分自身に答えます.jsTreeのすべての特徴を発見するために多くの時間をグーグルで調べました. 以下は、バージョン 1.0 および MVC4 を参照しています。

まず、サーバー部分: View() とそれに対応する投稿を返すだけの Tree アクションを持つコントローラーがあります。


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

このアクションは、TempData にメッセージを設定して、受信したノードをインデックス ページに表示するだけです。動的にロードされるツリーであるため、アクションが受け取るのは、選択したノード ID のリストではなく、ユーザー アクションのリストです。選択する必要がある ID (checkedNodes) のリストと、選択を解除する必要がある ID のリストです。 (未チェックのノード)。リポジトリのサーバー側でこれらのアクションを実行すると、ユーザーが 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 コードは次のとおりです (何が起こっているかを確認するためにいくつかのログ呼び出しを使用しました)。addToCheckedOrUnchecked は、ノード ID とブール値 (ノードがチェックされているかチェックされていないかを示す) を取得し、それに応じてこのストレージを更新します。

次に、同期関数: synchChecks は、ajax 呼び出しが正常に完了したときに使用され、サーバーから受信したチェックをユーザーの編集でオーバーライドします。open_node ハンドラーは、ノードが開かれるときに使用され、その子は同様の方法で同期する必要があります。JS ストレージを処理するいくつかのユーティリティ関数 (sessionStorage を使用するか、利用できない場合は Cookie にフォールバックする) を除いて、完全なコードを次に示します。誰かがより良いアプローチ/実装を提案した場合、これが誰かの助けになることを願っています。ありがとう!

<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>
于 2012-10-09T20:53:05.127 に答える