Reputation: 8226
I am trying to get all menus and children that satisfy those conditions using linq:
This is the Menu class:
public class Menu
{
public string Name { get; set; }
public string Link { get; set; }
public List<Menu> Children { get; set; }
public Menu()
{
Children = new List<Menu>();
}
}
Suppose we have this data structure:
List<Menu> root = new List<Menu>();
Menu parent_1 = new Menu() { Name = "Parent 1", Link = null };
Menu parent_2 = new Menu() { Name = "Parent 2", Link = null };
//children for parent 1
Menu p1_child_1 = new Menu() { Name = "p1_child_1", Link = null };
Menu p1_child_2 = new Menu() { Name = "p1_child_2", Link = null };
//sub children of p1_child_2
Menu p1_child_1_1 = new Menu() { Name = "p1_child_1_1", Link = "l1-1" };
Menu p1_child_1_2 = new Menu() { Name = "p1_child_1_2", Link = null };
p1_child_1.Children.AddRange(new List<Menu> { p1_child_1_1 , p1_child_1_2 });
parent_1.Children.AddRange(new List<Menu> { p1_child_1, p1_child_2 });
Menu p2_child_1 = new Menu() { Name = "p2_child_1", Link = null };
Menu p2_child_2 = new Menu() { Name = "p2_child_2", Link = "l2-2" };
Menu p2_child_1_1 = new Menu() { Name = "p2_child_1_1", Link = null };
Menu p2_child_1_2 = new Menu() { Name = "p2_child_1_2", Link = null };
p2_child_1.Children.AddRange(new List<Menu> { p2_child_1_1, p2_child_1_2 });
parent_2.Children.AddRange(new List<Menu> { p2_child_1, p2_child_2 });
root.Add(parent_1);
root.Add(parent_2);
Result: The filtered list returned based on the conditions requested will be:
parent_1
p1_child_1
parent_2
How to achieve that using Linq or alternative approach taking into consideration the menu could have up to many levels?
Trying the solution as proposed in the comments, i added the extension method
public static IEnumerable<TResult> SelectHierarchy<TResult>(this IEnumerable<TResult> source, Func<TResult, IEnumerable<TResult>> collectionSelector, Func<TResult, bool> predicate)
{
if (source == null)
{
yield break;
}
foreach (var item in source)
{
if (predicate(item))
{
yield return item;
}
var childResults = SelectHierarchy(collectionSelector(item), collectionSelector, predicate);
foreach (var childItem in childResults)
{
yield return childItem;
}
}
Then called the method:
var result = root.SelectHierarchy(n => n.Children, n => n.Children.Count > 0 || n.Link != null).ToList();
However this is not what i want, I expect two menus which carry the subMenus that satisfy my condition, but i am getting 6 menus which i guess are flattened.
Although, p2_child_1
was returned since children count > 0, however it shouldn't cause its menus has no links. ( I placed the predicate as above, since i don't have other option.
Upvotes: 2
Views: 2268
Reputation: 205539
Assuming the depth is not so big to cause stack overflow, you can use a simple recursive method or recursive lambda as follows:
Func<List<Menu>, List<Menu>> filter = null;
filter = items =>
(from item in items
let children = filter(item.Children)
where item.Link != null || children.Any()
select new Menu { Name = item.Name, Link = item.Link, Children = children }
).ToList();
var filtered = filter(root);
The essential part is to process the children first (post order traversal) before filtering the parent.
Upvotes: 2
Reputation: 117019
This works for me:
public static class Ex
{
public static List<Menu> CloneWhere(this List<Menu> source, Func<Menu, bool> predicate)
{
return
source
.Where(predicate)
.Select(x => new Menu()
{
Name = x.Name,
Link = x.Link,
Children = x.Children.CloneWhere(predicate)
})
.Where(predicate)
.ToList();
}
}
The sample data looks like this:
...then I can apply this:
var result = root.CloneWhere(m => m.Children.Any() || m.Link != null);
...and I get this result:
Upvotes: 2