2
votes

I have a big problem that a recursive doesn't update any childs if I'm the ItemSource () with a . I made up a small example WPF app to explain the irritating behaviour.

First I have a class named Folder. A collection of Folders is delivered from the DataLayer in the real app. Here's the simple example class:

// Represents a Folder from the database
public class Folder
{
    public string Label { get; set; }

    // Data recursion
    public ObservableCollection<Folder> Children { get; set; }

    public Folder()
    {
        Children = new ObservableCollection<Folder>();
    }
}

In the presentation layer I need more properties to view it in a treeview. I'm not able to alter the Folder class.

// Hold additional data to a Folder
public class TreeViewFolder
{
    public Folder Folder { get; set; }

    public bool IsExpanded { get; set; }
}

I hava a ViewModel class in my example that generates some root-level test data. Via a task I simulate a change in the test data after 3 seconds: Adding one root and one child Folder.

// ViewModel to MainWindow.xaml
public class ViewModel
{
    public ObservableCollection<Folder> Folders { get; set; }
    public ObservableCollection<TreeViewFolder> TreeViewFolders { get; set; }

    public ViewModel()
    {
        // childfolder
        var childs = new ObservableCollection<Folder>();
        childs.Add(new Folder{Label="3.1"});

        // Generate some test data
        Folders = new ObservableCollection<Folder>
                      {
                          new Folder {Label = "1."},
                          new Folder {Label = "2."},
                          new Folder {Label = "3.", Children = childs},
                          new Folder {Label = "4."}
                      };

        // Add the test data to new TreeViewFolders
        TreeViewFolders = new ObservableCollection<TreeViewFolder>
                      {
                          new TreeViewFolder{Folder = Folders[0]},
                          new TreeViewFolder{Folder = Folders[1]},
                          new TreeViewFolder{Folder = Folders[2]},
                          new TreeViewFolder{Folder = Folders[3]}
                      };

        // This is the UI Thread
        Execute.InitializeWithDispatcher();

        // Wait for 3 seconds...
        Task.Factory.StartNew(() =>
            System.Threading.Thread.Sleep(3000))
            // ...and then add a new main folder and one child folder
            .ContinueWith((pre) =>
                Execute.InvokeOnUIThread(() => {
                    Folders.Add( new Folder() {Label = "5."});
                    TreeViewFolders.Add(new TreeViewFolder{Folder = Folders[4]});
                    Folders[1].Children.Add(new Folder {Label = "2.1"});
                }));
    }
}

The converter implements a dictionary to keep track of all Folders that are allready a TreeViewFolder and is able to convert one Folder or a ObservableCollection into TreeViewFolder or ObservableCollection. The back-conversion is not implemented.

    public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {

        //DictionaryLookUp just holds a Folder <-> TreeViewFolder relation in a Dictionary<Folder, TreeViewFolder> for performance issues

        // Single object
        if (targetType == typeof(TreeViewFolder)) {
            var inputFolder = value as Folder;
            if (inputFolder == null) return null;
            return DictionaryLookUp(inputFolder);
        }

        // Collection of Folders
        // WPF SAYS IT WANTS AN IENUMERABLE??? I GIVE AN OBSERVABLECOLLECTION
        if (targetType == typeof(IEnumerable)) {
            var inputFolders = value as IEnumerable<Folder>;
            if (inputFolders == null) return null;

            var outputFolders = new ObservableCollection<TreeViewFolder>();

            foreach (var folder in inputFolders) {
                outputFolders.Add(DictionaryLookUp(folder));
            }

            return outputFolders;
        }
        //Error!
        return null;
    }

In the MainWindow.xaml I have just two TreeViews. The first one is again just to demonstrate that everything works when I use the original Folder (not the TreeViewFolder). The second TreeView is how I need it.

