0

I want to bind multiple, different lists to a TreeView in WPF. I looked up some other solutiones but could not find any help for my problem. This answer is pretty close but not quite what I am looking for.

I tried the linked solution above but it only displays two levels in the TreeView. I can't figure out how to show the name of each list as parent in the TreeView.

My object I want to display looks like this:

public class LightDistributor
{
  public string Description { get; set; }
  // ...

  public List<Field> Hardware { get; set; }
  public List<Type> Inputs { get; set; }
  public List<Type> Outputs { get; set; }
}

public class Field
{
  public string Fieldname { get; set; }
  // ...
}

public class Type
{
  public string TypeDescription { get; set; }
  // ...
}

And the XAML:

<TreeView ItemsSource="{Binding LightDistributors}">
  <TreeView.ItemTemplate>
    <HierarchicalDataTemplate DataType="{x:Type data:LightDistributor}" ItemsSource="{Binding Hardware}">
       <TextBlock Text="{Binding Description}" />
       <HierarchicalDataTemplate.ItemTemplate>
          <DataTemplate DataType="{x:Type data:Field}">
               <TextBlock Text="{Binding Description}" />
          </DataTemplate>
       </HierarchicalDataTemplate.ItemTemplate>
    </HierarchicalDataTemplate>
  </TreeView.ItemTemplate>   
</TreeView>

What I want my Treeview to look:

LightDistributor - LongFloor
    | Hardware
        - Field1
        - Field2
        - Field3
    | Inputs
        - InputTypeA
        - InputTypeB
    | Outputs
        - OutputTypeY
        - OutputTypeZ

What it currently looks:

LightDistributor - LongFloor
        - Field1
        - Field2
        - Field3

Depending on the SelectedItem, a UserControl is displayed with more parameters.

Boeckle
  • 13
  • 7

3 Answers3

2

Add a NamedSection, which groups a name with a list of items:

public class NamedSection
{
    public string Name { get; set; }
    public IReadOnlyList<object> Items { get; set; }
}

Then update your LightDistributor. Note how I've made the List<T> properties getter-only, so that the NamedSection can correctly capture the reference on construction.

public class LightDistributor
{
    public string Description { get; set; }
    // ...

    public List<Field> Hardware { get; } = new List<Field>();
    public List<Type> Inputs { get; } = new List<Type>();
    public List<Type> Outputs { get; } = new List<Type>();

    public List<NamedSection> Sections { get; }

    public LightDistributor()
    {
        this.Sections = new List<NamedSection>()
        {
            new NamedSection() { Name = "Hardware", Items = this.Hardware },
            new NamedSection() { Name = "Inputs", Items = this.Inputs },
            new NamedSection() { Name = "Outputs", Items = this.Outputs },
        };
    }
}

Then your XAML:

<TreeView ItemsSource="{Binding LightDistributors}">
    <TreeView.Resources>
        <HierarchicalDataTemplate DataType="{x:Type local:LightDistributor}" ItemsSource="{Binding Sections}">
            <TextBlock Text="{Binding Description}" />
        </HierarchicalDataTemplate>
        <HierarchicalDataTemplate DataType="{x:Type local:NamedSection}" ItemsSource="{Binding Items}">
            <TextBlock Text="{Binding Name}"/>
        </HierarchicalDataTemplate>
        <DataTemplate DataType="{x:Type local:Field}">
            <TextBlock Text="{Binding Fieldname}"/>
        </DataTemplate>
        <DataTemplate DataType="{x:Type local:Type}">
            <TextBlock Text="{Binding TypeDescription}"/>
        </DataTemplate>
    </TreeView.Resources>
</TreeView>

I initially thought you could also achieve this by declaring an x:Array of TreeViewItem as a resource (with an item each for Hardware, Inputs, Output) and then setting it as the ItemsSource of the HierarchicalTemplate for LightDistributor. However this doesn't work, as there doesn't seem to be a way to clone this x:Array for each LightDistributor we want to show.

