I have a table in a database which looks like this:
| id | parentID | name |
|----------+----------+-------------|
|ABCD-12345| | Top |
|----------+----------+-------------|
|ABCD-23456|ABCD-12345| Middle |
|----------+----------+-------------|
|ABCD-34567|ABCD-23456| Bottom |
|----------+----------+-------------|
|ABCD-45678|ABCD-23456| Bottom |
etc. - Basically, a hierarchical structure of N depth. I've taken this and shoved it into a datatable.
I have the following class built to hold this data:
public class TreeNode
{
public string id { get; set; }
public string name { get; set; }
public string parentID { get; set; }
public List<TreeNode> children { get; set; }
}
My goal is to go through each of these DataTable rows and insert them into the appropriate location in the TreeNode structure, but I'm super confused as to how I should approach this.
The main point of confusion for me is how I search through the entire existing structure of TreeNodes to see if a node with the parentID exists. Can anyone point me in the right direction?
I tried the following code, but it doesn't work:
public List<TreeNode> BuildTree(int currNode, List<TreeNode> treeList, DataTable dt)
{
foreach(DataRow row in dt.Rows)
{
if(row[1].ToString() == treeList[currNode].id)
{
treeList[currNode].children.Add(new TreeNode
{
id = row[0].ToString(),
name = row[2].ToString(),
parentID = row[1].ToString()
});
dt.Rows.Remove(row);
if(dt.Rows.Count > 0)
{
currNode++;
BuildTree(currNode, treeList, dt);
}
else
{
return treeList;
}
}
}
return null;
}
The problem is this line:
if(row[1].ToString() == treeList[currNode].id)
which gets an out of range exception, because I have a root at index 0, so on the second run (when currNode is 1), it breaks. I need to traverse to treeList[0].Children[int], followed by treeList[0].Children[int].Children[int] and so on and so forth.
So how do I accomplish this goal?
First I'm going to modify the TreeNode class for our convenience. It's not necessary, but just a nice to have. Also I'm going to assume that in your datatable you've done your error checking and there's only one node with ParentId = "".
public class TreeNode
{
public string Id { get; set; }
public string Name { get; set; }
public string ParentID { get; set; }
public List<TreeNode> Children { get; set; }
public TreeNode()
{
Id = Name = ParentID = string.Empty;
Children = new List<TreeNode>();
}
public bool IsRoot { get { return ParentID == string.Empty; } }
public bool IsChild { get { return Children == null || Children.Count == 0; } }
}
First, I'd convert your datatable data into a list of TreeNode objects. Forget about relationships, just create a list with each objects Children being empty. I wrote a method to simulate data retrival from datatable. Instead of that you can use your actual datatable.
static List<DataTableData> GetDataTableData()
{
var data = new List<DataTableData>
{
new DataTableData() { Id = "23456", ParentID = "12345", Name = "Middle" },
new DataTableData() { Id = "55555", ParentID = "12345", Name = "Middle" },
new DataTableData() { Id = "34567", ParentID = "23456", Name = "Bottom" },
new DataTableData() { Id = "12345", ParentID = string.Empty, Name = "Top" },
new DataTableData() { Id = "45678", ParentID = "23456", Name = "Bottom" },
new DataTableData() { Id = "66666", ParentID = "55555", Name = "Bottom" }
};
return data;
}
And this is what your Main() would look like:
static void Main(string[] args)
{
var treeNodes = new List<TreeNode>();
var dataTable = GetDataTableData();
foreach (var data in dataTable)
{
treeNodes.Add(new TreeNode() { Id = data.Id, Name = data.Name, ParentID = data.ParentID });
}
var root = BuildTree(treeNodes);
Console.ReadLine();
}
Now, in my BuildTree() method, instead of passing the datatable I can pass my TreeNode list, and return just the root node.
public static TreeNode BuildTree(List<TreeNode> nodes)
{
foreach (var node in nodes)
{
node.Children = nodes.Where(x => x.ParentID == node.Id).ToList();
}
return nodes.Find(x => x.IsRoot);
}
BuildTree() Breakdown
The nodes list already have all the nodes corresponding to data in your datatable. The BuildTree() is merely going to create the parent-child relations and fill in each object's Children list.
So I iterate through the list, and see what other elements in the list are supposed to be its children. When you have iterated through the list you'd created all the parent-child relationships. Finally, I pick the root node (the one who's ParentId is empty) and return it.
Edit
Here's an easy method to print and verify your tree.
static void PrintTree(TreeNode node, int indents)
{
for (int tab = 0; tab < indents; tab++)
{
Console.Write("\t");
}
Console.WriteLine("{0} - {1}", node.Id, node.Name);
if (node.Children != null && node.Children.Count > 0)
{
indents++;
foreach (var child in node.Children)
{
PrintTree(child, indents);
}
}
}
My output looks like this:
If you are building a class structure then you need a class with a recursive method. Not sure how efficient this will be if it gets too big. Execute the method from the top of the tree.
public class TreeNode
{
public string id { get; set; }
public string name { get; set; }
public string parentID { get; set; }
public List<TreeNode> children { get; set; }
public TreeNode() {
children = new List<TreeNode>();
}
public TreeNode FindParentWithID(string ID)
{
TreeNode ParentWithID = null;
//check my parentID if i am the one being looked for then return
if (id == ID) return this;
//search children
foreach (TreeNode treeNode in children)
{
ParentWithID = treeNode.FindParentWithID(ID);
if (ParentWithID != null)
{
break;
}
}
return ParentWithID;
}
}
You would load your data into the classes from the database. I had to hard code the values for the example to work:
TreeNode treeNode5 = new TreeNode() { id = "ABCD-12345", parentID = null, name = "Top" };
TreeNode treeNode6 = new TreeNode() { id = "ABCD-12346", parentID = "ABCD-12345", name = "Middle" };
treeNode5.children.Add(treeNode6);
TreeNode treeNode7 = new TreeNode() { id = "ABCD-12347", parentID = "ABCD-12346", name = "Bottom" };
TreeNode treeNode8 = new TreeNode() { id = "ABCD-12348", parentID = "ABCD-12346", name = "Bottom" };
treeNode6.children.Add(treeNode7);
treeNode6.children.Add(treeNode8);
TreeNode topOne = treeNode5.FindParentWithID("ABCD-12346");
topOne will be end up being treeNode6 name="Middle" in this example.
try this code, i have same issue and it works perfectly
this method used to build tree from list of items, by looping through all items, and add each item to its parent's child list. and return only the root item with its nested child.
public TreeNode BuildTree(List<TreeNode> source)
{
// build the children list for each item
foreach (var item in source)
{
var itm = source.Where(i => i.parentID == item.Id).ToList();
item.ChildItems = itm;
}
// return only the root parents with its child inside
return source.Where(i => i.parentID == null).FirstOrDefault();
}
noting that this method return only on TreeNode Object with its child, you can return List by changing .FirstOrDefault() to .ToList() in return line
Related
I am trying to build a data pipeline in .NET. I have been given an xsd and have used the XML Schema Definition Tool to generate C# classes that represent the object model. In order to load this data into my data store I need to transform the data coming out of the XML into structure that matches my application schema and collect/dedupe elements. To do this I have a parser class that will read a file and load the contents into local collections which will then be loaded into my database. I see two options to do this -
Manually loop through the XML with an XmlReader and pull out the data I need, loading it into the local collections in stream. This is not desirable because it does not take advantage of the strongly typed/strict xsd that I was given and requires a lot of hard coding things like while (reader.Read()), check for specific XML nodes, and then `reader.GetAttribute("HardCodedString").
Use XmlSerializer to deserialize the whole file at once and then loop through the deserialized collections and insert into my local collections. This is not desirable because the files could be very large and this method forces me to loop through all of the data twice (once to deserialize and once to extract the data to my local collections).
Ideally I would like some way to register a delegate to be executed as each object is deserialized to insert into my local collections. Is there something in the framework that allows me to do this? Requirements are as follows:
Performant - Only loop through the data once.
Functional - Data is inserted into the local collections during deserialization.
Maintainable - Utilize strongly typed classes that were generated via the xsd.
I have created a minimal example to illustrate my point.
Example XML File:
<Hierarchy xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://www.example.com/example">
<Children>
<Child ChildId="1" ChildName="First">
<Parents>
<Parent ParentId="1" ParentName="First" RelationshipStart="1900-01-01T00:00:00"/>
<Parent ParentId="2" ParentName="Second" RelationshipStart="2000-01-01T00:00:00"/>
</Parents>
</Child>
<Child ChildId="2" ChildName="Second">
<Parents>
<Parent ParentId="2" ParentName="Second" RelationshipStart="1900-01-01T00:00:00"/>
<Parent ParentId="3" ParentName="Third" RelationshipStart="2000-01-01T00:00:00"/>
</Parents>
</Child>
</Children>
</Hierarchy>
Local collections I am trying to load:
public Dictionary<int, string> Parents { get; }
public Dictionary<int, string> Children { get; }
public List<Relationship> Relationships { get; }
Manual version (not maintainable and doesn't use xsd):
public void ParseFileManually(string fileName)
{
using (var reader = XmlReader.Create(fileName))
{
while (reader.Read())
{
if (reader.NodeType == XmlNodeType.Element && reader.Name == "Hierarchy")
{
while (reader.Read())
{
if (reader.NodeType == XmlNodeType.Element && reader.Name == "Child")
{
int childId = int.Parse(reader.GetAttribute("ChildId"));
string childName = reader.GetAttribute("ChildName");
Children[childId] = childName;
while (reader.Read())
{
if (reader.NodeType == XmlNodeType.Element && reader.Name == "Parent")
{
int parentId = int.Parse(reader.GetAttribute("ParentId"));
string parentName = reader.GetAttribute("ParentName");
DateTime relationshipStart = DateTime.Parse(reader.GetAttribute("RelationshipStart"));
Parents[parentId] = parentName;
Relationships.Add(
new Relationship{
ParentId = parentId,
ChildId = childId,
Start = relationshipStart
});
}
else if (reader.NodeType == XmlNodeType.EndElement && reader.Name == "Child")
{
break;
}
}
}
}
}
}
}
}
Deserialize version (loops through the data twice):
public void ParseFileWithDeserialize(string fileName)
{
var serializer = new XmlSerializer(typeof(Hierarchy));
using (var fileStream = new FileStream(fileName, FileMode.Open))
{
var fileData = (Hierarchy) serializer.Deserialize(fileStream);
foreach (var child in fileData.Children)
{
Children[child.ChildId] = child.ChildName;
foreach (var parent in child.Parents)
{
Parents[parent.ParentId] = parent.ParentName;
Relationships.Add(
new Relationship
{
ParentId = parent.ParentId,
ChildId = child.ChildId,
Start = parent.RelationshipStart
});
}
}
}
}
You should use some annotations to get the data from the correct field in the XML, if you use these definitions;
public class Hierarchy
{
public Hierarchy()
{
Children = new List<Child>();
}
public List<Child> Children { get; set; }
}
public class Child
{
public Child()
{
Parents = new List<Parent>();
}
[XmlAttribute("ChildId")]
public int ChildId { get; set; }
[XmlAttribute("ChildName")]
public string ChildName { get; set; }
public List<Parent> Parents { get; set; }
}
public class Parent
{
[XmlAttribute("ParentId")]
public int ParentId { get; set; }
[XmlAttribute("ParentName")]
public string ParentName { get; set; }
[XmlAttribute("RelationshipStart")]
public DateTime RelationshipStart { get; set; }
}
Then you should be able to simplify your code to;
public static Hierarchy Deserialize(string fileName)
{
using (var fileStream = new StreamReader(fileName, Encoding.UTF8))
{
XmlSerializer ser = new XmlSerializer(typeof(Hierarchy));
return (Hierarchy)ser.Deserialize(fileStream);
}
}
To test it out you can create a sample data set and serialize it to a file, then use the above code to read it back
public static void Serialize(Hierarchy h, string fileName)
{
System.Xml.Serialization.XmlSerializer ser = new System.Xml.Serialization.XmlSerializer(typeof(Hierarchy));
StreamWriter sw = new StreamWriter(fileName, false, Encoding.UTF8);
ser.Serialize(sw, h);
}
Test Code
static void Test()
{
Hierarchy h = new Hierarchy();
Parent p1 = new Parent() { ParentId = 1, ParentName = "First", RelationshipStart = DateTime.Now };
Parent p2 = new Parent() { ParentId = 2, ParentName = "Second", RelationshipStart = DateTime.Now };
Parent p3 = new Parent() { ParentId = 3, ParentName = "Third", RelationshipStart = DateTime.Now };
Child c1 = new Child() { ChildId = 1, ChildName = "First" };
c1.Parents.Add(p1);
c1.Parents.Add(p2);
Child c2 = new Child() { ChildId = 2, ChildName = "Second" };
c2.Parents.Add(p2);
c2.Parents.Add(p3);
h.Children.Add(c1);
h.Children.Add(c2);
Serialize(h, AppContext.BaseDirectory + "Text.xml");
Hierarchy hReadBack = Deserialize(AppContext.BaseDirectory + "Text.xml");
}
Edit : To answer your question
Use these classes
public class Hierarchy
{
public Hierarchy()
{
Children = new List<Child>();
}
public List<Child> Children { get; set; }
private Dictionary<int, string> _parents;
private Dictionary<int, string> _childrenList;
private List<Relationship> _relationships;
private void CalcuateLists()
{
_parents = new Dictionary<int, string>();
_childrenList = new Dictionary<int, string>();
_relationships = new List<Relationship>();
foreach (Child c in this.Children)
{
if (!_childrenList.ContainsKey(c.ChildId))
{
_childrenList.Add(c.ChildId, c.ChildName);
}
foreach (Parent p in c.Parents)
{
if (!_parents.ContainsKey(p.ParentId))
{
_parents.Add(p.ParentId, p.ParentName);
}
if (_relationships.FirstOrDefault(dat => dat.ParentId == p.ParentId && dat.ChildId == c.ChildId) == null)
{
_relationships.Add(new Relationship() { ChildId = c.ChildId, ParentId = p.ParentId, Start = p.RelationshipStart });
}
}
}
}
public Dictionary<int, string> Parents {
get
{
if (_parents == null)
CalcuateLists();
return _parents;
}
}
public Dictionary<int, string> ChildrenList {
get
{
if (_childrenList == null)
CalcuateLists();
return _childrenList;
}
}
public List<Relationship> Relationships {
get
{
if (_relationships == null)
CalcuateLists();
return _relationships;
}
}
}
public class Child
{
public Child()
{
Parents = new List<Parent>();
}
[XmlAttribute("ChildId")]
public int ChildId { get; set; }
[XmlAttribute("ChildName")]
public string ChildName { get; set; }
public List<Parent> Parents { get; set; }
}
public class Parent
{
[XmlAttribute("ParentId")]
public int ParentId { get; set; }
[XmlAttribute("ParentName")]
public string ParentName { get; set; }
[XmlAttribute("RelationshipStart")]
public DateTime RelationshipStart { get; set; }
}
Then your test code becomes
public static void Test()
{
Hierarchy h = new Hierarchy();
Parent p1 = new Parent() { ParentId = 1, ParentName = "First", RelationshipStart = DateTime.Now };
Parent p2 = new Parent() { ParentId = 2, ParentName = "Second", RelationshipStart = DateTime.Now };
Parent p3 = new Parent() { ParentId = 3, ParentName = "Third", RelationshipStart = DateTime.Now };
Child c1 = new Child() { ChildId = 1, ChildName = "First" };
c1.Parents.Add(p1);
c1.Parents.Add(p2);
Child c2 = new Child() { ChildId = 2, ChildName = "Second" };
c2.Parents.Add(p2);
c2.Parents.Add(p3);
h.Children.Add(c1);
h.Children.Add(c2);
Serialize(h, AppContext.BaseDirectory + "Text.xml");
Hierarchy hReadBack = Deserialize(AppContext.BaseDirectory + "Text.xml");
Dictionary<int, string> Parents = hReadBack.Parents;
Dictionary<int, string> Children = hReadBack.ChildrenList;
List<Relationship> Relationships = hReadBack.Relationships;
}
EDIT
To get the results directly without looping
You will need this class
public class Relationship
{
public int ParentId { get; set; }
public int ChildId { get; set; }
public DateTime Start { get; set; }
}
And this selection
// Get a list of child ids and names
Dictionary<int, string> Children = (from c in hReadBack.Children select new { ChildId = c.ChildId, Name = c.ChildName}).ToDictionary(dat => dat.ChildId, dat => dat.Name);
// Get a parent ids and names
Dictionary<int, string> Parents = (from p in hReadBack.Children.SelectMany(i => i.Parents) select new { ParentId = p.ParentId, Name = p.ParentName }).Distinct().ToDictionary(dat => dat.ParentId, dat => dat.Name);
// Get the relationships
List<Relationship> Relationship = (from Child c in hReadBack.Children from Parent p in c.Parents select new Relationship() { ChildId = c.ChildId, ParentId = p.ParentId, Start = p.RelationshipStart }).ToList();
For some reason such as performance, I have to use HiarachyId in my database. I have to convert the HierarchyId data type to JSON to show up in FancyTree.
I Use the solution here but won't work. My code was
static void Main(string[] args)
{
{
var dd = new List<Field>();
dd.Add(new Field(1, "Earth", HierarchyId.Parse("/")));
dd.Add(new Field(2, "Europe", HierarchyId.Parse("/1/")));
dd.Add(new Field(3, "South America", HierarchyId.Parse("/2/")));
dd.Add(new Field(4, "Antarctica", HierarchyId.Parse("/3/")));
dd.Add(new Field(5, "Brazil", HierarchyId.Parse("/2/1/")));
dd.Add(new Field(6, "France", HierarchyId.Parse("/1/1/")));
dd.Add(new Field(7, "Germany", HierarchyId.Parse("/1/4/")));
dd.Add(new Field(8, "test", HierarchyId.Parse("/1/5/")));
dd.Add(new Field(9, "McMurdo Station", HierarchyId.Parse("/3/1/")));
dd.Add(new Field(10, "Italy", HierarchyId.Parse("/1/3/")));
dd.Add(new Field(11, "Spain", HierarchyId.Parse("/1/2/")));
dd.Add(new Field(12, "Morano", HierarchyId.Parse("/1/3/1/")));
dd.Add(new Field(13, "Rio de Janeiro", HierarchyId.Parse("/2/1/3/")));
dd.Add(new Field(14, "Paris", HierarchyId.Parse("/1/1/1/")));
dd.Add(new Field(15, "Madrid", HierarchyId.Parse("/1/2/1/")));
dd.Add(new Field(16, "Brasilia", HierarchyId.Parse("/2/1/1/")));
dd.Add(new Field(17, "Bahia", HierarchyId.Parse("/2/1/2/")));
dd.Add(new Field(18, "Salvador", HierarchyId.Parse("/2/1/2/1/")));
dd.Add(new Field(19, "tets1", HierarchyId.Parse("/2/1/3/1/")));
dd.Add(new Field(20, "test2", HierarchyId.Parse("/2/1/3/1/1/")));
dd.Add(new Field(21, "test3", HierarchyId.Parse("/2/1/3/1/1/1/")));
dd.Add(new Field(22, "test24", HierarchyId.Parse("/2/1/3/1/1/2/")));
MyClass clss = new MyClass();
var x= clss.NewMthodTest(dd);
}
}
Method to get child:
public class MyClass
{
public List<HierarchicalNode> NewMthodTest(List<Field> query)
{
var root = new HierarchicalNode("Root", 0);
foreach (var rec in query)
{
var current = root;
foreach (string part in rec.Node.ToString().Split(new[] { '/' },
StringSplitOptions.RemoveEmptyEntries))
{
int parsedPart = int.Parse(part);
current = current.Children[parsedPart - 1];
}
current.Children.Add(new HierarchicalNode(rec.FieldName, rec.Id));
}
return null; // in this method i don't know what do we suppose to return
}
}
and my input parameter class is :
public class Field
{
public Field(long id, string fieldName, HierarchyId node)
{
Id = id;
FieldName = fieldName;
Node = node;
}
public long Id { get; set; }
public string FieldName { get; set; }
public HierarchyId Node { get; set; }
}
and output class is
class HierarchicalNode
{
private readonly List<HierarchicalNode> children =
new List<HierarchicalNode>();
public List<HierarchicalNode> Children { get { return children; } }
private readonly string name;
public string Name { get { return name; } }
private readonly long id;
public long Id { get { return id; } }
public HierarchicalNode(string name, long id)
{
this.name = name;
this.id = id;
}
}
it seems something wrong and it returns this:
One of the benefits of using HierarchyId is so that you can build a tree without doing recursive calls.
I would also name things a big differently. Let's say you call your database table Nodes. Here is the table structure:
CREATE TABLE dbo.Nodes
(
[NodeId] [int] NOT NULL,
[NodeName] [nvarchar](100) NOT NULL,
[HierarchyId] [hierarchyid] NOT NULL,
[Level] AS [HierarchyId].GetLevel() PERSISTED,
CONSTRAINT Primary_Key_Nodes PRIMARY KEY CLUSTERED ([NodeId])
);
CREATE UNIQUE NONCLUSTERED INDEX
[Nodes_1]
ON
[dbo].[Nodes] ([HierarchyId] ASC);
--Important - put level as the first column to index
CREATE UNIQUE NONCLUSTERED INDEX
[Nodes_2]
ON
[dbo].[Nodes] ([Level] ASC, [HierarchyId] ASC);
Here is the SQL to return nodes for a given parent. I would wrap this up in a function called GetDescendantsAndSelf():
SELECT
[NodeId]
,[NodeName]
,[HierarchyId].ToString() AS 'HierarchyPath'
FROM
[dbo].[Nodes]
WHERE
[HierarchyId].IsDescendantOf(#parentHierarchyId) = 1
ORDER BY
[Level] ASC
,[NodeName] ASC;
My data transfer object could look like this:
public class TreeNode
{
public string Text { get; set; } = String.Empty;
public List<TreeNode> Nodes { get; set; } = new List<TreeNode>();
}
GetDescendantsAndSelf() should return a list of Node data access objects like this one:
public class Node
{
public int NodeId { get; set; }
public string NodeName { get; set; } = String.Empty;
public SqlHierarchyId HierarchyId { get; set; }
public int Level => HierarchyId.GetLevel().ToSqlInt32().Value;
}
Here is the code to build a tree:
TreeNode? rootNode = null;
Dictionary<string, TreeNode> treeBuilder = new Dictionary<string, TreeNode>();
string parentHierarchyId = "/1/2/3";
var nodes = GetDescendantsAndSelf(parentHierarchyId);
foreach (Node node in nodes)
{
TreeNode currentNode = new TreeNode() { Text = node.NodeName };
treeBuilder[node.HierarchyId.ToString()] = currentNode;
if (node.Level == 1)
{
rootNode = currentNode;
}
else
{
string parentKey = node.HierarchyId.GetAncestor(1).ToString();
treeBuilder[parentKey].Nodes.Add(currentNode);
}
}
if (rootNode is {})
{
//rootNode contains your tree structure
}
else
{
//no data found for parentHierarchyId
}
After a lot of search On the net with no result, I solve this by myself with a recursive method in c#.
I put my total code here to help people who search in the future for this question. If someone has any nice advice to make it better please leave a note and make me happy.
This is my code:
This is My Main method which calls GetTreeMethod
public List<TreeView> GetTree()
{
//get all nodes from DB to make a collection in RAM
var nodesColect = _getFieldsList.GetFieldsByNode("/");
var x = GetTreeMethod("/", nodesColect);
return x;
}
This is my main recursive method
private List<TreeView> GetTreeMethod(string nodeStr,List<FieldListDto> lstCollection)
{
List<TreeView> lst = new List<TreeView>();
HierarchyId node = HierarchyId.Parse(nodeStr);
var lastItemInCurrentLevel = GetChilds(node, lstCollection);
foreach (var item in lastItemInCurrentLevel)
{
TreeView tr = new TreeView
{
title = item.title,
id = item.id,
node = item.node,
fin = item.fin,
};
tr.children = GetTreeMethod(item.node.ToString(), lstCollection);
lst.Add(tr);
}
return lst;
}
this just gives children of a specific node
private List<TreeView> GetChilds(HierarchyId node, List<FieldListDto> lstCollection)
{
List<TreeView> child = lstCollection.Where(x => x.Node.ToString() != "/" && x.Node.GetAncestor(1).ToString() == node.ToString()).Select(q => new TreeView { id = q.Id, node = q.Node, title = q.FieldName }).ToList();
return child;
}
Models
public class FieldListDto
{
public long id { get; set; }
public string FieldName { get; set; }
public HierarchyId Node { get; set; }
}
public class TreeView
{
public long id { get; set; }
public string title { get; set; }
public HierarchyId node { get; set; }
public List<TreeView> children { get; set; }
}
here my SQL data
and here my final result
How to sum the value of child items from a hierarchical list in a loop.
I need that during the loop in the list the values of the amount and price property contain the sum of these properties of the children.
Below are the two classes to be used to help me solve my problem.
namespace SGP.Dto.Custo
{
public class PlanilhaCusto
{
public int id{ get; set; }
public int parenteId{ get; set; }
public string name { get; set; }
public decimal amount{ get; set; }
public decimal price{ get; set; }
public PlanilhaCusto(int pId, int pParenteId, pName, decimal pAmount, decimal pPrice)
{
id = pId;
parentId = pParentId;
name = pName;
amount = pAmount;
price = pPrice;
}
}
}
namespace SGP.Dto.Custo
{
public class ShowList
{
List<Dto.Custo.PlanilhaCusto> myList = new List<PlanilhaCusto>();
public void Show()
{
myList.Add(new PlanilhaCusto(1, null, "Projetos", 0, 0));
myList.Add(new PlanilhaCusto(2, 1, "Arquitetura", 5,10));
myList.Add(new PlanilhaCusto(3, 1, "Estrutura", 0, 0));
myList.Add(new PlanilhaCusto(4, 3, "Civil", 1, 50));
myList.Add(new PlanilhaCusto(5, 3, "Infra", 3, 75));
myList.Add(new PlanilhaCusto(6, null, "Pessoal", 0, 0));
myList.Add(new PlanilhaCusto(7, 6, "Mão de Obra", 20, 5700));
/*In this loop the value of the parent items must be updated
(calculated). The hierarchy of the list can be unlimited,
like a tree. I tried using a recursive method but I could
not do it.*/
foreach (var itemList in myList)
{
}
}
}
}
Assuming the Elements in the list are sorted the way they are in your example, the fastest way to do this should be to iterate through the list in reverse order and add the amount to the list entry with the correct id.
Iterate backwards through the list
When the current element has a parent add amount and amount*price to it
EDIT:
After reading your comment in your source, i assume you already knew what i just wrote.
My approach would be the following:
for (int i = myList.Count - 1; i > 1; i--)
{
var temp = myList.ElementAt(i);
if (temp.parentId != null)
{
var parent = myList.ElementAt(temp.parentId - 1);
parent.amount += temp.amount;
parent.price += (temp.amount * temp.price);
}
}
for creating a hierarchical structure, i modified your PlanilhaCusto to resemble a sort of a Node in tree, having both parent and children members, for ease the traverse
public class PlanilhaCusto
{
public int Id { get; set; }
public string Name { get; set; }
public int? ParentId { get; set; }
public decimal Amount { get; set; }
public decimal Price { get; set; }
public IEnumerable<PlanilhaCusto> Children { get; set; }
}
Given this structure and an initial input
you can build the entire tree structure regardless of how many levels there are relying on some recursion
public IEnumerable<PlanilhaCusto> Descendants(PlanilhaCusto parent, IEnumerable<PlanilhaCusto> source)
{
var query = from node in source
where node.ParentId == parent.Id
let children = Descendants(node, source)
select new PlanilhaCusto
{
Id = node.Id,
ParentId = node.ParentId,
Name = node.Name,
Amount = node.Amount,
Price = node.Price,
Children = children,
};
return query.ToList();
}
var hierarchy = from node in source
let children = Descendants(node, source)
where node.ParentId == null
select new PlanilhaCusto
{
Id = node.Id,
ParentId = node.ParentId,
Name = node.Name,
Price = node.Price,
Children = children,
};
that would project the initial data source into something similar
and from here you just need to traverse the hierarchy and compose the total price
public decimal? Total(PlanilhaCusto node)
{
decimal? price = node.Price * node.Amount;
if (node.Children != null)
{
foreach (var child in node.Children)
{
price += Total(child);
}
}
return price;
}
var totals = from node in hierarchy
select new
{
Id = node.Id,
Name = node.Name,
Total = Total(node),
};
I have a class which represents a tree:
public class Tree
{
public String id { get; set; }
public String text { get; set; }
public List<Tree> item { get; set; }
public string im0 { get; set; }
public string im1 { get; set; }
public string im2 { get; set; }
public String parentId { get; set; }
public Tree()
{
id = "0";
text = "";
item = new List<Tree>();
}
}
And the tree looks like this:
tree {Tree} Tree
id "0" string
im0 null string
im1 null string
im2 null string
item Count = 1 System.Collections.Generic.List<Tree>
[0] {Tree} Tree
id "F_1" string
im0 "fC.gif" string
im1 "fO.gif" string
im2 "fC.gif" string
item Count = 12 System.Collections.Generic.List<Tree>
parentId "0" string
text "ok" string
parentId null string
text "" string
How would I delete the node with id = someId?
For example, how would I delete the node with id = "F_123"?
All of its children should be also deleted.
I have a method which searches in the tree for a given id. I tried using that method and then setting the node to null, but it doesn't work.
Here's what I got till now:
//This is the whole tree:
Tree tree = serializer.Deserialize<Tree>(someString);
//this is the tree whose root is the parent of the node I want to delete:
List<Tree> parentTree = Tree.Search("F_123", tree).First().item;
//This is the node I want to delete:
var child = parentTree.First(p => p.id == id);
How do I delete child from tree?
So here's a fair straightforward traversal algorithm that can get the parent of a given node; it does so using an explicit stack rather than using recursion.
public static Tree GetParent(Tree root, string nodeId)
{
var stack = new Stack<Tree>();
stack.Push(root);
while (stack.Any())
{
var parent = stack.Pop();
foreach (var child in parent.item)
{
if (child.id == nodeId)
return parent;
stack.Push(child);
}
}
return null;//not found
}
Using that it's simple enough to remove a node by finding it's parent and then removing it from the direct descendants:
public static void RemoveNode(Tree root, string nodeId)
{
var parent = GetParent(root, nodeId).item
.RemoveAll(child => child.id == nodeId);
}
Find the parent node of the node to delete (id=F_1). Recursively remove it from the tree
// something like
Tree parent = FindParentNodeOf("F_1");
var child = parent.Items.First(p=> p.id="F_1");
RecurseDelete(parent, child);
private void RecurseDelete(Tree theTree, Tree toDelete)
{
foreach(var child in toDelete.item)
RecurseDelete(toDelete, child);
theTree.item.Remove(toDelete);
}
Problem
What is the fastest method to order a flat unordered set of nodes such that the parent is always listed before it's children. My current solution uses a queue to iterate the tree in a breadth first manner. However I've been wondering if there was a better / more efficient method.
Notes:
I can't pre compute any values
Id and ParentId could also be Guids (even if ints I can't guarantee sequential ids)
Linq Pad Code
void Main()
{
var nodes = new Node[] {
new Node { Id = 7, ParentId = 3 },
new Node { Id = 4, ParentId = 2 },
new Node { Id = 1, ParentId = 0 },
new Node { Id = 2, ParentId = 1 },
new Node { Id = 3, ParentId = 1 },
new Node { Id = 5, ParentId = 2 },
new Node { Id = 6, ParentId = 3 },
new Node { Id = 8, ParentId = 0 },
};
SortHierarchy(nodes).Dump();
}
IEnumerable<Node> SortHierarchy(IEnumerable<Node> list)
{
// hashtable lookup that allows us to grab references to the parent containers, based on id
var lookup = new Dictionary<int, List<Node>>();
Queue<Node> nodeSet = new Queue<Node>();
List<Node> children;
foreach (Node item in list) {
if (item.ParentId == 0) { // has no parent
nodeSet.Enqueue(item); // This will add all root nodes in the forest
} else {
if (lookup.TryGetValue(item.ParentId, out children)) {
// add to the parent's child list
children.Add(item);
} else {
// no parent added yet
lookup.Add(item.ParentId, new List<Node> { item });
}
}
}
while (nodeSet.Any()) {
var node = nodeSet.Dequeue();
if (lookup.TryGetValue(node.Id, out children)) {
foreach (var child in children) {
nodeSet.Enqueue(child);
}
}
yield return node;
}
}
private class Node {
public int Id { get; set; }
public int ParentId { get; set; }
public string Name { get; set; }
}
Research
I did find this however it wasn't quite what I was after (also the code doesn't work)
Building hierarchy objects from flat list of parent/child
This code gives the same dump() result, order the list with parentid first:
IEnumerable<Node> SortHierarchy2(IEnumerable<Node> list)
{
return list.OrderBy(l => l.ParentId);
}
This will work as long as your child ids are < their parents ids.