Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

WinForms TreeView checking/unchecking hierarchy

The following code is intended to recursively check or un-check parent or child nodes as required.

enter image description here

For instance, at this position, A, G, L, and T nodes must be unchecked if we un-check any one of them.

enter image description here

The problem with the following code is, whenever I double-click any node the algorithm fails to achieve its purpose.

The tree-searching algorithm starts here:

    // stack is used to traverse the tree iteratively.
    Stack<TreeNode> stack = new Stack<TreeNode>();
    private void treeView1_AfterCheck(object sender, TreeViewEventArgs e)
    {
        TreeNode selectedNode = e.Node;
        bool checkedStatus = e.Node.Checked;

        // suppress repeated even firing
        treeView1.AfterCheck -= treeView1_AfterCheck;

        // traverse children
        stack.Push(selectedNode);

        while(stack.Count > 0)
        {
            TreeNode node = stack.Pop();

            node.Checked = checkedStatus;                

            System.Console.Write(node.Text + ", ");

            if (node.Nodes.Count > 0)
            {
                ICollection tnc = node.Nodes;

                foreach (TreeNode n in tnc)
                {
                    stack.Push(n);
                }
            }
        }

        //traverse parent
        while(selectedNode.Parent!=null)
        {
            TreeNode node = selectedNode.Parent;

            node.Checked = checkedStatus;

            selectedNode = selectedNode.Parent;
        }

        // "suppress repeated even firing" ends here
        treeView1.AfterCheck += treeView1_AfterCheck;

        string str = string.Empty;
    }

Driver Program

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Windows.Forms;

namespace WindowsFormsApplication1
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        #region MyRegion
        private void button1_Click(object sender, EventArgs e)
        {
            TreeNode a = new TreeNode("A");
            TreeNode b = new TreeNode("B");
            TreeNode c = new TreeNode("C");
            TreeNode d = new TreeNode("D");
            TreeNode g = new TreeNode("G");
            TreeNode h = new TreeNode("H");
            TreeNode i = new TreeNode("I");
            TreeNode j = new TreeNode("J");
            TreeNode k = new TreeNode("K");
            TreeNode l = new TreeNode("L");
            TreeNode m = new TreeNode("M");
            TreeNode n = new TreeNode("N");
            TreeNode o = new TreeNode("O");
            TreeNode p = new TreeNode("P");
            TreeNode q = new TreeNode("Q");
            TreeNode r = new TreeNode("R");
            TreeNode s = new TreeNode("S");
            TreeNode t = new TreeNode("T");
            TreeNode u = new TreeNode("U");
            TreeNode v = new TreeNode("V");
            TreeNode w = new TreeNode("W");
            TreeNode x = new TreeNode("X");
            TreeNode y = new TreeNode("Y");
            TreeNode z = new TreeNode("Z");

            k.Nodes.Add(x);
            k.Nodes.Add(y);

            l.Nodes.Add(s);
            l.Nodes.Add(t);
            l.Nodes.Add(u);

            n.Nodes.Add(o);
            n.Nodes.Add(p);
            n.Nodes.Add(q);
            n.Nodes.Add(r);

            g.Nodes.Add(k);
            g.Nodes.Add(l);

            i.Nodes.Add(m);
            i.Nodes.Add(n);


            j.Nodes.Add(b);
            j.Nodes.Add(c);
            j.Nodes.Add(d);

            a.Nodes.Add(g);
            a.Nodes.Add(h);
            a.Nodes.Add(i);
            a.Nodes.Add(j);

            treeView1.Nodes.Add(a);
            treeView1.ExpandAll();

            button1.Enabled = false;
        } 
        #endregion

Expected to happen:

Take a look at the screenshot of the application. A, G, L, and T are checked. If I uncheck, say, L,
- T should be unchecked as T is a child of L.
- G and A should be unchecked as they will have no children left.

What is happening:

