I have list of students that needs to be loaded as tree structure.
The following is my Employee class
public class Employee
{
public Guid Id { get; set; }
public Guid ParentId { get; set; }
public string Name { get; set; }
public bool IsDefault { get; set; }
public int Order { get; set; }
}
This is my tree view class which I want to display to the user
public class Tree
{
public Guid Id { get; set; }
public string Name { get; set; }
public bool isSelected { get; set; }
public List<Tree> children { get; set; }
}
Based on the employee list the json should be displayed something like this
The below code is giving me duplicates for each parent id and not giving the expected result.
var groupedUsers = result.GroupBy(t=>t.ParentId).SelectMany(t=>t.ToList());
foreach (var item in groupedUsers)
{
if (item.ParentId == Guid.Empty)
{
treeData.Add(new Tree()
{
Id = item.Id,
children = new List<Tree>(),
isSelected = item.IsDefault,
Name = item.Name
});
}
else
{
var parent = treeData.FirstOrDefault(t => t.Id == item.ParentId);
if (parent != null)
{
parent.children.Add(new Tree()
{
Id = item.Id,
children = new List<Tree>(),
isSelected = item.IsDefault,
Name = item.Name
});
treeData.Add(parent);
}
}
}
Update: With Robert's answer I was able to get going but it is still duplicates
the end json could be found here Tree JSON So I updated the code to something like this to eliminate duplicates. But it is not working I guess I am missing some thing in recursion?
static void Main(string[] args)
{
var text = File.ReadAllText(#"C:\Users\Dinesh\source\repos\Tests\Tests\Employee.json");
var result = JsonConvert.DeserializeObject<List<Employee>>(text);
var groupedUsers = result.GroupBy(t => t.ParentId).SelectMany(t => t.ToList()).OrderBy(t=>t.Order).ToList();
foreach (var item in groupedUsers)
{
AddEmployee(item, null, groupedUsers, treeData);
}
}
static Tree AddEmployee(Employee parentEmployee, Employee childEmployee, List<Employee> groupedUsers, List<Tree> newTreeData)
{
if (parentEmployee.ParentId == Guid.Empty)
{
var root = new Tree()
{
Id = parentEmployee.Id,
children = new List<Tree>(),
isSelected = parentEmployee.IsDefault,
Name = parentEmployee.Name
};
treeData.Add(root);
return root;
}
else
{
var parent = treeData.FirstOrDefault(t => t.Id == parentEmployee.ParentId);
if (parent == null)
{
var parentItem = groupedUsers.Single(x => x.Id == parentEmployee.ParentId);
var currentTree = newTreeData.SelectMany(t => t.children).Where(y => y.Id == parentEmployee.ParentId).ToList();
parent = AddEmployee(parentItem, parentEmployee, groupedUsers, currentTree);
}
if (childEmployee != null)
{
var childsParent = newTreeData.Where(y=>y.Id == childEmployee.ParentId).First();
return childsParent;
}
var child = newTreeData.FirstOrDefault(t => t.Id == parentEmployee.Id);
if (child != null)
return child;
child = new Tree()
{
Id = parentEmployee.Id,
children = new List<Tree>(),
isSelected = parentEmployee.IsDefault,
Name = parentEmployee.Name
};
parent.children.Add(child);
return child;
}
}
UPDATED: I think you should use recurrency. You may loose children if parent is not found.
Some draft may look like this:
var treeData = new List<Tree>();
var groupedUsers = result.GroupBy(t => t.ParentId).SelectMany(t => t.ToList());
foreach (var item in groupedUsers)
{
AddEmployee(item);
}
Tree AddEmployee(dynamic item)
{
if (item.ParentId == Guid.Empty)
{
var root = new Tree()
{
Id = item.Id,
children = new List<Tree>(),
isSelected = item.IsDefault,
Name = item.Name
};
treeData.Add(root);
return root;
}
else
{
var parent = Find(item.ParentId, treeData);
if (parent == null)
{
var parentItem = groupedUsers.Single(x => x.Id == item.ParentId);
parent = AddEmployee(parentItem );
}
var child = Find(item.Id, treeData);
if (child != null)
return child;
child = new Tree()
{
Id = item.Id,
children = new List<Tree>(),
isSelected = item.IsDefault,
Name = item.Name
};
parent.children.Add(child );
return child;
}
}
Tree Find(Guid item, List<Tree> tree)
{
var parent = tree?.FirstOrDefault(t => t.Id == item);
if (parent != null)
{
return parent;
}
if (tree != null)
{
foreach(var t in tree)
{
parent = Find(item, t?.children);
if (parent != null)
return parent;
}
}
return null;
}
UPDATE: Added Find method. You need to search through whole tree
Because it gets better formated as in the comments.
Your line treeData.Add(parent); always adds the item -
regardless of whether it already was added or not.
var parent = treeData.FirstOrDefault(t => t.Id == item.ParentId);
if (parent != null)
{
parent.children.Add(new Tree()
{
Id = item.Id,
children = new List<Tree>(),
isSelected = item.IsDefault,
Name = item.Name
});
// do not add the parent again here
}
else
{
// add the parent only if was not found above
// TODO parent = new Tree(...);
treeData.Add(parent);
}
Related
I am programming a multilevel menu / submenu in ASP.NET MVC.
I have this table:
Id Name ParentId
----------------------------------
1 Menu1 null
2 Menu2 null
3 Submenu1-menu1 1
4 Submenu2-menu1 1
5 1-Level3-submenu1 3
6 2-Level3-submenu1 3
7 3-Level3-submenu1 3
To fetch the data using Entity Framework, I wrote this code:
var category = _context.Categories
.Include(p => p.SubCategories)
.Where(p => p.ParentId == null)
.ToList()
.Select(p => new MenuItemDto
{
CatId = p.Id,
Name = p.Name,
Child = p.SubCategories.ToList().Select(child => new MenuItemDto
{
CatId = child.Id,
Name = child.Name,
Child = child.SubCategories?.ToList().Select(grandchild => new MenuItemDto
{
CatId = grandchild.Id,
Name = grandchild.Name,
}).ToList()
}).ToList(),
}).ToList();
public class MenuItemDto
{
public long CatId { get; set; }
public string Name { get; set; }
public List<MenuItemDto> Child { get; set; }
}
but the result is that just Menu1 and Menu2 and the children of Menu1 (Submenumen1-menu1 and Submenu2-menu1), I could not fetch 1-Level3-submenu1 and 2-Level3-submenu1 and 3-Level3-submenu1
I would recommend simplifying the database call and building the tree in code as this would be more efficient.
To do this create a recursive method for building the children of an parent.
In the example below I am manually generating the Database list and taking the assumption of the ParentId being a string. I am also manually looping through the output and only going to 3 levels for this example. You should be aware that you can have infinite levels if you self reference.
You can see a working example of the code below at https://dotnetfiddle.net/BCyO6I
public class MenuItemDb
{
public long Id { get; set; }
public string Name { get; set; }
public string ParentId { get; set; }
}
public class MenuItemDto
{
public long CatId { get; set; }
public string Name { get; set; }
public List<MenuItemDto> Children { get; set; }
}
public class Program{
public static void Main(string[] args){
//get all data from Context
//var categories = _context.Categories.ToList();
//for the purpose of this test manually generate categories
List<MenuItemDb> categories = new List<MenuItemDb>();
categories.Add(new MenuItemDb() {
Id = 1, Name = "Menu1", ParentId = null
});
categories.Add(new MenuItemDb() {
Id = 2, Name = "Menu2", ParentId = null
});
categories.Add(new MenuItemDb() {
Id = 3, Name = "Submenu1-menu1", ParentId = "1"
});
categories.Add(new MenuItemDb() {
Id = 4, Name = "Submenu1-menu2", ParentId = "1"
});
categories.Add(new MenuItemDb() {
Id = 5, Name = "1-Level3-submenu1", ParentId = "3"
});
categories.Add(new MenuItemDb() {
Id = 6, Name = "2-Level3-submenu1", ParentId = "3"
});
categories.Add(new MenuItemDb() {
Id = 7, Name = "3-Level3-submenu1", ParentId = "3"
});
List<MenuItemDto> menu = new List<MenuItemDto>();
//build top level
foreach(var child in categories.Where(w => w.ParentId == null))
{
MenuItemDto childDto = new MenuItemDto() {
CatId = child.Id,
Name = child.Name
};
AddChildren(childDto, categories);
menu.Add(childDto);
}
foreach(var item in menu){
Console.WriteLine(item.Name);
foreach(var childLevel1 in item.Children){
Console.WriteLine(" -- " + childLevel1.Name);
foreach(var childLevel2 in item.Children){
Console.WriteLine(" -- " + childLevel2.Name);
}
}
}
}
public static void AddChildren(MenuItemDto parent, List<MenuItemDb> categories){
parent.Children = new List<MenuItemDto>();
foreach(var child in categories.Where(w => w.ParentId == parent.CatId.ToString()))
{
var childDto = new MenuItemDto() {
CatId = child.Id,
Name = child.Name
};
AddChildren(childDto, categories);
parent.Children.Add(childDto);
}
}
}
I think you want to create tree of categories so you can do like that:
first Get all of your category and put it in to the memory for better performance of sorting.
with this you can do it:
var unsortedCategories = _context.Categories.ToList()
and then sort your category with parentId
private async Task<List<Category>> SortCategoriesForTreeAsync(
List<Category> source,
int parentId = 0,
CancellationToken cancellationToken = default)
{
if (source is null)
throw new ArgumentNullException(nameof(source));
var result = new List<Category>();
foreach (var cat in source.Where(c => c.ParentId == parentId).ToList())
{
result.Add(cat);
result.AddRange(await SortCategoriesForTreeAsync(source, cat.Id, true, cancellationToken));
}
if (ignoreCategoriesWithoutExistingParent || result.Count == source.Count)
return result;
//find categories without parent in provided category source and insert them into result
foreach (var cat in source)
if (result.FirstOrDefault(x => x.Id == cat.Id) is null)
result.Add(cat);
return result;
}
use method like this:
var sortedAllCategory = await SortCategoriesForTreeAsync(unsortedCategories)
then when you have sorted categories you can create the tree:
private async Task<List<MenuItemDto>> CreateCategories(List<Category> allSortedCategories, int categoryId, CancellationToken cancellationToken)
{
var categories = allSortedCategories
.Where(c => c.ParentId == categoryId);
var listModels = new List<MenuItemDto>();
foreach (var category in categories)
{
var categoryModel = new MenuItemDto
{
CatId = category.Id,
Name = category.Name,
};
var subCategories = await PrepareCategories(allSortedCategories, category.Id, cancellationToken);
categoryModel.Child.AddRange(subCategories);
categoryModel.HaveSubCategories = categoryModel.SubCategories.Any();
listModels.Add(categoryModel);
}
return listModels;
}
so your can use this method to create tree like this:
var result = await PrepareCategories(sortedAllCategory, 0, cancellationToken);
I have Student and Backpack entities:
internal class Student
{
public Guid Guid { get; set; }
public string Name { get; set; }
ICollection<Backpack> backpacks;
public virtual ICollection<Backpack> Backpacks
{
get
{
if (backpacks == null && LazyLoader != null)
{
backpacks = LazyLoader.Load(this, ref backpacks);
}
return backpacks;
}
set
{
backpacks = value;
}
}
ILazyLoader LazyLoader;
public Student(ILazyLoader lazyLoader)
{
LazyLoader = lazyLoader;
}
public Student()
{
}
}
internal class Backpack
{
public Backpack()
{
}
ILazyLoader LazyLoader;
public Backpack(ILazyLoader lazyLoader)
{
LazyLoader = lazyLoader;
}
public Guid Guid { get; set; }
public string Name { get; set; }
Student student;
public virtual Student Student
{
get
{
if (student == null && LazyLoader != null)
{
student = LazyLoader.Load(this, ref student);
}
return student;
}
set
{
student = value;
}
}
}
When an entity is changed and saved from one context and is used in another context, I want to update them. For example:
Scenario 1: when changing primitive properties and saved, I am updating the entry using reload:
var context1 = new MainContext();
var studentDilshodFromContext1 = context1.Set<Student>().First(s => s.Name == "Mike");
var context2 = new MainContext();
var studentMikeFromContext2 = context2.Set<Student>().First(s => s.Name == "Mike");
studentMikeFromContext1.Name = "Jake";
context1.SaveChanges();
context2.Entry(studentMikeFromContext2).Reload();
Console.WriteLine(studentMikeFromContext2.Name); //Nice! it is Jake after reloading
Scenario 2: now, I want to change the navigation property:
var context1 = new MainContext();
var jakeStudent = context1.Set<Student>().First(s => s.Name == "Jake");
var mikeBackpackFromContext1 = context1.Set<Backpack>().First(s => s.Name == "Mike's Backpack");
mikeBackpackFromContext1.Student = jakeStudent;
var context2 = new MainContext();
var mikeBackpackFromContext2 = context2.Set<Backpack>().First(s => s.Name == "Mike's Backpack");
context1.SaveChanges();
context2.Entry(mikeBackpackFromContext2).Reload();
Console.WriteLine(mikeBackpackFromContext2.Student.Name); //Object reference exception because Student is null. I am expecting Jake
Scenario 3: when the item was added to the navigation collection property, I would like to see it in context2:
var context1 = new MainContext();
var jakeStudentFromContext1 = context1.Set<Student>().First(s => s.Name == "Jake");
var newBackpack = new Backpack() { Student = jakeStudentFromContext1, Guid = Guid.NewGuid(), Name = "New Jake backpack" };
context1.Add(newBackpack);
var context2 = new MainContext();
var jakeStudentFromContext2 = context2.Set<Student>().First(s => s.Name == "Jake");
var backpacks = jakeStudentFromContext2.Backpacks;
context1.SaveChanges();
context2.Entry(jakeStudentFromContext2).Reload();
Console.WriteLine(jakeStudentFromContext2.Backpacks.Any(d => d.Guid == newBackpack.Guid)); //It is false but I am expecting true
As you can see entry.Reload() working fine only with primitive type properties, but not working with navigation properties. I tried NavigationEntry.Load and CollectionEntry.Load but they are not working as well.
So, in these scenarios how can I re-load my navigation properties?
I have a batch of users defined with that model.
I need to iterate inside Manager nesting while manager is not null and create Departments entities with parents relation. I have an example of code what I already have, so I post it here.
What I already have:
public class AdUserModel
{
public string UserName { get; set; }
public AdUserModel Manager { get; set; }
}
...
List<UserDepartment> userDepartmentsToAdd = new List<UserDepartment>();
List<Department> newDeps = new List<Department>();
RecurseDepartments(adUser);
var userDepartment = new UserDepartment
{
User = user,
PositionName = adUser.PositionName,
Department = newDeps.FirstOrDefault(x => x.Name == adUser.DepartmentName),
IsHeadUser = user.SubordinateUsers?.Any() ?? false
};
userDepartmentsToAdd.Add(userDepartment);
void RecurseDepartments(AdUserModel model)
{
var department = new Department();
var existDep = newDeps.FirstOrDefault(x => x.Name ==
model.DepartmentName);
if (existDep == null)
{
department.Name = model.DepartmentName;
}
if (model.Manager is not null)
{
if (newDeps.FirstOrDefault(x => x.Name == model.Manager.DepartmentName) == null)
{
var parentDepartment = new Department
{
Name = model.Manager.DepartmentName
};
department.ParentDepartment = existDep ?? department;
if (existDep == null)
{
newDeps.Add(department);
}
newDeps.Add(parentDepartment);
}
if (model.Manager.DepartmentName != model.DepartmentName)
{
RecurseDepartments(model.Manager);
}
}
}
Thanks for any help in advance, being stuck here for some reason.
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
So I've been bashing my head against a wall for a while on this one. What Im attempting to do is follow through a category trace defined as "category>child category> child of child category..." but for some reason, of which I am clearly failing to grasp, The categories that should have a parent sometimes fail to set their ParentId, not all the time though!?
string catString = "Category1Name>Category2Name>Category3Name";
if (!string.IsNullOrEmpty(catString)) {
string[] catStrings = catString.Split('>');
ProductCategory[] categories = new ProductCategory[catStrings.Length];
for (int j = 0; j < catStrings.Length; j++) {
string categoryName = catStrings[j];
ProductCategory parent = j > 0 ? categories[j - 1] : null;
if (j > 0) {
categories[j] = _context.ProductCategories.SingleOrDefault(x => x.Name.ToUpper().Replace(" ", "") == categoryName.ToUpper().Replace(" ", "") && x.ParentId == parent.Id);
} else {
categories[j] = _context.ProductCategories.SingleOrDefault(x => x.Name.ToUpper().Replace(" ", "") == categoryName.ToUpper().Replace(" ", ""));
}
if (categories[j] == null) {
if (j > 0) {
categories[j] = new ProductCategory { Name = categoryName, ParentId = parent.Id };
if (parent.Children == null) {
parent.Children = new List<ProductCategory>();
}
parent.Children.Add(categories[j]);
} else {
categories[j] = new ProductCategory { Name = categoryName };
}
_context.ProductCategories.Add(categories[j]);
categoriesCreated++;
}
}
product.Category = categories.Last();
}
A Category is defined as such
public int Id { get; set; }
public string Name { get; set; }
public int ParentId { get; set; }
public virtual List<ProductCategory> Children { get; set; }
String Examples
Glassware>Shot Glasses
Glassware>Wine Glasses
Glassware>Glass Carafes/Decanters/Jugs>Glass Carafes
Glassware>Glass Carafes/Decanters/Jugs>Glass Jugs
Tableware>Cutlery>Premium 18/10
Something simple like this works, using a TreeNode object to contain everything.
public class SO50846156
{
public TreeNode Test(String[] data)
{
var root = new TreeNode();
foreach (var line in data)
{
var split = line.Split('>');
var count = split.Length;
var item = root.Nodes.Add(split[0]);
if (1 < count)
{
var subCat = item.Nodes.Add(split[1]);
if (2 < count)
{
var catName = subCat.Nodes.Add(split[2]);
}
}
}
return root;
}
}
The root element gets created every time, and you get a node for each line of your data.
I'm not sure what yours is trying to accomplish that is different, but maybe this gives you some ideas.