canton7
  • 37,633
  • 3
  • 64
  • 77
  • Good solution, but the templates for Field and Type should be `DataTemplate`, not `HierarchicalDataTemplate`. (It'll still work though). – Mark Feldman Feb 18 '19 at 10:44
  • Fair point. It wasn't doing any harm, but edited for correctness. Thanks! – canton7 Feb 18 '19 at 10:53
  • Actually now that I've thought about it some more your second solution isn't going to work. The `DistributorItems` resource will get recycled, I've just done a quick test and specifying `x:Shared="False"` doesn't seem to work in this case either. If you really had to you could merge the lists with a multi-binding and a custom converter, but that's a pretty messy solution. I think your first answer is better, this is a clear case of a view model not really doing its job properly in presenting the data in a format that the view can readily consume. – Mark Feldman Feb 19 '19 at 00:09
  • First I tried your second solution. It worked perfectly fine until I tried to open multiple light distributor in the TreeView. It only displayed the elements for one light distributor, the other ones are invisible/hidden. The first solutiones works for me, thank you! – Boeckle Feb 19 '19 at 12:01
  • @Boeckle looks like the same thing Mark Feldman mentioned. I can't find a way to make it work, so I'll remove that part from my answer. – canton7 Feb 19 '19 at 12:19
  • @MarkFeldman thanks! I tried several other ways, and couldn't get it to work either, so I've removed it. Which is a shame, because it seems like one of those things which should be able to work.... – canton7 Feb 19 '19 at 12:23
  • @canton7 managed to get it working and added it as another answer, still think your original solution should have worked with sharing disabled though. – Mark Feldman Feb 19 '19 at 22:02
2

Adding another answer here showing how to do it in pure XAML. This is based almost entirely on canton7's original second solution, which was very close but was creating an array of TreeViewItems which were getting recycled. Ordinarily setting x:Shared="False" should fix that, the fact that it didn't work in his case seems like a WPF bug to me.

In any case, instead of creating a array of controls create an array of data objects. Type sys:String will work fine in this case, with the added bonus that we'll also be able to use it as the TreeViewItem header text later on:

    <x:Array x:Key="DistributorItems" Type="{x:Type sys:String}">
        <sys:String>Hardware</sys:String>
        <sys:String>Inputs</sys:String>
        <sys:String>Outputs</sys:String>
    </x:Array>

These represent the child properties in your LightDistributor class. The second level of your TreeView will get one of these strings assigned as its DataContext, so we'll create a style for those and use Triggers to set the ItemsSource accordingly via the parent TreeViewItem's DataContext instead:

    <Style x:Key="TreeViewItemStyle" TargetType="TreeViewItem">
        <Style.Triggers>
            <Trigger Property="DataContext" Value="Hardware">
                <Setter Property="ItemsSource" Value="{Binding DataContext.Hardware, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type TreeViewItem}, AncestorLevel=1}}" />
            </Trigger>
            <Trigger Property="DataContext" Value="Inputs">
                <Setter Property="ItemsSource" Value="{Binding DataContext.Inputs, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type TreeViewItem}, AncestorLevel=1}}" />
            </Trigger>
                <Trigger Property="DataContext" Value="Outputs">
                <Setter Property="ItemsSource" Value="{Binding DataContext.Outputs, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type TreeViewItem}, AncestorLevel=1}}" />
            </Trigger>
        </Style.Triggers>
    </Style>

The rest of the code is basically the same as canton7's original code, except I'm setting the LightDistributor's ItemContainerStyle to the style that I created above to set the ItemsSource accordingly:

<TreeView ItemsSource="{Binding LightDistributors}">
    <TreeView.Resources>

        <HierarchicalDataTemplate DataType="{x:Type vm:LightDistributor}"
                                  ItemsSource="{Binding Source={StaticResource DistributorItems}}"
                                  ItemContainerStyle="{StaticResource TreeViewItemStyle}">
            <TextBlock Text="{Binding Description}"/>
        </HierarchicalDataTemplate>

        <DataTemplate DataType="{x:Type vm:Field}">
            <TextBlock Text="{Binding Fieldname}"/>
        </DataTemplate>

        <DataTemplate DataType="{x:Type vm:Type}">
            <TextBlock Text="{Binding TypeDescription}"/>
        </DataTemplate>

    </TreeView.Resources>
</TreeView>

Now just because this works doesn't mean it's a good solution, I'm still very much of the opinion that canton7's first solution is a better one. Just throwing this out there though to show that it can be done in pure XAML after all.

Mark Feldman
  • 15,731
  • 3
  • 31
  • 58
0

The easiest way to project arbitrary objects into TreeViews is by using a Converter. For example, if you start with a single object in a single property:

public class EnumerateConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter,
        System.Globalization.CultureInfo culture)
    {
        if (value == null)
            return new List<object> { };
        
        return new List<object> { value };
    }

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

Then you can bind to the property:

<TreeView Name="MainTreeView" HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
Grid.Column="0" SelectedItemChanged="OnTreeViewSelectedItemChanged"
ItemsSource="{Binding ObjProperty, Converter={StaticResource EnumerateConverter}}">

The converter can return any arbitrary list of child properties, in any order. ItemTemplates can be used to specify converters for sub-types. This can also all be combined with the NamedSection trick, i.e. instead of returning a bare list, return a list of NamedSections.

What is better about this solution is that you don't have to touch your model classes, either because you don't want to, or maybe actually can't because they are part of a third-party assembly.

Alexander Gräf
  • 511
  • 1
  • 3
  • 10