This application code works fine if I single-click any node. If I double-click a node, that node becomes checked/unchecked but the same change is not reflected on the parent and children.

Double-click also freezes the application for a while.

How can I fix this issue and obtain the expected behavior?

like image 226
user366312 Avatar asked May 23 '18 17:05

user366312


1 Answers

These are the main problems to solve here:

  • Prevent AfterCkeck event handler from repeating the logic recursively.

    When you change Checked property of a node in AfterCheck, it cause another AfterCheck event which may lead us to an stack overflow or at least unnecessary after check events or unpredictable result in our algorithm.

  • Fix DoubleClick on check-boxes bug in TreeView.

    When you double click on a CheckBox in TreeView, the Checked value of the Node will change twice and will be set to original state before double click, but the AfterCheck event will raise once.

  • Extension Methods to get Descendants and Ancestors of a Node

    We need to create methods to get descendants and ancestors of a node. To do so, we will create extension methods for TreeNode class.

  • Implement algorithm

    After fixing above problems, a correct algorithm will result in what we expect by click. Here is the expectation:

    When you check/uncheck a node:

    • All descendants of that node should change to the same check state.
    • All nodes in ancestors, should be checked if there is at least one child in their descendants checked, otherwise, should be unchecked.

After we fixed above problems and creating Descendants and Ancestors to traverse tree, it's enough for us to handle AfterCheck event and have this logic:

e.Node.Descendants().ToList().ForEach(x =>
{
    x.Checked = e.Node.Checked;
});
e.Node.Ancestors().ToList().ForEach(x =>
{
    x.Checked = x.Descendants().ToList().Any(y => y.Checked);
});

Download

You can download a working example from the following repository:

  • r-aghaei/TreeViewCheckUnCheckHierarchyExample
  • Zip File

Detailed Answer

Prevent AfterCkeck event handler from repeating the logic recursively

In fact we don't stop AfterCheck event handler from raising AfterCheck. Instead, we detect if the AfterCheck is raised by user or by our code inside the handler. To do so, we can check Action property of the event arg:

To prevent the event from being raised multiple times, add logic to your event handler that only executes your recursive code if the Action property of the TreeViewEventArgs is not set to TreeViewAction.Unknown.

private void exTreeView1_AfterCheck(object sender, TreeViewEventArgs e)
{
    if (e.Action != TreeViewAction.Unknown)
    {
        // Changing Checked
    }
}

Fix DoubleClick on check-boxes bug in TreeView

As also mentioned in this post, there is a bug in TreeView, when you double click on a CheckBox in TreeView, the Checked value of the Node will change twice and will be set to original state before double click, but the AfterCheck event will raise once.

To solve the problem, you can handle WM_LBUTTONDBLCLK message and check if double click is on check box, neglect it:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Forms;
public class ExTreeView : TreeView
{
    private const int WM_LBUTTONDBLCLK = 0x0203;
    protected override void WndProc(ref Message m)
    {
        if (m.Msg == WM_LBUTTONDBLCLK)
        {
            var info = this.HitTest(PointToClient(Cursor.Position));
            if (info.Location == TreeViewHitTestLocations.StateImage)
            {
                m.Result = IntPtr.Zero;
                return;
            }
        }
        base.WndProc(ref m);
    }
}

Extension Methods to get Descendants and Ancestors of a Node

To get Descendants and Ancestors of a Node, we need to create a few extension method to use in AfterCheck to implement the algorithm:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Forms;
public static class Extensions
{
    public static List<TreeNode> Descendants(this TreeView tree)
    {
        var nodes = tree.Nodes.Cast<TreeNode>();
        return nodes.SelectMany(x => x.Descendants()).Concat(nodes).ToList();
    }
    public static List<TreeNode> Descendants(this TreeNode node)
    {
        var nodes = node.Nodes.Cast<TreeNode>().ToList();
        return nodes.SelectMany(x => Descendants(x)).Concat(nodes).ToList();
    }
    public static List<TreeNode> Ancestors(this TreeNode node)
    {
        return AncestorsInternal(node).ToList();
    }
    private static IEnumerable<TreeNode> AncestorsInternal(TreeNode node)
    {
        while (node.Parent != null)
        {
            node = node.Parent;
            yield return node;
        }
    }
}

