1

To produce a flat structure in the ListView's GridView? (the same collection is already bound to a treeview which is why it is in a Hierarchical Structure and there are already a lot of methods that manipulate the data in this structure so I would rather keep it as it is).

The data looks like this:

class Node
{
  ObservableCollection<Node> Children;
  ...
}

At the top level it is all contained in a collection itself:

ObservableCollection<Node> nodes;

Now I want all the Children at a certain level (but could be in many branches) in my list view...One way seems to be maintaining a cloned copy but it looks terribly inefficant, I just want to bind to the same collection.

markmnl
  • 11,116
  • 8
  • 73
  • 109

5 Answers5

2

What you're trying to do here is hard. Flattening a hierarchy's not hard - it's pretty easy to build a method that traverses a tree of T objects and returns an IEnumerable<T>. But what you want is much harder: you want the flattened list to be maintained in sync with the tree.

It's possible to do this. You can, in principle at least, have each node in the hierarchy know its position in the flattened list, and then translate CollectionChanged events on its children into an something that the flattened list can deal with. That might work if you were only handling single-item add and remove operations.

There's a much easier way. Don't use a ListView. Display your data in a HeaderedItemsControl, and use a HierarchicalDataTemplate, as described in the answer to this question. Only don't set a left margin on the ItemsPresenter. This will present all of the items in a single column. You'll know that some of the items are parents and some are children, but the user won't.

If you want a columnar layout, use a Grid as the template, and use shared size scopes to control the column widths.

Community
  • 1
  • 1
Robert Rossney
  • 94,622
  • 24
  • 146
  • 218
  • 1
    Thanks, thats the kind of answer I was hanging out for, though I have got it working acceptably with capturing CollectionChanged - even when a large number of items are added - the event is called for every single one (see my answer). I will check out what you have proposed later, but im unlikely to change what i have done now. Thanks anyway. – markmnl Feb 17 '11 at 08:53
  • This is a great solution. Thank you! –  Feb 12 '19 at 09:44
1

Maintaining a new collection which all nodes add to/remove from when their ChildrenCollection changes seems the best. One can catch a Node's Children's CollectionChanged event:

    void ChildrenCollection_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
    {
        // ASSUMPTION: only one item is ever added/removed so will be at NewItems[0]/OldItems[0]

        switch (e.Action)
        {
            case NotifyCollectionChangedAction.Add: nodes.AllChildren.Add(e.NewItems[0]);break;
            case NotifyCollectionChangedAction.Remove: nodes.AllChildren.Remove(e.OldItems[0]); break;
            case NotifyCollectionChangedAction.Replace:
                {
                    int i = nodes.AllChildren.IndexOf(e.OldItems[0]);
                    nodes.AllChildren.RemoveAt(i);
                    nodes.AllChildren.Insert(i, e.NewItems[0]);
                }
                break;
            case NotifyCollectionChangedAction.Reset:
                {
                    nodes.AllChildren.Clear();
                    foreach (Node n in this.ChildrenCollection)
                        nodes.AllChildren.Add(n);
                }
                break;
            // NOTE: dont't care if it moved
        }
    }

Where 'nodes' is a reference to the top level collection.

You can then bind you ListView.ItemsSource to the AllChildren which if it is an ObervableCollection will stay up to date!

NOTE: Should Properties in a Node change they will not be reflected in the AllChildren collection - it is only the addition/removal and replacement of nodes in one the ChildrenCollection's that will replicate itself in the AllChildren collection.

NOTE II: You have to be careful where before you could just replace a node in the tree thereby forfieting the entire branch below, you now have to do a depth first removal of all nodes in the that branch so the "mirror" AllChildren collection is updated too!

markmnl
  • 11,116
  • 8
  • 73
  • 109
0

Edit: For efficient flattening the CompositeCollection has been quite useful to me.


I would use a value converter for that, then your binding source can stay the same.

Edit: The converter might look something like this (untested!):

[ValueConversion(typeof(ObservableCollection<Node>), typeof(List<Node>))]
public class ObservableCollectionToListConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        ObservableCollection<Node> input = (ObservableCollection<Node>)value;
        int targetLevel = int.Parse(parameter as string);
        List<Node> output = new List<Node>();

        foreach (Node node in input)
        {
            List<Node> tempNodes = new List<Node>();
            for (int level = 0; level < targetLevel; level++)
            {
                Node[] tempNodesArray = tempNodes.ToArray();
                tempNodes.Clear();
                foreach (Node subnode in tempNodesArray)
                {
                    if (subnode.Children != null) tempNodes.AddRange(subnode.Children);
                }
            }
            output.AddRange(tempNodes);
        }

        return output;
    }

    public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        throw new NotSupportedException();
    }
}

You would use it like this in Xaml:

<Window.Resources>
    ...
    <local:ObservableCollectionToListConverter x:Key="ObservableCollectionToListConverter"/>
</Window.Resources>
...
<ListView ItemsSource="{Binding MyNodes, Converter={StaticResource ObservableCollectionToListConverter}, ConverterParameter=3}">

