Arrange TreeView by getting file paths? - c#

I have this code:
public void AddNode(string Node)
{
try
{
treeView.Nodes.Add(Node);
treeView.Refresh();
}
catch (Exception ex)
{
MessageBox.Show(ex.ToString());
}
}
Very simple as you see, this method gets file path. like C:\Windows\notepad.exe
Now i want the TreeView to show it like FileSystem..
-C:\
+Windows
And if i click the '+' it gets like this:
-C:\
-Windows
notepad.exe
Here is what i get now from sending theses pathes to the method above:
How can i do that it will arrange the nodes?

If I were you, I would split the input string onto substrings, using the string.Split method and then search for the right node to insert the relevant part of a node. I mean, that before adding a node, you should check whether node C:\ and its child node (Windows) exist.
Here is my code:
...
AddString(#"C:\Windows\Notepad.exe");
AddString(#"C:\Windows\TestFolder\test.exe");
AddString(#"C:\Program Files");
AddString(#"C:\Program Files\Microsoft");
AddString(#"C:\test.exe");
...
private void AddString(string name) {
string[] names = name.Split(new char[] { '\\' }, StringSplitOptions.RemoveEmptyEntries);
TreeNode node = null;
for(int i = 0; i < names.Length; i++) {
TreeNodeCollection nodes = node == null? treeView1.Nodes: node.Nodes;
node = FindNode(nodes, names[i]);
if(node == null)
node = nodes.Add(names[i]);
}
}
private TreeNode FindNode(TreeNodeCollection nodes, string p) {
for(int i = 0; i < nodes.Count; i++)
if(nodes[i].Text.ToLower(CultureInfo.CurrentCulture) == p.ToLower(CultureInfo.CurrentCulture))
return nodes[i];
return null;
}

If you are in windows forms (and I guess so), you can implement the IComparer class and use the TreeView.TreeViewNodeSorter property:
public class NodeSorter : IComparer
{
// Compare the length of the strings, or the strings
// themselves, if they are the same length.
public int Compare(object x, object y)
{
TreeNode tx = x as TreeNode;
TreeNode ty = y as TreeNode;
// Compare the length of the strings, returning the difference.
if (tx.Text.Length != ty.Text.Length)
return tx.Text.Length - ty.Text.Length;
// If they are the same length, call Compare.
return string.Compare(tx.Text, ty.Text);
}
}

Is the issue that the parents and children aren't being differentiated?
Each one of the nodes in the tree also has a Nodes property, which represents the collection of its children. Your AddNode routine needs to be changed so you can specify the parent node to whom you want to add a child node. Like:
TreeNode parent = //some node
parent.Nodes.Add(newChildNode);
If you want it to just populate the paths and figure out the parent-child relationships itself, you're going to have to write some code to parse the paths, and identify the parent node based on the path segments.

Try taking a look at this Filesystem TreeView. It should do exactly what you are looking for.

Related

How do I merge two XDocuments removing duplicates

I have two XML files (*.resx files) that I am trying to merge in to one removing duplicates, but am unable to do so. I've tried the following without any success:
var resource1 = XDocument.Load("C:\\Resources.resx");
var resource2 = XDocument.Load("C:\\Resources2.resx");
// This results in a file with all the nodes from the second file included inside
// the root element of the first file to form a properly formatted, concatenated file.
resource1.Descendants().FirstOrDefault().Add(resource2.Descendants().FirstOrDefault().Nodes());
var nodeContent = new List<string>();
foreach (XElement node in resource1.Root.Elements())
{
if (nodeContent.Contains(node.ToString()))
resource1.Remove();
else
nodeContent.Add(node.ToString());
}
resource1.Save("C:\\FinalResources.resx");
On the remove statement I get an InvalidOperationException - "The parent is missing.":
Am I doing something wrong?
You need to define an EqualityComparer<XElement> that will enable you to use the standard LINQ operators.
So, as a simple example I created this:
public class ElementComparer : EqualityComparer<XElement>
{
public override int GetHashCode(XElement xe)
{
return xe.Name.GetHashCode() ^ xe.Value.GetHashCode();
}
public override bool Equals(XElement xe1, XElement xe2)
{
var #return = xe1.Name.Equals(xe2.Name);
if (#return)
{
#return = xe1.Value.Equals(xe2.Value);
}
return #return;
}
}
So I can then start with these two XML documents:
<xs>
<x>D</x>
<x>A</x>
<x>B</x>
</xs>
<xs>
<x>E</x>
<x>B</x>
<x>C</x>
</xs>
And do this:
var xml1 = XDocument.Parse(#"<xs><x>D</x><x>A</x><x>B</x></xs>");
var xml2 = XDocument.Parse(#"<xs><x>E</x><x>B</x><x>C</x></xs>");
xml1.Root.Add(
xml2.Root.Elements("x")
.Except(xml1.Root.Elements("x"), new ElementComparer()));
Then xml1 will look like this:
<xs>
<x>D</x>
<x>A</x>
<x>B</x>
<x>E</x>
<x>C</x>
</xs>
Well, the most straight forward way is:
var resource1 = XDocument.Load("C:\\Resources.resx");
var resource2 = XDocument.Load("C:\\Resources2.resx");
foreach (XElement node in resource2.Root.Elements())
{
if (resource1.Root.Contains(node)) continue;
resource1.Add(node);
}
resource1.Save("C:\\FinalResources.resx");
public static class XElementExtensions
{
public static bool Contains(this XElement root, XElement e)
{
//or w/e equality logic you need
return root.Elements().Any(x => x.ToString().Equals(e.ToString()));
}
}
This will only merge first level entries tho. If you need deep merge, then you will have to set up a simple recursion (using the same loop for child elements).
resource1.Remove(); is called twice and what it does is remove the root element. So the second time there is no longer a root element to remove from and thus throwing the exception.

How can I get all expanded nodes in treeview?

I have a program that contains a TreeView. All of my nodes except for the root and two nodes under the root are loaded from a database.
When the user adds data to the database it must be automatically added to the TreeView. I can successfully do this by clearing all the nodes, add the default nodes and add all the data including the new one to my TreeView, but all nodes of the new TreeView are collapsed.
Our client wants to retain all expanded nodes but still add the new data he just added. Is there any way to know all expanded nodes and expand it again once collapsed or refreshed? Thank you for any response.
Hi As i understand you want to save you tree view map after you refresh tree view (by adding new data or even remove some) you want to expand all expanded nodes the other are collapsed by default.
Solution is:
1) save expanded tree view nodes before refresh
2) refresh tree view data (notice that if you are removing a node , remove it from saved list as well)
3) set tree view map which saved before
saving tree view map (only expanded nodes)->
This Code Look through a tree view node collection and saves expanded nodes Names in a string list
List<string> collectExpandedNodes(TreeNodeCollection Nodes)
{
List<string> _lst = new List<string>();
foreach (TreeNode checknode in Nodes)
{
if (checknode.IsExpanded)
_lst.Add(checknode.Name);
if (checknode.Nodes.Count > 0)
_lst.AddRange(collectExpandedNodes(checknode.Nodes));
}
return _lst;
}
Now you have collected expanded nodes name in a list and you want to gain back your tree view appearance you need 2 functions a function which retrieves node by a name and a function which expand selected node and it's parents the following codes does :
This function retrieves a pointer to selected node Name if node is exist in Tree Node Collection
TreeNode FindNodeByName(TreeNodeCollection NodesCollection , string Name)
{
TreeNode returnNode = null; // Default value to return
foreach (TreeNode checkNode in NodesCollection)
{
if (checkNode.Name == Name) //checks if this node name is correct
returnNode = checkNode;
else if (checkNode.Nodes.Count > 0 ) //node has child
{
returnNode = FindNodeByName(checkNode.Nodes , Name);
}
if (returnNode != null) //check if founded do not continue and break
{
return returnNode;
}
}
//not found
return returnNode;
}
and This Function Expand node and it's parents
void expandNodePath(TreeNode node)
{
if (node == null)
return;
if (node.Level != 0) //check if it is not root
{
node.Expand();
expandNodePath(node.Parent);
}
else
{
node.Expand(); // this is root
}
}
and the following show you the usage of the functions
private void button4_Click(object sender, EventArgs e)
{
//saving expanded nodes
List<string> ExpandedNodes = new List<string>();
ExpandedNodes = collectExpandedNodes(treeView1.Nodes);
//resetting tree view nodes status to colapsed
treeView1.CollapseAll();
//Restore it back
if (ExpandedNodes.Count > 0)
{
TreeNode IamExpandedNode;
for (int i = 0; i < ExpandedNodes.Count;i++ )
{
IamExpandedNode = FindNodeByName(treeView1.Nodes, ExpandedNodes[i]);
expandNodePath(IamExpandedNode);
}
}
}
For expanding all the nodes use below code
treeView1.ExpandAll();
For expanding selected node use below code
treeView1.SelectedNode.ExpandAll();
For expanding particular node use below code
treeView1.Nodes[Index].Expand();
Assume the Nodename is unique.
Using an Database the nodename can be the unique rowid of an table
The state of the tree (List) can simply saved with an Formatter (e.g. BinaryFormatter)
if User want to save status
Only save the expandedstate
private List<string> SaveTreeState(TreeNodeCollection nodes)
{
List<string> nodeStates = new List<string>();
foreach (TreeNode node in nodes)
{
if (node.IsExpanded) nodeStates.Add(node.Name);
nodeStates.AddRange(SaveTreeState(node.Nodes));
}
return (nodeStates);
}
Let the treeview do the work finding the nodes for restore
private void RestoreTreeState(TreeView tree, List<string> treeState)
{
foreach (string NodeName in treeState)
{
TreeNode[] NodeList = treeView1.Nodes.Find(NodeName, true);
if (NodeList.Length > 0) // only if node after reload is avail
NodeList[0].Expand();
}
}
Using:
List<string> StateList = SaveTreeState(treeView1.Nodes);
... // reload
RestoreTreeState(treeView1, StateList);
To simply expand nodes you can try this following code
private void button1_Click(object sender, EventArgs e)
{
treeView1.Nodes.Add(new TreeNode("New Node",
new TreeNode[2] { new TreeNode("Node1"), new TreeNode("Node2") }));
treeView1.Nodes[1].Expand();
}
Hope that helps
It's very simple. Below, you can see my recursive version:
//List of storage ids of expanded nodes
List<int> expandedNodeIds = new List<int>();
//call recursive fun for our tree
CollectExpandedNodes(tree.Nodes);
//recursive fun for collect expanded node ids
private void CollectExpandedNodes(TreeListNodes nodes)
{
foreach (TreeListNode node in nodes)
{
if (node.Expanded) expandedNodeIds.Add(node.Id);
if (node.HasChildren) CollectExpandedNodes(node.Nodes);
}
}
I know this post is old but if the tree is deep it might not be a good idea to use a recursive traversal of the tree. Since I have not seen any anwser using a non-recursive way here is a solution for doing getting expanded nodes without impacting performance.
public static IEnumerable<TreeNodeAdv> CollectExpandedNodes(this TreeNodeAdv root)
{
Stack<TreeNodeAdv> s = new Stack<TreeNodeAdv>();
s.Push(root);
while (s.Count > 0)
{
TreeNodeAdv n = s.Pop();
if (n.IsExpanded)
yield return n;
foreach (var child in n.Children.ToArray().Reverse())
{
s.Push(child);
}
}
}
To use this method, you can do the following:
foreach (TreeNodeAdv expandedNode in yourTreeView.Root.CollectExpandedNodes())
{
//Do processing on the expanded node or add in list.
}
This extension method uses Deep-First traversal in post-order along with the yield keyword to generate an IEnumerable collection.

Efficient algorithm for converting an array of depths into a Tree

I have a stored procedure that returns a flat list of names that are organized in a Tree. To communicate who is the parent of whom there is a Depth value, so a result of 5 records (going up to 3 levels) looks like this:
Depth|Name
----------
0|Ford
1|Compact Cars
2|Pinto
1|Trucks
2|H-Series
I am trying to construct a Tree out of this array by reading the depth values. Is there some obvious algorithm for constructing a tree out of a sequence of data like this? I'm adding the C# tag because I'm open to LINQy solutions to this problem though a generic Computer Science answer would be extremely helpful.
Here is my current attempt:
class Record
{
public string Name{ get; set; }
public List<Record> children { get; set; }
}
var previousLevel = 0;
var records = new List<Record>();
foreach (var thing in TreeFactory.fetch(dao))
{
if(this.Depth == 0) {
//Root node
} else if(thing.Depth > previousLevel) {
//A Child of the last added node
} else if(thing.Depth < previousLevel) {
//A Cousin of the last added node
} else {
//A Sibling of the of the last added node
}
previousLevel = this.Depth;
}
By "efficient" I'm talking List sizes up to 200,000 elements and trees that extend up to 100 levels, so really I'm just looking to for something that is easier to reason about.
Recursion is unneeded here. I believe the fastest way would be this:
public static TreeView TreeFromArray(Item[] arr)
{
var tv = new TreeView();
var parents = new TreeNodeCollection[arr.Length];
parents[0] = tv.Nodes;
foreach (var item in arr)
{
parents[item.Depth + 1] = parents[item.Depth].Add(item.Name).Nodes;
}
return tv;
}
Item is anything that has the Depth and Name information:
public class Item
{
public int Depth;
public string Name;
}
When using my own implementation of TreeNode, to simplify the procedure and strip it from unneeded functionalities that slow the whole thing down, and altering the method a little bit to suit thsoe changes, I came up with this:
Classes:
public class Node
{
public string Name;
public List<Node> Childs = new List<Node>();
}
public class Item
{
public int Depth;
public string Name;
}
Implementation:
public static Node TreeFromArray(Item[] arr)
{
var tree = new Node();
var parents = new Node[arr.Length];
parents[0] = tree;
foreach (var item in arr)
{
var curr = parents[item.Depth + 1] = new Node {Name = item.Name};
parents[item.Depth].Childs.Add(curr);
}
return tree;
}
Results:
With the given data: 1,000,000 times in 900 milliseconds
public void AddNode(Tree tree, Node nodeToAdd, int depth)
{
//you might need to add a special case to handle adding the root node
Node iterator = tree.RootNode;
for(int i = 0; i < depth; i++)
{
iterator = iterator.GetLastChild(); //I assume this method won't exist, but you'll know what to put here
}
iterator.AddChild(nodeToAdd);
}
It's kinda pseudocode-y. It doesn't add error handling, and I pretend methods exist for sections of code I imagine you could figure out on your own.
This array looks like a left-to-right "flattening" of an original tree structure. If it is safe to assume that, then the method is simple:
For each element in the array
If the depth of the element is less than or equal to the "current node"
traverse upwards to the parent until current depth = element depth -1
Create a child node of the current node
Traverse to that node as the new "current" node
Ingredients:
A class that allows for child nodes.
A stack of this class type.
An array or list of this class type.
An int to store the current depth.
A local to hold our current item of interest.
Method.
Take your first item from the list. Put it in the stack. Store 0 as the current depth.
Take each remaining item from the list in turn. Look at its depth. If the depth is equal to or less than the current depth pop off (currentdepth - depth + 1) items from the stack, and the peek in the stack to get the new current item. Make our new item a child of the "current item", make it the current item, make its depth the current depth.
Bake in a compiler at 200°C or Gas-mark 6 for 300 milliseconds, or until golden brown.

C# Treeview checking if node exists

I'm trying to populate a treeview from an XmlDocument.
The Root of the tree is set as 'Scripts' and from the root the next level should be 'Departments' which is within the XML script. I can get data from the XML document no problem. My question is when looping through the XmlDocument and adding nodes to the root node, I want to ensure that if a department is already within the treeview then it is not added again. I should also add that each Department also has a list of scripts that need to be child nodes of the department.
My code so far is:
XmlDocument xDoc = new XmlDocument();
xDoc.LoadXml(scriptInformation);
TreeNode t1;
TreeNode rootNode = new TreeNode("Script View");
treeView1.Nodes.Add(rootNode);
foreach (XmlNode node in xDoc.SelectNodes("//row"))
{
t1 = new TreeNode(node["DEPARTMENT"].InnerXml);
//How to check if node already exists in treeview?
}
Thanks.
if(treeView1.Nodes.ContainsKey("DEPARTMENT")){
//...
}
EDIT: Recursive method:
bool exists = false;
foreach (TreeNode node in treeView1.Nodes) {
if (NodeExists(node, "DEPARTMENT"))
exists = true;
}
private bool NodeExists(TreeNode node, string key) {
foreach (TreeNode subNode in node.Nodes) {
if (subNode.Text == key) {
return true;
}
if (node.Nodes.Count > 0) {
NodeExists(node, key);
}
}
return false;
}
Depending upon the size of your XML file, you could consider using an associated List for fast lookup. As you add each node to the TreeView also add it to the List.
If your XML document has a set structure where 'Departments' will always be indexed at 1;
ie:
index:[0] Scripts
index:[1] Department
index:[2] Script
index:[1] Department2
index:[2] Script
Then you could encapsulate the following code into a method where 'name' is a string parameter and the return type is boolean.
foreach (TreeNode node in uxTreeView.Nodes[0].Nodes) {
if (node.Name.ToLower() == name.ToLower()) {
return true;
}
}
return false;
The idea is you would call that function each time you encounter a 'Department' node in your Xml, before creating the TreeNode.
Full example:
private bool DepartmentNodeExists(string name) {
foreach (TreeNode node in uxTreeView.Nodes[0].Nodes) {
if (node.Name.ToLower() == name.ToLower()) {
return true;
}
}
return false;
}
Lastly, the easy way:
private bool DepartmentNodeExists(string name) {
if (uxTreeView.Nodes[0].ContainsKey(name)) {
return true;
}
else {
return false;
}
}
These are all just refactored and encapsulated into their own named methods, you of course could just call:
if (uxTreeView.Nodes[0].ContainsKey(name)) {
// do not create TreeNode
}
...during your parsing of your XML. PS. These examples all assume that you have the first root node in the TreeView already created and added to the TreeView.
http://www.vbdotnetforums.com/listviews-treeviews/13278-treeview-search.html#post39625
http://forums.asp.net/t/1645725.aspx/1?Check+if+child+Node+exists+on+treeview
You can do something like this:
TreeNode parentNode = t1.Parent;
if (parentNode != null}
{
if(parentNode.Nodes.Cast<TreeNode>().ToList().Find(t => t.Text.Equals(node["DEPARTMENT"].InnerXml) == null)
{
//Add node
}
}
else
{
bool isFound = true;
if (treeView1.Nodes.Cast<TreeNode>().ToList().Find(t => t.Text.Equals(node["DEPARTMENT"].InnerXml) == null)
{
isFound = false;
}
if(!isFound)
{
//Add node
}
}
Not sure about the document structure...
Couldn't you use Linq to Xml, load the document and get the distinct row ( row = department?) and consider only those elements to create a TreeNode? It is more efficient than trying to find if a node with such a text has already been added.
ex:
var rows = ( from row in XDocument.Load(document).Root.Elements("row")
select row
).Distinct(new SampleElementComparerOnNameAttribute());
Here the EqualityComparer is made on the "name" attribute value assuming the doc structure to be
<rows><row name='dep1'><script>script1</script><script>script2</script></row><row name='dep1'><script>script3</script><script>script4</script></row></rows>
I use,
string department = node["DEPARTMENT"].InnerXml;
TreeNode node = parentNode.Nodes[department] ?? parentNode.Nodes.Add(department, department);
That line guarantees that a lookup of the value department will be done first, if not found it creates it. You have to do the double entry in Add() so it will have a key value you can do the lookup with the .Nodes[department].
It depends on the structure of your input. Since you don't show how exactly you add your subnodes I can only point you towards either the Contains or the ContainsKey method of the Nodes property, either of the treeView1 itself, or of any subnodes you add. You should use an overload of the Add method to specify a key name to simplify lookup.

How to get xpath from an XmlNode instance

Could someone supply some code that would get the xpath of a System.Xml.XmlNode instance?
Thanks!
Okay, I couldn't resist having a go at it. It'll only work for attributes and elements, but hey... what can you expect in 15 minutes :) Likewise there may very well be a cleaner way of doing it.
It is superfluous to include the index on every element (particularly the root one!) but it's easier than trying to work out whether there's any ambiguity otherwise.
using System;
using System.Text;
using System.Xml;
class Test
{
static void Main()
{
string xml = #"
<root>
<foo />
<foo>
<bar attr='value'/>
<bar other='va' />
</foo>
<foo><bar /></foo>
</root>";
XmlDocument doc = new XmlDocument();
doc.LoadXml(xml);
XmlNode node = doc.SelectSingleNode("//#attr");
Console.WriteLine(FindXPath(node));
Console.WriteLine(doc.SelectSingleNode(FindXPath(node)) == node);
}
static string FindXPath(XmlNode node)
{
StringBuilder builder = new StringBuilder();
while (node != null)
{
switch (node.NodeType)
{
case XmlNodeType.Attribute:
builder.Insert(0, "/#" + node.Name);
node = ((XmlAttribute) node).OwnerElement;
break;
case XmlNodeType.Element:
int index = FindElementIndex((XmlElement) node);
builder.Insert(0, "/" + node.Name + "[" + index + "]");
node = node.ParentNode;
break;
case XmlNodeType.Document:
return builder.ToString();
default:
throw new ArgumentException("Only elements and attributes are supported");
}
}
throw new ArgumentException("Node was not in a document");
}
static int FindElementIndex(XmlElement element)
{
XmlNode parentNode = element.ParentNode;
if (parentNode is XmlDocument)
{
return 1;
}
XmlElement parent = (XmlElement) parentNode;
int index = 1;
foreach (XmlNode candidate in parent.ChildNodes)
{
if (candidate is XmlElement && candidate.Name == element.Name)
{
if (candidate == element)
{
return index;
}
index++;
}
}
throw new ArgumentException("Couldn't find element within parent");
}
}
Jon's correct that there are any number of XPath expressions that will yield the same node in an an instance document. The simplest way to build an expression that unambiguously yields a specific node is a chain of node tests that use the node position in the predicate, e.g.:
/node()[0]/node()[2]/node()[6]/node()[1]/node()[2]
Obviously, this expression isn't using element names, but then if all you're trying to do is locate a node within a document, you don't need its name. It also can't be used to find attributes (because attributes aren't nodes and don't have position; you can only find them by name), but it will find all other node types.
To build this expression, you need to write a method that returns a node's position in its parent's child nodes, because XmlNode doesn't expose that as a property:
static int GetNodePosition(XmlNode child)
{
for (int i=0; i<child.ParentNode.ChildNodes.Count; i++)
{
if (child.ParentNode.ChildNodes[i] == child)
{
// tricksy XPath, not starting its positions at 0 like a normal language
return i + 1;
}
}
throw new InvalidOperationException("Child node somehow not found in its parent's ChildNodes property.");
}
(There's probably a more elegant way to do that using LINQ, since XmlNodeList implements IEnumerable, but I'm going with what I know here.)
Then you can write a recursive method like this:
static string GetXPathToNode(XmlNode node)
{
if (node.NodeType == XmlNodeType.Attribute)
{
// attributes have an OwnerElement, not a ParentNode; also they have
// to be matched by name, not found by position
return String.Format(
"{0}/#{1}",
GetXPathToNode(((XmlAttribute)node).OwnerElement),
node.Name
);
}
if (node.ParentNode == null)
{
// the only node with no parent is the root node, which has no path
return "";
}
// the path to a node is the path to its parent, plus "/node()[n]", where
// n is its position among its siblings.
return String.Format(
"{0}/node()[{1}]",
GetXPathToNode(node.ParentNode),
GetNodePosition(node)
);
}
As you can see, I hacked in a way for it to find attributes as well.
Jon slipped in with his version while I was writing mine. There's something about his code that's going to make me rant a bit now, and I apologize in advance if it sounds like I'm ragging on Jon. (I'm not. I'm pretty sure that the list of things Jon has to learn from me is exceedingly short.) But I think the point I'm going to make is a pretty important one for anyone who works with XML to think about.
I suspect that Jon's solution emerged from something I see a lot of developers do: thinking of XML documents as trees of elements and attributes. I think this largely comes from developers whose primary use of XML is as a serialization format, because all the XML they're used to using is structured this way. You can spot these developers because they're using the terms "node" and "element" interchangeably. This leads them to come up with solutions that treat all other node types as special cases. (I was one of these guys myself for a very long time.)
This feels like it's a simplifying assumption while you're making it. But it's not. It makes problems harder and code more complex. It leads you to bypass the pieces of XML technology (like the node() function in XPath) that are specifically designed to treat all node types generically.
There's a red flag in Jon's code that would make me query it in a code review even if I didn't know what the requirements are, and that's GetElementsByTagName. Whenever I see that method in use, the question that leaps to mind is always "why does it have to be an element?" And the answer is very often "oh, does this code need to handle text nodes too?"
I know, old post but the version I liked the most (the one with names) was flawed:
When a parent node has nodes with different names, it stopped counting the index after it found the first non-matching node-name.
Here is my fixed version of it:
/// <summary>
/// Gets the X-Path to a given Node
/// </summary>
/// <param name="node">The Node to get the X-Path from</param>
/// <returns>The X-Path of the Node</returns>
public string GetXPathToNode(XmlNode node)
{
if (node.NodeType == XmlNodeType.Attribute)
{
// attributes have an OwnerElement, not a ParentNode; also they have
// to be matched by name, not found by position
return String.Format("{0}/#{1}", GetXPathToNode(((XmlAttribute)node).OwnerElement), node.Name);
}
if (node.ParentNode == null)
{
// the only node with no parent is the root node, which has no path
return "";
}
// Get the Index
int indexInParent = 1;
XmlNode siblingNode = node.PreviousSibling;
// Loop thru all Siblings
while (siblingNode != null)
{
// Increase the Index if the Sibling has the same Name
if (siblingNode.Name == node.Name)
{
indexInParent++;
}
siblingNode = siblingNode.PreviousSibling;
}
// the path to a node is the path to its parent, plus "/node()[n]", where n is its position among its siblings.
return String.Format("{0}/{1}[{2}]", GetXPathToNode(node.ParentNode), node.Name, indexInParent);
}
Here's a simple method that I've used, worked for me.
static string GetXpath(XmlNode node)
{
if (node.Name == "#document")
return String.Empty;
return GetXpath(node.SelectSingleNode("..")) + "/" + (node.NodeType == XmlNodeType.Attribute ? "#":String.Empty) + node.Name;
}
My 10p worth is a hybrid of Robert and Corey's answers. I can only claim credit for the actual typing of the extra lines of code.
private static string GetXPathToNode(XmlNode node)
{
if (node.NodeType == XmlNodeType.Attribute)
{
// attributes have an OwnerElement, not a ParentNode; also they have
// to be matched by name, not found by position
return String.Format(
"{0}/#{1}",
GetXPathToNode(((XmlAttribute)node).OwnerElement),
node.Name
);
}
if (node.ParentNode == null)
{
// the only node with no parent is the root node, which has no path
return "";
}
//get the index
int iIndex = 1;
XmlNode xnIndex = node;
while (xnIndex.PreviousSibling != null) { iIndex++; xnIndex = xnIndex.PreviousSibling; }
// the path to a node is the path to its parent, plus "/node()[n]", where
// n is its position among its siblings.
return String.Format(
"{0}/node()[{1}]",
GetXPathToNode(node.ParentNode),
iIndex
);
}
There's no such thing as "the" xpath of a node. For any given node there may well be many xpath expressions which will match it.
You can probably work up the tree to build up an expression which will match it, taking into account the index of particular elements etc, but it's not going to be terribly nice code.
Why do you need this? There may be a better solution.
If you do this, you will get a Path with Names of der Nodes AND the Position, if you have Nodes with the same name like this:
"/Service[1]/System[1]/Group[1]/Folder[2]/File[2]"
public string GetXPathToNode(XmlNode node)
{
if (node.NodeType == XmlNodeType.Attribute)
{
// attributes have an OwnerElement, not a ParentNode; also they have
// to be matched by name, not found by position
return String.Format("{0}/#{1}", GetXPathToNode(((XmlAttribute)node).OwnerElement), node.Name);
}
if (node.ParentNode == null)
{
// the only node with no parent is the root node, which has no path
return "";
}
//get the index
int iIndex = 1;
XmlNode xnIndex = node;
while (xnIndex.PreviousSibling != null && xnIndex.PreviousSibling.Name == xnIndex.Name)
{
iIndex++;
xnIndex = xnIndex.PreviousSibling;
}
// the path to a node is the path to its parent, plus "/node()[n]", where
// n is its position among its siblings.
return String.Format("{0}/{1}[{2}]", GetXPathToNode(node.ParentNode), node.Name, iIndex);
}
I found that none of the above worked with XDocument, so I wrote my own code to support XDocument and used recursion. I think this code handles multiple identical nodes better than some of the other code here because it first tries to go as deep in to the XML path as it can and then backs up to build only what is needed. So if you have /home/white/bob and /home/white/mike and you want to create /home/white/bob/garage the code will know how to create that. However, I didn't want to mess with predicates or wildcards, so I explicitly disallowed those; but it would be easy to add support for them.
Private Sub NodeItterate(XDoc As XElement, XPath As String)
'get the deepest path
Dim nodes As IEnumerable(Of XElement)
nodes = XDoc.XPathSelectElements(XPath)
'if it doesn't exist, try the next shallow path
If nodes.Count = 0 Then
NodeItterate(XDoc, XPath.Substring(0, XPath.LastIndexOf("/")))
'by this time all the required parent elements will have been constructed
Dim ParentPath As String = XPath.Substring(0, XPath.LastIndexOf("/"))
Dim ParentNode As XElement = XDoc.XPathSelectElement(ParentPath)
Dim NewElementName As String = XPath.Substring(XPath.LastIndexOf("/") + 1, XPath.Length - XPath.LastIndexOf("/") - 1)
ParentNode.Add(New XElement(NewElementName))
End If
'if we find there are more than 1 elements at the deepest path we have access to, we can't proceed
If nodes.Count > 1 Then
Throw New ArgumentOutOfRangeException("There are too many paths that match your expression.")
End If
'if there is just one element, we can proceed
If nodes.Count = 1 Then
'just proceed
End If
End Sub
Public Sub CreateXPath(ByVal XDoc As XElement, ByVal XPath As String)
If XPath.Contains("//") Or XPath.Contains("*") Or XPath.Contains(".") Then
Throw New ArgumentException("Can't create a path based on searches, wildcards, or relative paths.")
End If
If Regex.IsMatch(XPath, "\[\]()#='<>\|") Then
Throw New ArgumentException("Can't create a path based on predicates.")
End If
'we will process this recursively.
NodeItterate(XDoc, XPath)
End Sub
What about using class extension ? ;)
My version (building on others work) uses the syntaxe name[index]... with index omited is element has no "brothers".
The loop to get the element index is outside in an independant routine (also a class extension).
Just past the following in any utility class (or in the main Program class)
static public int GetRank( this XmlNode node )
{
// return 0 if unique, else return position 1...n in siblings with same name
try
{
if( node is XmlElement )
{
int rank = 1;
bool alone = true, found = false;
foreach( XmlNode n in node.ParentNode.ChildNodes )
if( n.Name == node.Name ) // sibling with same name
{
if( n.Equals(node) )
{
if( ! alone ) return rank; // no need to continue
found = true;
}
else
{
if( found ) return rank; // no need to continue
alone = false;
rank++;
}
}
}
}
catch{}
return 0;
}
static public string GetXPath( this XmlNode node )
{
try
{
if( node is XmlAttribute )
return String.Format( "{0}/#{1}", (node as XmlAttribute).OwnerElement.GetXPath(), node.Name );
if( node is XmlText || node is XmlCDataSection )
return node.ParentNode.GetXPath();
if( node.ParentNode == null ) // the only node with no parent is the root node, which has no path
return "";
int rank = node.GetRank();
if( rank == 0 ) return String.Format( "{0}/{1}", node.ParentNode.GetXPath(), node.Name );
else return String.Format( "{0}/{1}[{2}]", node.ParentNode.GetXPath(), node.Name, rank );
}
catch{}
return "";
}
I produced VBA for Excel to do this for a work project. It outputs tuples of an Xpath and the associated text from an elemen or attribute. The purpose was to allow business analysts to identify and map some xml. Appreciate that this is a C# forum, but thought this may be of interest.
Sub Parse2(oSh As Long, inode As IXMLDOMNode, Optional iXstring As String = "", Optional indexes)
Dim chnode As IXMLDOMNode
Dim attr As IXMLDOMAttribute
Dim oXString As String
Dim chld As Long
Dim idx As Variant
Dim addindex As Boolean
chld = 0
idx = 0
addindex = False
'determine the node type:
Select Case inode.NodeType
Case NODE_ELEMENT
If inode.ParentNode.NodeType = NODE_DOCUMENT Then 'This gets the root node name but ignores all the namespace attributes
oXString = iXstring & "//" & fp(inode.nodename)
Else
'Need to deal with indexing. Where an element has siblings with the same nodeName,it needs to be indexed using [index], e.g swapstreams or schedules
For Each chnode In inode.ParentNode.ChildNodes
If chnode.NodeType = NODE_ELEMENT And chnode.nodename = inode.nodename Then chld = chld + 1
Next chnode
If chld > 1 Then '//inode has siblings of the same nodeName, so needs to be indexed
'Lookup the index from the indexes array
idx = getIndex(inode.nodename, indexes)
addindex = True
Else
End If
'build the XString
oXString = iXstring & "/" & fp(inode.nodename)
If addindex Then oXString = oXString & "[" & idx & "]"
'If type is element then check for attributes
For Each attr In inode.Attributes
'If the element has attributes then extract the data pair XString + Element.Name, #Attribute.Name=Attribute.Value
Call oSheet(oSh, oXString & "/#" & attr.Name, attr.Value)
Next attr
End If
Case NODE_TEXT
'build the XString
oXString = iXstring
Call oSheet(oSh, oXString, inode.NodeValue)
Case NODE_ATTRIBUTE
'Do nothing
Case NODE_CDATA_SECTION
'Do nothing
Case NODE_COMMENT
'Do nothing
Case NODE_DOCUMENT
'Do nothing
Case NODE_DOCUMENT_FRAGMENT
'Do nothing
Case NODE_DOCUMENT_TYPE
'Do nothing
Case NODE_ENTITY
'Do nothing
Case NODE_ENTITY_REFERENCE
'Do nothing
Case NODE_INVALID
'do nothing
Case NODE_NOTATION
'do nothing
Case NODE_PROCESSING_INSTRUCTION
'do nothing
End Select
'Now call Parser2 on each of inode's children.
If inode.HasChildNodes Then
For Each chnode In inode.ChildNodes
Call Parse2(oSh, chnode, oXString, indexes)
Next chnode
Set chnode = Nothing
Else
End If
End Sub
Manages the counting of elements using:
Function getIndex(tag As Variant, indexes) As Variant
'Function to get the latest index for an xml tag from the indexes array
'indexes array is passed from one parser function to the next up and down the tree
Dim i As Integer
Dim n As Integer
If IsArrayEmpty(indexes) Then
ReDim indexes(1, 0)
indexes(0, 0) = "Tag"
indexes(1, 0) = "Index"
Else
End If
For i = 0 To UBound(indexes, 2)
If indexes(0, i) = tag Then
'tag found, increment and return the index then exit
'also destroy all recorded tag names BELOW that level
indexes(1, i) = indexes(1, i) + 1
getIndex = indexes(1, i)
ReDim Preserve indexes(1, i) 'should keep all tags up to i but remove all below it
Exit Function
Else
End If
Next i
'tag not found so add the tag with index 1 at the end of the array
n = UBound(indexes, 2)
ReDim Preserve indexes(1, n + 1)
indexes(0, n + 1) = tag
indexes(1, n + 1) = 1
getIndex = 1
End Function
Another solution to your problem might be to 'mark' the xmlnodes which you will want to later identify with a custom attribute:
var id = _currentNode.OwnerDocument.CreateAttribute("some_id");
id.Value = Guid.NewGuid().ToString();
_currentNode.Attributes.Append(id);
which you can store in a Dictionary for example.
And you can later identify the node with an xpath query:
newOrOldDocument.SelectSingleNode(string.Format("//*[contains(#some_id,'{0}')]", id));
I know this is not a direct answer to your question, but it can help if the reason you wish to know the xpath of a node is to have a way of 'reaching' the node later after you have lost the reference to it in code.
This also overcomes problems when the document gets elements added/moved, which can mess up the xpath (or indexes, as suggested in other answers).
This is even easier
''' <summary>
''' Gets the full XPath of a single node.
''' </summary>
''' <param name="node"></param>
''' <returns></returns>
''' <remarks></remarks>
Private Function GetXPath(ByVal node As Xml.XmlNode) As String
Dim temp As String
Dim sibling As Xml.XmlNode
Dim previousSiblings As Integer = 1
'I dont want to know that it was a generic document
If node.Name = "#document" Then Return ""
'Prime it
sibling = node.PreviousSibling
'Perculate up getting the count of all of this node's sibling before it.
While sibling IsNot Nothing
'Only count if the sibling has the same name as this node
If sibling.Name = node.Name Then
previousSiblings += 1
End If
sibling = sibling.PreviousSibling
End While
'Mark this node's index, if it has one
' Also mark the index to 1 or the default if it does have a sibling just no previous.
temp = node.Name + IIf(previousSiblings > 0 OrElse node.NextSibling IsNot Nothing, "[" + previousSiblings.ToString() + "]", "").ToString()
If node.ParentNode IsNot Nothing Then
Return GetXPath(node.ParentNode) + "/" + temp
End If
Return temp
End Function
I had to do this recently. Only elements needed to be considered. This is what I came up with:
private string GetPath(XmlElement el)
{
List<string> pathList = new List<string>();
XmlNode node = el;
while (node is XmlElement)
{
pathList.Add(node.Name);
node = node.ParentNode;
}
pathList.Reverse();
string[] nodeNames = pathList.ToArray();
return String.Join("/", nodeNames);
}
public static string GetFullPath(this XmlNode node)
{
if (node.ParentNode == null)
{
return "";
}
else
{
return $"{GetFullPath(node.ParentNode)}\\{node.ParentNode.Name}";
}
}

Categories

Resources