Implementing the algorithm

Using above extension methods, I'll handle AfterCheck event, so when you check/uncheck a node:

  • All descendants of that node should change to the same check state.
  • All nodes in ancestors, should be checked if there is at list one child in their descendants checked, otherwise, should be unchecked.

Here is the implementation:

private void exTreeView1_AfterCheck(object sender, TreeViewEventArgs e)
{
    if (e.Action != TreeViewAction.Unknown)
    {
        e.Node.Descendants().ToList().ForEach(x =>
        {
            x.Checked = e.Node.Checked;
        });
        e.Node.Ancestors().ToList().ForEach(x =>
        {
            x.Checked = x.Descendants().ToList().Any(y => y.Checked);
        });
    }
}

Example

To test the solution, you can fill the TreeView with the following data:

private void Form1_Load(object sender, EventArgs e)
{
    exTreeView1.Nodes.Clear();
    exTreeView1.Nodes.AddRange(new TreeNode[] {
        new TreeNode("1", new TreeNode[] {
                new TreeNode("11", new TreeNode[]{
                    new TreeNode("111"),
                    new TreeNode("112"),
                }),
                new TreeNode("12", new TreeNode[]{
                    new TreeNode("121"),
                    new TreeNode("122"),
                    new TreeNode("123"),
                }),
        }),
        new TreeNode("2", new TreeNode[] {
                new TreeNode("21", new TreeNode[]{
                    new TreeNode("211"),
                    new TreeNode("212"),
                }),
                new TreeNode("22", new TreeNode[]{
                    new TreeNode("221"),
                    new TreeNode("222"),
                    new TreeNode("223"),
                }),
        })
    });
    exTreeView1.ExpandAll();
}

.NET 2 Support

Since .NET 2 doesn't have linq extension methods, for those who are interested to have the feature in .NET 2 (including the original poster) here is the code in .NET 2.0:

ExTreeView

using System;
using System.Collections.Generic;
using System.Windows.Forms;
public class ExTreeView : TreeView
{
    private const int WM_LBUTTONDBLCLK = 0x0203;
    protected override void WndProc(ref Message m)
    {
        if (m.Msg == WM_LBUTTONDBLCLK) {
            var info = this.HitTest(PointToClient(Cursor.Position));
            if (info.Location == TreeViewHitTestLocations.StateImage) {
                m.Result = IntPtr.Zero;
                return;
            }
        }
        base.WndProc(ref m);
    }
    public IEnumerable<TreeNode> Ancestors(TreeNode node)
    {
        while (node.Parent != null) {
            node = node.Parent;
            yield return node;
        }
    }
    public IEnumerable<TreeNode> Descendants(TreeNode node)
    {
        foreach (TreeNode c1 in node.Nodes) {
            yield return c1;
            foreach (TreeNode c2 in Descendants(c1)) {
                yield return c2;
            }
        }
    }
}

AfterSelect

private void exTreeView1_AfterCheck(object sender, TreeViewEventArgs e)
{
    if (e.Action != TreeViewAction.Unknown) {
        foreach (TreeNode x in exTreeView1.Descendants(e.Node)) {
            x.Checked = e.Node.Checked;
        }
        foreach (TreeNode x in exTreeView1.Ancestors(e.Node)) {
            bool any = false;
            foreach (TreeNode y in exTreeView1.Descendants(x))
                any = any || y.Checked;
            x.Checked = any;
        };
    }
}
like image 172
Reza Aghaei Avatar answered Nov 05 '22 10:11

Reza Aghaei