jsTree tri-state checkboxes with ASP.NET MVC revisited

My original jsTree checkboxes post is still my most popluar, yet it is 18 months old and no longer works with the latest version of jsTree (pre 1.0). Since that post is so popular I thought I should update it.

My first surprise was how much the jsTree guys changed their API between releases – I was only going from version 0.9.9 to version 1.0, but  I had to practically start from scratch.

So, without further ado:

jsTree is a jQuery plugin for creating a treeviews, and jsTree’s checkbox plugin allows you to create a treeview with tri-state checkboxes, like so:

Notice how “Origination” appears half-checked because only some of its children are checked.

Getting started

For this demo I am using ASP.NET MVC 3 and jsTree pre 1.0 fixed. Let’s start with a new “ASP.NET MVC 3 Web Application”, choose Internet Application, name it jsTreeDemo, and add the required jsTree files to our solution:

In the View, create a div which you want to become a treeview. I’ll name mine demoTree. Also add references to the jsTree script, and add a new index.js file for our custom javascript.

Views/Home/Index.cshtml

@{
 ViewBag.Title = "Home Page";
}

<h2>@ViewBag.Message</h2>

<div id="demoTree">

</div>
<script type="text/javascript">
treeModel = [{"data":"Confirm Application","attr":{"id":"10"},"children":null},{"data":"Things","attr":{"id":"20"},"children":[{"data":"Thing 1","attr":{"id":"21"},"children":null},{"data":"Thing 2","attr":{"id":"22"},"children":null},{"data":"Thing 3","attr":{"id":"23"},"children":null},{"data":"Thing 4","attr":{"id":"24"},"children":[{"data":"Thing 4.1","attr":{"id":"241"},"children":null},{"data":"Thing 4.2","attr":{"id":"242"},"children":null},{"data":"Thing 4.3","attr":{"id":"243"},"children":null}]}]},{"data":"Colors","attr":{"id":"40"},"children":[{"data":"Red","attr":{"id":"41"},"children":null},{"data":"Green","attr":{"id":"42"},"children":null},{"data":"Blue","attr":{"id":"43"},"children":null},{"data":"Yellow","attr":{"id":"44"},"children":null}]}];
</script>
<script src="@Url.Content("~/Scripts/jquery.jstree.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Scripts/index.js")" type="text/javascript"></script>

Scripts/index.js

/// <reference path="jquery-1.7.1-vsdoc.js" />

$(function () {
   $("#demoTree").jstree({
      json_data : {
         data : treeModel
      },
      plugins : ["themes", "json_data", "ui", "checkbox"]
 });
});

Briefly, jstree has a number of ways of poplulating the tree with data – in my previous jsTree post I was populating the tree via AJAX but for now the tree is populated via hardcoded JSON (using the built-in "json_data" plugin). Don’t forget to specify the "checkbox" plugin too.

Populate the tree from a viewmodel

Just like last time, let’s create a viewmodel for creating the tree structure.

Models/JsTreeModel.cs

namespace jsTreeDemo.Models
{
  public class JsTreeModel
  {
    public string data;
    public JsTreeAttribute attr;
    public JsTreeModel[] children;
  }
  
  public class JsTreeAttribute
  {
    public string id;
    public bool selected;
  }
}

Now let’s change our controller to create a viewmodel, serialize it to JSON, and pass that string to our View.
Controllers/HomeController.cs

public ActionResult Index()
{
    ViewBag.Message = "Welcome!";
            
    var model = GetTreeData();
            
    string jsonModel = new JavaScriptSerializer().Serialize(model);
            
    return View("Index", "_Layout", jsonModel);
}