(ConverterParameter specifies the level)

H.B.
  • 166,899
  • 29
  • 327
  • 400
  • same problem as the other two, by creating a copy how do I know when to create the copy, everytime a node is added or removed? then when 1000 nodes are added the binding would be updated 1000 times calling the converter 1000 times which creates a new copy of all nodes 999 time needlessly – markmnl Feb 17 '11 at 01:07
  • Well, personally i would not deal with such problems until it *actually* becomes a problem in terms of performance. You could add a boolean property to the converter which turns it on or off. I doubt that there is any much easier way to do something like this, you might be able to improve certain areas but in the end your product will always be a list consisting of merged lists. – H.B. Feb 17 '11 at 01:34
  • Sorry but creating a new copy of list 999 times and that copy will just be disposed of when 1000 items are added and only the 1000th copy created is used is not acceptable - I could have many more than 1000 items. Turning the converter on and off is not an option as I do not know how many items may be about to be added. Incidentally I tend to use converters only what they are designed for: converting objects between the target and source of different types, it is better to use an ObjectDataProvider if populating from a method. – markmnl Feb 17 '11 at 02:07
  • Actually that converter does not even do that since the source is the root ObservableCollection which should not change very often. You'd probably need to fire additional events or use dedicated properties. Anyway, as i said, i doubt you'll find any efficient way to do this with your current data structures. Maybe you should consider encapsulating your collections so you can insert any items which are added to any child collection to a static collection of the level at which they where added or something like that. – H.B. Feb 17 '11 at 03:16
0

Add a method to your DataContext that returns a IEnumerable and bind that to your ListView.

Within the new method, return the result of a LINQ query of the hierarchical data collection. You won't have a "shadow copy" of the collection as long as you don't .ToList() your results and you'll get up-to-date results as long as your observable collections or INotifyCollectionChanged events are properly implemented.

Dave White
  • 3,451
  • 1
  • 20
  • 25
  • The problem here is I dont think the list would be kept up to date: after the query is executed it wont be executed again unless I tell it to after say eveytime one of the observable collections changes - but then I would be querying it everytime a single node is added/removed - what about when 1000's are added then the query is run a 1000 times! – markmnl Feb 17 '11 at 01:03
  • The query would only be run when the WPF binding system queried for an updated result, which would only happen if you fired the INotifyCollectionChanged event or if the observable collection fired the events on it's own. I don't know if you can tell an observable collection (or all observable collections within a collection) to be silent while you load them up. I'll have to look into that. But regardless, WPF will not query for an update on the method unless you provide some sort of notification to the system. And if the binding doesn't call an update, your LINQ query will not be executed. – Dave White Feb 17 '11 at 02:31
  • unfortuantly i will not know how many items are about to be added, even after the initial load it could be many many items are added, or just one – markmnl Feb 17 '11 at 02:55
  • You wouldn't need to know. You would just need to control when you tell WPF (via the event on the INotifyCollectionChanged interface) when to update the binding. In this case, an observable collection may not be the best choice, so you could use an object that derives from List and implements INotifyCollectionChanged. – Dave White Feb 17 '11 at 13:03
  • Yes but then if I updated the list every time anytime one of the children, grand children etc. was added removed I would be querying the entire structure each time and assigning that to my list, which would mean running the query 1000 times if 1000 nodes where added. I really only want the single added node appended to my list.. – markmnl Feb 19 '11 at 04:14
  • If I did know a lot of nodes were about to be added/removed i could supress update notifications until all had been added, but as I said unfortunately I dont! Its a more difficult problem then anticipated! – markmnl Feb 19 '11 at 04:18
0

You can create a recursive method that yield returns an IEnumerable and make a property that returns the value of that method. Not sure if the example below would work but it's the idea:

public Node MainNode { get; set;}

public IEnumerable<Node> AllNodes
{
    get { return GetChildren(MainNode); }
}

public IEnumerable<Node> GetChildren(Node root)
{
    foreach (Node node in root.Nodes)
    {
        if (node.Nodes.Count > 0)
            yield return GetChildren(node) as Node;

        yield return node;
    }
}

Another option around the same idea would be that the AllNodes property calls a method that recursively loads all the Nodes in a flat list, then returns that list (if you don't want to use the yield return):

public ObservableCollection<Node> AllNodes
{
    get
    {
        ObservableCollection<Node> allNodes = new ObservableCollection<Node>();

        foreach(Node node in GetChildren(this.MainNode))
            allNodes.Add(node);

        return allNodes;
    }
}
Carlo
  • 25,602
  • 32
  • 128
  • 176
  • but then when to bind to AllNodes, I need to keep the list up to date, how do I know when the binding should read the source again, after single node is added/removed? then when I add 1000 nodes AllNodes is called 1000 times! – markmnl Feb 17 '11 at 01:05
  • You're right. You'd need some special handling like calling NotifyPropertyChanged for AllNodes once the adding 1000 items transaction is finished, so it only calls it once. – Carlo Feb 17 '11 at 01:08