Vaccano
Vaccano

Reputation: 82301

WPF - Good Way to take a list to a Tree

I have a list that looks like this:

Base/Level1/Item1
Base/Level1/Item2
Base/Level1/Sub1/Item1
Base/Level2/Item1
Base/Level3/Sub1/Item1

I would like an easy way put that into a ListView. (Ie similar to this)

Base
  |
  +->Level1
  |    |
  |    +=Item1
  |    +=Item2
  |    |
  |    +->Sub1
  |        |
  |        +=Item1
  |
  +->Level2
  |    |
  |    +=Item1

  |
  +->Level3
       |
       +->Sub1
           |
           +=Item1

Is there an established way to make this kind of conversion or do I just need to roll my own parser?

(In case it may be relevant the real items in my code are TFS Iteration Paths.)

Upvotes: 5

Views: 2926

Answers (3)

silentman.it
silentman.it

Reputation: 361

A more generic implementation could be this one. Imagine a Node class defined as:

public class Node<TItem, TKey>
{
    public TKey Key { get; set; }
    public int Level { get; set; }
    public IEnumerable<TItem> Data { get; set; }
    public List<Node<TItem, TKey>> Children { get; set; }
}

and two generic IEnumerable<T> extension methods:

public static List<Node<TItem, TKey>> ToTree<TItem, TKey>(this IEnumerable<TItem> list, params Func<TItem, TKey>[] keySelectors)
{
    return list.ToTree(0, keySelectors);
}

public static List<Node<TItem, TKey>> ToTree<TItem, TKey>(this IEnumerable<TItem> list, int nestingLevel, params Func<TItem, TKey>[] keySelectors)
{
    Stack<Func<TItem, TKey>> stackSelectors = new Stack<Func<TItem, TKey>>(keySelectors.Reverse());
    if (stackSelectors.Any())
    {
        return list
            .GroupBy(stackSelectors.Pop())
            .Select(x => new Node<TItem, TKey>()
            {
                Key = x.Key,
                Level = nestingLevel,
                Data = x.ToList(),
                Children = x.ToList().ToTree(nestingLevel + 1, stackSelectors.ToArray())
            })
            .ToList();
        }
        else
        {
            return null;
        }

You can use these methods to aggregate a flat list of user objects to a tree, with an arbitrary aggregation level and a more elegant syntax. The only limitation is that the aggregation keys must be of the same type.

Example:

class A
{
    public int a { get;set; }
    public int b { get;set; }
    public int c { get;set; }
    public int d { get;set; }
    public string s { get;set; }

    public A(int _a, int _b, int _c, int _d, string _s)
    {
        a = _a;
        b = _b;
        c = _c;
        d = _d;
        s = _s;     
    }
}

void Main()
{
    A[] ls = {
        new A(0,2,1,10,"one"),
        new A(0,1,1,11,"two"),
        new A(0,0,2,12,"three"),
        new A(0,2,2,13,"four"),
        new A(0,0,3,14,"five"),
        new A(1,0,3,15,"six"),
        new A(1,1,4,16,"se7en"),
        new A(1,0,4,17,"eight"),
        new A(1,1,5,18,"nine"),
        new A(1,2,5,19,"dunno")
    };

    var tree = ls.ToTree(x => x.a, x => x.b, x => x.c, x => x.d);

}

Notes: This implementation is not a "real" tree, since there is no single root node, but you can implement a Tree<TItem, TKey> wrapper class quite easily.

HTH

Upvotes: 1

Ray Burns
Ray Burns

Reputation: 62909

This will take your list of strings and turn it into a tree suitable for viewing with TreeView as you described:

public IList BuildTree(IEnumerable<string> strings)
{
  return
    from s in strings
    let split = s.Split("/")
    group s by s.Split("/")[0] into g  // Group by first component (before /)
    select new
    {
      Name = g.Key,
      Children = BuildTree(            // Recursively build children
        from s in grp
        where s.Length > g.Key.Length+1
        select s.Substring(g.Key.Length+1)) // Select remaining components
    };
}

This will return a tree of anonymous types, each containing a Name property and a Children property. This can be bound directly to the TreeView by specifying a HierarchicalDataTemplate with ItemsSource="{Binding Children}" and content consisting of a <TextBlock Text="{Binding Name}"> or similar.

Alternatively you could define a tree node class in code if you want additional members or semantics. For example, given this node class:

public class Node 
{
  public string Name { get; set; } 
  public List<Node> Children { get; set; } 
}

your BuildTree function would be slightly different:

public List<Node> BuildTree(IEnumerable<string> strings)
{
  return (
    from s in strings
    let split = s.Split("/")
    group s by s.Split("/")[0] into g  // Group by first component (before /)
    select new Node
    {
      Value = g.Key,
      Children = BuildTree(            // Recursively build children
        from s in grp
        where s.Length > g.Key.Length+1
        select s.Substring(g.Key.Length+1)) // Select remaining components
    }
    ).ToList();
}

Again this can be bound directly using a HierarchicalDataTemplate. I generally use the first solution (anonymous types) unless I want to do something special with the tree nodes.

Upvotes: 5

Thomas Levesque
Thomas Levesque

Reputation: 292405

The WPF TreeView can display hierarchical data using HierarchicalDataTemplates. However, currently your data is flat, so you will have to transform it to a hierarchical data structure. There is no built-in way to do that for you...

For instance, you can create a class like that :

class Node
{
    public string Name { get; set; }
    public List<Node> Children { get; set; }
}

Parse your flat data into a list of Node objects with subnodes , and create a TreeView with the following HierarchicalDataTemplate in the resources :

<TreeView ItemsSource="{Binding ListOfNodes}">
  <TreeView.Resources>
    <HierarchicalDataTemplate DataType="{x:Type local:Node}" ItemsSource="{Binding Children}">
      <TextBlock Text="{Binding Name}" />
    </HierarchicalDataTemplate>
  </TreeView.Resources>
</TreeView>

The tree nodes will be automatically generated based on your data. If you need different classes for different levels of the hierarchy, create a different HierarchicalDataTemplate for each class

Upvotes: 2

Related Questions