<Window x:Class="TreeViewConverterBinding.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:TreeViewConverterBinding"
        Title="MainWindow" Height="350" Width="525">
    <Window.DataContext>
        <local:ViewModel />
    </Window.DataContext>
    <Window.Resources>
        <local:FolderToTreeViewFolderConverter x:Key="FtTVFConverter" />
    </Window.Resources>
    <StackPanel >
        <Label Content="TreeView for Folders: Binding without Converter" />
        <TreeView ItemsSource="{Binding Folders}">
            <TreeView.ItemTemplate>
                <HierarchicalDataTemplate 
                    DataType="{x:Type local:Folder}"
                    ItemsSource="{Binding Children}">
                    <StackPanel Orientation="Horizontal">
                        <TextBlock Text="{Binding Label}"/>
                    </StackPanel>
                </HierarchicalDataTemplate>
            </TreeView.ItemTemplate>
        </TreeView>
        <Label Content="TreeView for TreeViewFolders: Binding to same Source with Converter" />
        <TreeView ItemsSource="{Binding TreeViewFolders}">
            <TreeView.ItemTemplate>
                <HierarchicalDataTemplate 
                    DataType="{x:Type local:TreeViewFolder}"
                    ItemsSource="{Binding Folder.Children, Converter={StaticResource FtTVFConverter}}">
                    <StackPanel Orientation="Horizontal">
                        <TextBlock Text="{Binding Folder.Label}"/>
                        <TextBlock Text=" Folder.Children.Count=" />
                        <TextBlock Text="{Binding Folder.Children.Count}"/>
                    </StackPanel>
                </HierarchicalDataTemplate>
            </TreeView.ItemTemplate>
        </TreeView>
    </StackPanel>
</Window>

Furthermore I bind a special TextBlock to the Count of the Children of a Folder.

The issue here is, that in the second TreeView the TextBlock displays a Count=1 after the 3 seconds at entry "2." but no [+] appears in the TreeView. The [+] is there in entry "3." though. The upper TreeView works great!

Can anyone please tell me whats the problem here?? How do I make the [+] appear in the 2nd entry of the bottom TreeView?

I know that there are other methods to expand the Folder with the properties in the TreeViewFolder (inheritance, reflection, ...) but that are not suitable for real app. The main issue here is, that one Folder only exists one time for the whole application whereat the corresponding TreeViewFolder exists n times with different properties...

Thanks in advance Soko

2
if i use an icommand instead of your task.factory to do your add folder stuff it works for me. i also get rid of the converter in this case.blindmeis
Thanks for the input. But I must use the converter as I need TreeViewFolder instances in all levels of the TreeView. As a matter of fact if you just delete the ", Converter={StaticResource FtTVFConverter}" in the ItemSource binding everything works - I'm aware of that. But that is not what I need...Soko

2 Answers

1
votes

It seems that the problem here is that WPF can not convert a ObservableCollection into a ObservableCollection. The targetType of IEnumerable in the Convert() method gives a good hint to that issue. I've found no way to tell WPF to request a ObservableCollection. Even if I return an ObservableCollection WPF doesn't treat it like one...

So after a lot of try and error I came up with a workaround that solves the problem. Basically it uses the Count Property of the collection to trigger the update in WPF.

Here's the altered section in xaml of the initial post:

                <HierarchicalDataTemplate.ItemsSource>
                    <MultiBinding Converter="{StaticResource FtTVFMultiConverter}">
                        <Binding Path="Folder.Children" />
                        <Binding Path="Folder.Children.Count" />
                    </MultiBinding>
                </HierarchicalDataTemplate.ItemsSource>

FtTVFMultiConverter is a class that implements only the Convert() of the IMultiValueConverter Interface:

    public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
    {
        var inputFolders = values[0] as ObservableCollection<Folder>;
        if (inputFolders == null) return null;

        var outputTreeViewFolders = new ObservableCollection<TreeViewFolder>();

        foreach (var folder in inputFolders) {
            outputTreeViewFolders.Add(new TreeViewFolder { Folder = folder });
        }

        return outputTreeViewFolders;
    }

So a change of the source in the ViewModel gets triggered by the change of the Folder.Children.Count property. Hence the Convert method is called which delivers the new added Childs.

0
votes

You said ...

The converter is nothing special. It implements a dictionary to keep track of all Folders that are allready a TreeViewFolder and is able to convert one Folder or a IEnumerable into TreeViewFolder or IEnumerable. The back-conversion is not implemented.

But thats whats causing all this, isnt it! By converting your observable collection to a dictionary (which is a non-observable IEnumerable) you have lost the constant update-ability of the WPF GUI!!

Either you need to implement a custom dictionary with INotifyCollectionChanged interface and then as you handle the Folders.CollectionChanged event, you synchronize your dictionary according to what has been changed in the observable collection and raise its own CustomDictionarty.CollectionChanged event.