TreeView checking/unchecking descendants - sometimes only root is affected - c#

Consider a TreeView structure such as the following:
The goal is to have a node's descendants check or uncheck themselves accordingly when a particular node is checked. For example, in the above, if "D" is unchecked, "D A", "D A A" and "D A B" should uncheck themselves.
Currently, the code being used is as follows:
private void treeView_AfterCheck(object sender, TreeViewEventArgs e)
{
if (e.Action != TreeViewAction.Unknown)
{
if (e.Node.Checked)
{
checkChildNodes(e.Node.Nodes);
}
else
{
uncheckChildNodes(e.Node.Nodes);
}
}
}
private void checkChildNodes(TreeNodeCollection nodes)
{
foreach (TreeNode node in nodes)
{
node.Checked = true;
if (node.Nodes.Count>0)
checkChildNodes(node.Nodes);
}
}
private void uncheckChildNodes(TreeNodeCollection nodes)
{
foreach (TreeNode node in nodes)
{
node.Checked = false;
if (node.Nodes.Count>0)
uncheckChildNodes(node.Nodes);
}
}
The problem with this is that sometimes the checking/unchecking of descendants does not occur, when the "root" node is clicked very fast. How can this be solved?
What has also been tried, is using the BeforeCheck event, as per the following link: https://learn.microsoft.com/en-us/dotnet/api/system.windows.forms.treeview.aftercheck?view=netcore-3.1

Related

WinForms TreeView checking/unchecking hierarchy

The following code is intended to recursively check or un-check parent or child nodes as required.
For instance, at this position, A, G, L, and T nodes must be unchecked if we un-check any one of them.
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?
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;
};
}
}

Count nodes with and without subsequent child nodes

I am trying to count number of tree nodes with (A) and without subsequent child nodes (B). For example the image below should return 2/1.
I tried to play around with TreeView methods but cant figure out how to work with deeper level nodes.
private void tvResources_BeforeExpand(object sender, TreeViewCancelEventArgs e)
{
int a=0,b=0;
foreach (TreeNode node in e.Node.Nodes)
{
if (e.Node.Nodes.Contains(node))
a += 1;
else
b += 1;
}
e.Node.Text += #" - " + a+"/"+b;
}
This can probably be done with some recursive function, but is there any easier solution?
If you need just the current node (before expanding), you could just use the foreach loop to check the number of nodes for each direct child and that will give you the answer directly.
private void tvResources_BeforeExpand(object sender, TreeViewCancelEventArgs e)
{
CalculateAB( e.Node );
}
private void CalculateAB( TreeNode node )
{
int a = 0;
int b = 0;
foreach ( TreeNode child in node.Nodes )
{
if ( child.Nodes.Count() > 0 )
{
a++;
}
else
{
b++;
}
}
node.Text += #" - " + a + "/" + b;
}
You can then use recursion if you want to calculate the values for the whole TreeView at once. You can use depth-first search and calculate the a/b values of all nodes you encounter using the CalculateAB method.
private void CalculateForTree( TreeNode root )
{
foreach ( var child in root.Nodes )
{
CalculateForTree( child );
}
CalcualteAB( root );
}

TreeView Node Selection

I have a problem with nodes selection. Here is what I want to achieve:
+ [ ] Directory1
- [x] Files
[ ] File1
[ ] File2
[ ] File3
[ ] File4
[ ] File5
[ ] File6
When I click on Files (checkBox enabled), it select only Files the folder node, but instead I want it to check and select entire files while single Directory selection (i.e. By Clicking Files, it select all files contained in it). Though there are thousands of files in that directory so it is impossible to check each file manually.
I think I'm missing something here.
private void SetCheck(TreeNode node, bool check)
{
foreach (TreeNode n in node.Nodes)
{
n.Checked = check;
if (n.Nodes.Count != 0)
{
SetCheck(n, check);
}
}
}
and/or
private void GetCheckedFiles(TreeNode node, List<string> fileNames)
{
if (node.Nodes.Count == 0)
{
if (node.Checked)
{
fileNames.Add(node.FullPath);
}
}
else
{
foreach (TreeNode n in node.Nodes)
{
GetCheckedFiles(n, fileNames);
}
}
}
Assuming your code runs from the AfterCheck event, you would have to remove the handler or use a variable to prevent running the same code every time the checkmark changes for every node that gets affected by the SetCheck routine.
Example:
private bool ignoreCheckEvent = false;
void treeView1_AfterCheck(object sender, TreeViewEventArgs e) {
if (!ignoreCheckEvent) {
ignoreCheckEvent = true;
SetCheck(e.Node, e.Node.Checked);
ignoreCheckEvent = false;
}
}

