Search code examples
c#wpfxamldata-bindingtreeview

How do I write the DataTemplates for a TreeView when all the items are in a wrapper?


I'm working on a WPF project. I recently did a refactoring that put some of my model objects into wrappers. I've abridged the classes so they read nicely:

public class Wrapper
{
    public AbstractModel Model; 
}

public abstract class AbstractModel 
{
    public string DisplayName;
}

public class Parent : AbstractModel
{
    public ObservableCollection<Wrapper> MyChildren;
}

public class Child : AbstractModel
{
    //Nothing relevant to the issue (I think).
}

//a .xaml.cs file
public partial class MyPropertiesControl
{
    //This is actually a dependency property that I bind to in other views, but I was too lazy to write the whole thing out.
    public ObservableCollection<Wrapper> ItemsToDisplay { get; }
}

I want to display a TreeView that shows the collection of Wrappers and their (potential) children. Previously, there was no Wrapper class, just AbstractModel, Parent, and Child. It was quite simple to create the tree view in that case:

<TreeView ItemsSource="{Binding ElementName=Root, Path=ItemsToDisplay}">
    <TreeView.Resources>
        <HierarchicalDataTemplate DataType="{x:Type Parent}" ItemsSource="{Binding MyChildren}">
            <Label Content="{Binding DisplayName}"/>
        </HierarchicalDataTemplate>

        <DataTemplate DataType="{x:Type Child}">
            <Label Content="{Binding DisplayName}" />
        </DataTemplate>
    </TreeView.Resources>
</TreeView>

Now that the objects are in wrappers, I can't figure out how to write the templates. I tried a bunch of different things. These are two ideas I tried:

<TreeView ItemsSource="{Binding ElementName=Root, Path=ItemsToDisplay}">
    <TreeView.Resources>
        <DataTemplate DataType="{x:Type Wrapper}">
            <ContentControl Content="{Binding Model}">
                <ContentControl.Resources>
                    <HierarchicalDataTemplate DataType="{x:Type Parent}" ItemsSource="{Binding MyChildren}">
                        <Label Content="{Binding DisplayName}"/>
                    </HierarchicalDataTemplate>

                    <DataTemplate DataType="{x:Type Child}">
                        <Label Content="{Binding DisplayName}"/>
                    </DataTemplate>

                </ContentControl.Resources>
            </ContentControl>
        </DataTemplate>
    </TreeView.Resources>
</TreeView>
<TreeView ItemsSource="{Binding ElementName=Root, Path=ItemsToDisplay}">
    <TreeView.Resources>
        <DataTemplate DataType="{x:Type Wrapper}">
            <ContentControl Content="{Binding Model}">
                <ContentControl.Resources>
                    <HierarchicalDataTemplate DataType="{x:Type Parent}" ItemsSource="{Binding MyChildren}">
                        <HierarchicalDataTemplate.Resources>
                            <DataTemplate DataType="{x:Type Wrapper}">
                                <ContentControl Content="{Binding Model}">
                                    <ContentControl.Resources>
                                        <DataTemplate DataType="{x:Type Child}">
                                            <Label Content="{Binding DisplayName}"/>
                                        </DataTemplate>
                                    </ContentControl.Resources>
                                </ContentControl>
                            </DataTemplate>
                        </HierarchicalDataTemplate.Resources>

                        <Label Content="{Binding DisplayName}"/>
                        
                    </HierarchicalDataTemplate>

                </ContentControl.Resources>
            </ContentControl>
        </DataTemplate>
    </TreeView.Resources>
</TreeView>

In both cases, I only see the first level of parents, I don't see any children. Not even the default class name that WPF will use when there isn't a defined template for that type, but the parent nodes actually show no children at all.

I added an event handler to the mouse dragging over the tree view and put a breakpoint on the event handler so I could make sure all the collections were populated (they were).

Has anyone else here bound wrapped objects to a tree view? How did you get all the data to display correctly?

I guess another potential issue is that getting to the wrapped object via ContentControls might not be the best idea. I'm not getting a code-smell vibe from it, but it is kind of annoying that I can't access the wrapped object in a cleaner way. If anyone has any ideas to remedy that, it may help with the TreeView issue.


Solution

  • one DataTemplate for Wrapper will do. Inside it bind to Model properties:

    <TreeView ItemsSource="{Binding ElementName=Root, Path=ItemsToDisplay}">
        <TreeView.Resources>
            <HierarchicalDataTemplate DataType="{x:Type local:Wrapper}" 
                                      ItemsSource="{Binding Path=Model.MyChildren}">
                <Label Content="{Binding Model.DisplayName}"/>
            </HierarchicalDataTemplate>
        </TreeView.Resources>
    </TreeView>
    

    Child type doesn't have MyChildren property, so it will generate a warning:

    System.Windows.Data Error: 40 : BindingExpression path error: 'MyChildren' property not found on 'object' ''Child' (HashCode=46092238)'. BindingExpression:Path=Model.MyChildren; DataItem='Wrapper' (HashCode=16310625); target element is 'TreeViewItem' (Name=''); target property is 'ItemsSource' (type 'IEnumerable')

    It can be safely ignored. Or you can move MyChildren property to AbstractModel and leave it empty in Child object.


    I created a test TreeView (tv):

    TreeView

    public class Wrapper
    {
        public AbstractModel Model { get; set;}
    
        public static implicit operator Wrapper(Parent p)
        {
            return new Wrapper { Model = p };
        }
    
        public static implicit operator Wrapper(Child c)
        {
            return new Wrapper { Model = c };
        }
    }
    

    using this data (implicit type conversion and collections initializer are used to shorten collection initialization)

    var L = new List<Wrapper>()
    {
        new Parent 
        { 
            DisplayName = "A", 
            MyChildren = 
            { 
                new Child { DisplayName = "A1"}, 
                new Parent 
                { 
                    DisplayName = "X", 
                    MyChildren = 
                    { 
                        new Parent { DisplayName = "X1" } 
                    }
                }
            } 
        },
        new Child  { DisplayName = "B"},
        new Parent { DisplayName = "C", MyChildren = { new Child { DisplayName = "C1"} } },
    };
    tv.ItemsSource = L;