private JsTreeModel[] GetTreeData()
{
var tree = new JsTreeModel[] 
{
    new JsTreeModel { data = "Confirm Application", attr = new JsTreeAttribute { id = "10", selected = true } },
    new JsTreeModel 
    { 
        data = "Things",
        attr = new JsTreeAttribute { id = "20" },
        children = new JsTreeModel[]
            {
                new JsTreeModel { data = "Thing 1", attr = new JsTreeAttribute { id = "21", selected = true } },
                new JsTreeModel { data = "Thing 2", attr = new JsTreeAttribute { id = "22" } },
                new JsTreeModel { data = "Thing 3", attr = new JsTreeAttribute { id = "23" } },
                new JsTreeModel 
                { 
                    data = "Thing 4", 
                    attr = new JsTreeAttribute { id = "24" },
                    children = new JsTreeModel[] 
                    { 
                        new JsTreeModel { data = "Thing 4.1", attr = new JsTreeAttribute { id = "241" } }, 
                        new JsTreeModel { data = "Thing 4.2", attr = new JsTreeAttribute { id = "242" } }, 
                        new JsTreeModel { data = "Thing 4.3", attr = new JsTreeAttribute { id = "243" } }
                    },
                },
            }
    },
    new JsTreeModel 
    {
        data = "Colors",
        attr = new JsTreeAttribute { id = "40" },
        children = new JsTreeModel[]
            {
                new JsTreeModel { data = "Red", attr = new JsTreeAttribute { id = "41" } },
                new JsTreeModel { data = "Green", attr = new JsTreeAttribute { id = "42" } },
                new JsTreeModel { data = "Blue", attr = new JsTreeAttribute { id = "43" } },
                new JsTreeModel { data = "Yellow", attr = new JsTreeAttribute { id = "44" } },
            }
    }
};

return tree;
}

Views/Index.cshtml

@model string
       
@{
    ViewBag.Title = "Home Page";
}

...

<script type="text/javascript">
    treeModel = @Html.Raw(Model);
</script>

Determining which items are checked when posting

Let’s put our tree inside a <form> and submit it.
Views/Index.cshtml

...
@using (Html.BeginForm("Results", "Home", FormMethod.Post))
{ 
    <div id="demoTree">
    </div>
    <br />
    <div>
        <input type="submit" value="Submit" id="btnSubmit" />
    </div>
} 
...

And now let’s add the Results method to the HomeController

Controllers/HomeController.cs

[HttpPost]
public ActionResult Results(FormCollection form)
{
    return View(form);
}

and the View

Views/Home/Results.cshtml

@model FormCollection

@{
    ViewBag.Title = "Results";
}

<h2>Results</h2>
<p>
    You chose:
    @foreach (var item in Model.AllKeys)
    {
        @Model[item]@: 
    } 
</p>

<p>
    @Html.ActionLink("Home", "Index")
</p>

If you press the Submit button, nothing will happen as nothing is passed through in the FormCollection to Results() in HomeController. Despite appearances, jsTree doesn’t by default render any HTML <input>s for the checkboxes. But it’s easy enough to tell it to render them using the real_checkboxes flag:

Scripts/index.js

$(function () {
    $("#demoTree").jstree({
        json_data: {
            data: treeModel
        },
        checkbox: {
            real_checkboxes: true,
            real_checkboxes_names: function (n) {
                return [("check_" + (n[0].id || Math.ceil(Math.random() * 10000))), n[0].id]
            }
        },
        plugins: ["themes", "json_data", "ui", "checkbox"]
    });
});

Now when we submit our form, we should see the ids of the values which were checked:

What about telling the tree to pre-check some items?

So how do we render the tree with some items already checked? Notice how we added “public bool selected” to JsTreeAttribute? This doesn’t do anything as far as the checkboxes are concerned, but it does add a custom property called “selected” to each node’s <li>. We can use that to tell the jsTree to check the given node, by binding to the ‘loaded.jstree’ event.

Scripts/index.js

$(function () {
    $("#demoTree").jstree({
        json_data: {
            data: treeModel
        },
        checkbox: {
            real_checkboxes: true,
            real_checkboxes_names: function (n) {
                return [("check_" + (n[0].id || Math.ceil(Math.random() * 10000))), n[0].id]
            }
        },
        plugins: ["themes", "json_data", "ui", "checkbox"]
    }).bind("loaded.jstree", function (event, data) {
        $('#demoTree').jstree('check_node', 'li[selected=selected]');
    });
});

You’ll see that the nodes that are marked with selected=true in GetTreeData() are now pre-checked when you first load the page.

Here’s a link to the solution (VS 2010).