How to search child nods in a treeview

I tride it in this way,
private void btnFind_Click(object sender, EventArgs
{
for (int i = 0; i < treeView1.Nodes.Count - 1; i++)
{
MessageBox.Show(i.ToString());
treeView1.Nodes[i].BackColor = Color.Empty;
}
var result = from TreeNode node in treeView1.Nodes
where node.Text.Contains( Convert.ToString(txtFind.Text))
select node.Index;
foreach (int search in result)
{
treeView1.Nodes[search].BackColor = Color.Yellow;
}
}
But in this way I can find only parent nodes. Is there a proper way to do this
You can have a method to process the TreeView and then another to recursively call the child nodes. This will load _matchingNodes with all of the nodes that match your text.
Private List<TreeNode> _matchingNodes;
// Process the TreeView.
private void ProcessTreeView(TreeView treeView, String FindText)
{
_matchingNodes = new List<TreeNode>();
// Process each node recursively.
foreach (TreeNode n in treeView.Nodes)
{
if(n.Text.Contains(FindText))
_matchingNodes.Add(n);
ProcessRecursive(n, FindText);
}
}
private void ProcessRecursive(TreeNode treeNode, String FindText)
{
// Process each node recursively.
foreach (TreeNode n in treeNode.Nodes)
{
if(n.Text.Contains(FindText))
_matchingNodes.Add(n);
ProcessRecursive(n, FindText);
}
}
private void btnFind_Click(object sender, EventArgs e)
{
CallRecursive(treeView1);
}
private void PrintRecursive(TreeNode treeNode)
{
if (treeNode.Text.Contains(txtFind.Text.ToString()))
{
//MessageBox.Show(treeNode.Text);
treeNode.BackColor = Color.Blue;
}
else
{
treeNode.BackColor = Color.Empty;
}
// Print each node recursively.
foreach (TreeNode tn in treeNode.Nodes)
{
PrintRecursive(tn);
}
}
// Call the procedure using the TreeView.
private void CallRecursive(TreeView treeView)
{
// Print each node recursively.
TreeNodeCollection nodes = treeView.Nodes;
foreach (TreeNode n in nodes)
{
PrintRecursive(n);
}
}
I solved It Like this and it works as expected.
TreeView.nodes.find(nodeName,1)
The numeral 1 specifies to look at all child nodes too. A 0 means say to not include children. Only tested in Powershell.
Perhaps not so helpful for searching the text of the nodes but hopefully you can obtain the node name.

How can I find a root node in TreeView

I have a TreeView in my Windows application. Tn this TreeView, the user can add some root nodes and also some sub nodes for these root nodes and also some sub nodes for these sub nodes and so on ...
For example:
Root1
A
B
C
D
E
Root2
F
G
.
.
.
Now my question is that if I am at node 'E' what is the best way to find its first root node ('Root1')?
Here is a little method for you:
private TreeNode FindRootNode(TreeNode treeNode)
{
while (treeNode.Parent != null)
{
treeNode = treeNode.Parent;
}
return treeNode;
}
you can call in your code like this:
var rootNode = FindRootNode(currentTreeNode);
public TreeNode RootTreeNode(TreeNode n) { while (n.Level > 0) { n = n.Parent; } return n; }
Example to get root treenode:
private void treeView1_NodeMouseClick(object sender, TreeNodeMouseClickEventArgs e)
{
var node = (e == null ? ((System.Windows.Forms.TreeView)sender).SelectedNode : e.Node);
var rootNode = RootTreeNode(node);
}
Enjoy

Categories

Resources