How can a converter for TreeView be used to create a tree structure from a list?

2.2k views Asked by At

Some function provides a flat list of strings, which looks like:

var list = new List<int>{1,2,3,4};

A TreeView now wants to transform this list into a tree with two root nodes (one for odd numbers and one for even numbers). This is just an example as the real scenario must create a much more hierachical structure. The point is that the backend provides a flat list and the view wants to transform it into a tree.

We tried a Converter as the ItemsSource but as it creates a new structure it basically breaks the binding to the original list (makes async filling impossible). Explaining why

Here is a small reproduction code:

Codebehind:

public partial class MainWindow : Window
    {
        public ObservableCollection<int> TreeViewSource { get; set; }
        public MainWindow()
        {
            InitializeComponent();
            DataContext = this;
            TreeViewSource = new ObservableCollection<int>();

            Action filler =() => { Enumerable.Range(0, 100).ToList().ForEach((i) => { Thread.Sleep(20); TreeViewSource.Add(i); }); };

            Task.Run(() => filler()); // ASYNC CALL DOES NOT WORK
            //filler(); // SYNC CALL DOES WORK
        }
    }
    public class TreeConverter : IValueConverter
    {

        public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            var sourceCollection = value as ObservableCollection<int>;          
            var outputCollection = new List<Item>();

            var odd = new Item { Text = "not divisible by 2" };
            var even = new Item { Text = "divisible by 2" };
            even.Children.AddRange(sourceCollection.Where(x => x % 2 == 0).Select(x => new Item { Text = x.ToString() }));
            odd.Children.AddRange(sourceCollection.Where(x => x % 2 != 0).Select(x => new Item { Text = x.ToString() }));

            outputCollection.Add(odd);
            outputCollection.Add(even);
            return outputCollection;
        }

        public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }
    public class Item
    {
        public string Text { get; set; }
        public List<Item> Children { get; set; }
        public Item()
        {
            Children = new List<Item>();
        }
    }

Xaml:

<Window x:Class="TreeViewAsync.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:TreeViewAsync"
        Title="MainWindow" Height="350" Width="525">
    <Window.Resources>
        <local:TreeConverter x:Key="treeConverter"/>
    </Window.Resources>
    <Grid>
        <TreeView Name="treeView" 
                  ItemsSource="{Binding TreeViewSource, Converter={StaticResource treeConverter}}">
            <TreeView.ItemTemplate>
                <HierarchicalDataTemplate DataType="{x:Type local:Item}" ItemsSource="{Binding Children}">
                    <TextBlock Text="{Binding Text}" />
                </HierarchicalDataTemplate>
            </TreeView.ItemTemplate>
        </TreeView>
    </Grid>
</Window>

This link says that a DataTemplate should be used instead of a Converter on an ItemsSource. But since the template is applied to each item individually, how can it create a full tree?

Again: The ViewModel does not need the data to be a tree and therefore does not provide a treestructure but a flat list only. The View wants to display it as a tree for convenience. Due to MVVM we would like to avoid codebehind in the View.

1

There are 1 answers

2
Dennis On BEST ANSWER

A TreeView now wants to transform this list into a tree with two root nodes The ViewModel does not need the data to be a tree and therefore does not provide a treestructure but a flat list only

You're misunderstanding View Model concept. If the View needs hierarchical data, then the View Model responsibility is to make data hierarchical and ready to be bound with view.

But since the template is applied to each item individually

Usually, templates are applied to any type of items.

public sealed class NestedItemsViewModel
{
    public string Name { get; set; }
    public int[] Items { get; set; }
}

public sealed class ViewModel
{
    private readonly List<int> list = new List<int> { 1, 2, 3, 4 };
    private NestedItemsViewModel[] items;

    public NestedItemsViewModel[] Items
    {
        get
        {
            if (items == null)
            {
                items = new[]
                {
                    new NestedItemsViewModel
                    { 
                        Name = "Even",
                        Items = list.Where(x => x % 2 == 0).ToArray() 
                    },
                    new NestedItemsViewModel
                    { 
                        Name = "Odd",
                        Items = list.Where(x => x % 2 != 0).ToArray() 
                    },                        
                };
            }
            return items;
        }
    }
}

XAML:

<!-- Assuming, that TreeView.DataContext = new ViewModel()  -->
<TreeView ItemsSource="{Binding Items, IsAsync=True}">
    <TreeView.Resources>
        <HierarchicalDataTemplate DataType="{x:Type local:NestedItemsViewModel}" ItemsSource="{Binding Items}">
            <TextBlock Text="{Binding Name}"/>
        </HierarchicalDataTemplate>
        <!-- If nested items will be more complex, then here will be their data template(s) -->
    </TreeView.Resources>
</TreeView>