ASP.NET TreeView and Checkboxes

Although the TreeView control is a very powerful one, there are some pitfalls that you can run into when using it. One of the most popular problem is using checkboxes in TreeView. Despite the fact that you can enable checkboxes by setting ShowCheckBoxes="All" you still have to write a lot of JavaScript code to make it work properly. The problems are:

  • When you check a parent or a branch node the child nodes don't get checked
  • TreeView doesn't generate <label> tags around the nodes text, so when user click on the text next to the checkbox, the latter don't get checked

My solution written with JavaScript and with the help of ASP.NET AJAX library (so, don't forget to put a ScriptManager to your page) solves both problems. It was tested in IE, Firefox, Safari and Opera.

There are some other solutions, but they are missing some features, for instance they don't convert a link click to a checkbox selection.

Providing you have a TreeView on your page, you have similar code:

<asp:TreeView ID="someTree" runat="server" ShowCheckBoxes="All">
    <Nodes>
        <asp:TreeNode Text="Root">
            <asp:TreeNode Text="Leaf" />
            <asp:TreeNode Text="Branch">
                <asp:TreeNode Text="Leaf" />
                <asp:TreeNode Text="Leaf" />
            </asp:TreeNode>
        </asp:TreeNode>
        <asp:TreeNode Text="Root">
            <asp:TreeNode Text="Leaf" />
            <asp:TreeNode Text="Leaf" />
            <asp:TreeNode Text="Leaf" />
            <asp:TreeNode Text="Leaf" />
        </asp:TreeNode>
    </Nodes>
</asp:TreeView>

You have to put the following code either to the section or to a separate JavaScript file.

<script type="text/javascript">
    var TREEVIEW_ID ="someTree"; //the ID of the TreeView control
    //the constants used by GetNodeIndex()
    var LINK = 0;
    var CHECKBOX = 1;

    //this function is executed whenever user clicks on the node text
    function ToggleCheckBox(senderId)
    {
        var nodeIndex = GetNodeIndex(senderId, LINK);
        var checkBoxId = TREEVIEW_ID + "n" + nodeIndex + "CheckBox";
        var checkBox = document.getElementById(checkBoxId);
        checkBox.checked = !checkBox.checked;

        ToggleChildCheckBoxes(checkBox);
        ToggleParentCheckBox(checkBox);
    }

    //checkbox click event handler
    function checkBox_Click(eventElement)
    {
        ToggleChildCheckBoxes(eventElement.target);
        ToggleParentCheckBox(eventElement.target);
    }

    //returns the index of the clicked link or the checkbox
    function GetNodeIndex(elementId, elementType)
    {
         var nodeIndex;
         if(elementType == LINK)
         {
            nodeIndex = elementId.substring((TREEVIEW_ID + "t").length);
         }
         else if (elementType == CHECKBOX)
         {
            nodeIndex = elementId.substring((TREEVIEW_ID + "n").length, elementId.indexOf("CheckBox"));
         }
         return nodeIndex;
    }

    //checks or unchecks the nested checkboxes
    function ToggleChildCheckBoxes(checkBox)
    {
        var postfix = "n";
        var childContainerId = TREEVIEW_ID + postfix + GetNodeIndex(checkBox.id, CHECKBOX) + "Nodes";
        var childContainer = document.getElementById(childContainerId);
        if (childContainer)
        {
            var childCheckBoxes = childContainer.getElementsByTagName("input");
            for (var i = 0; i < childCheckBoxes.length; i++)
            {
                childCheckBoxes[i].checked = checkBox.checked;
            }
        }
    }

    //unchecks the parent checkboxes if the current one is unchecked
    function ToggleParentCheckBox(checkBox)
    {
        if(checkBox.checked == false)
        {
            var parentContainer = GetParentNodeById(checkBox, TREEVIEW_ID);
            if(parentContainer)
            {
                var parentCheckBoxId = parentContainer.id.substring(0, parentContainer.id.search("Nodes")) + "CheckBox";
                if($get(parentCheckBoxId) && $get(parentCheckBoxId).type == "checkbox")
                {
                    $get(parentCheckBoxId).checked = false;
                    ToggleParentCheckBox($get(parentCheckBoxId));
                }
            }
        }
    }

    //returns the ID of the parent container if the current checkbox is unchecked
    function GetParentNodeById(element, id)
    {
        var parent = element.parentNode;
        if (parent == null)
        {
            return false;
        }
        if (parent.id.search(id) == -1)
        {
            return GetParentNodeById(parent, id);
        }
        else
        {
            return parent;
        }
    }
</script>

This code should be put to the bottom of the page, that will assign the event handler created above.

<script type="text/javascript">
    var links = document.getElementsByTagName("a");
    for (var i = 0; i < links.length; i++)
    {
        if (links[i].className == TREEVIEW_ID + "_0")
        {
            links[i].href = "javascript:ToggleCheckBox(\"" + links[i].id + "\");";
        }
    }

    var checkBoxes = document.getElementsByTagName("input");
    for (var i = 0; i < checkBoxes.length; i++)
    {
        if (checkBoxes[i].type == "checkbox")
        {
            $addHandler(checkBoxes[i], "click", checkBox_Click);
        }
    }
</script>

Although we don't use AJAX here, the code still makes uses of ASP.NET AJAX library because it provide convenient event handling classes and quick shortcuts.

The other way to solve the problem is to use CSS Friendly Control Adapters that contain a TreeView modification that has all the features we need, moreover it generates <ul> and <li> instead of tables that dramatically reduces the size of the HTML code.

The project files are located here.

Mike Borozdin (Twitter)
11 August 2008

The opinions expressed herein are my own personal opinions and do not represent my employer's view in any way. My personal thoughts tend to change, hence the articles in this blog might not provide an accurate reflection of my present standpoint.

© Mike Borozdin