Boeckle
Boeckle

Reputation: 13

WPF Treeview Binding multiple, different Lists

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.

Upvotes: 0

Views: 2435

Answers (3)

Alexander Gr&#228;f
Alexander Gr&#228;f

Reputation: 576

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.

Upvotes: 0

Mark Feldman
Mark Feldman

Reputation: 16119

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.

Upvotes: 2

canton7
canton7

Reputation: 42245

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.

Upvotes: 2

Related Questions