Search code examples
c#wpfxaml.net-corewpf-controls

WPF - Flat Tree View Control


I'm trying to build a flat tree view like this:

A flat tree view.

I have deeply nested hierarchical data:

- A1
  - B1
    - ...
      - X1
        - Y1
          - ...
            - AJ1
      - X2
        - Y2
          - Z2
- A2
  - B2
    - ...
- ...

I expect long "chains", but not many branches.

I'd like to display this data like this:

<StackPanel>
  <!-- A1 -->
  <WrapPanel>
    <TextBlock>A1</TextBlock>
    <TextBlock>B1</TextBlock>
    <!-- ... -->
    <StackPanel>
      <WrapPanel>
        <TextBlock>X1</TextBlock>
        <!-- ... -->
        <TextBlock>AJ1</TextBlock>
      </WrapPanel>

      <WrapPanel>
        <TextBlock>X2</TextBlock>
        <TextBlock>Y2</TextBlock>
        <TextBlock>Z2</TextBlock>
      </WrapPanel>
    <StackPanel>
  </WrapPanel>

  <!-- A2 -->
  <WrapPanel>
    <TextBlock>A2</TextBlock>
    <TextBlock>...</TextBlock>
  </WrapPanel>

  <!-- ... -->
</StackPanel>

In other words: If a node has zero or one children, it should just be displayed alongside its parent in a WrapPanel, if a node has more than one child, it should display each child as a new WrapPanel inside of a StackPanel.

I tried using a regular TreeView, however this forces each child to be contained inside of its parent (so you can't flatten the tree).

At this point I'm not sure if I'm using TreeView wrong, or if I'd have to write my own ItemsControl. I already looked at the MSDN docs for ItemsControl but as of right now it's not immediately obvious how I would use it/ which methods I'd need to implement.

Edit: I uploaded a repo with sample data to GitHub.


Solution

  • If I understand correctly what you want to get, then here is a solution:

    using System;
    using System.Collections.Generic;
    using System.Globalization;
    using System.Windows.Data;
    using System.Windows.Markup;
    
    namespace FlatTreeViewExample
    {
        [ValueConversion(typeof(MoveViewModel), typeof(IEnumerable<MoveViewModel>))]
        public class MoveVmToItemsSourceConverter : IValueConverter
        {
            public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
            {
                MoveViewModel? currentVM = value as MoveViewModel;
                while (currentVM?.Children.Count > 0)
                {
                    if (currentVM.Children.Count == 1)
                    {
                        currentVM = currentVM.Children[0];
                    }
                    else
                    {
                        return currentVM.Children;
                    }
                }
                return Array.Empty<MoveViewModel>();
            }
    
            public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
            {
                throw new NotImplementedException();
            }
    
            #region Singleton
            private MoveVmToItemsSourceConverter() { }
            public static MoveVmToItemsSourceConverter Instance { get; } = new();
            #endregion
        }
    
        [MarkupExtensionReturnType(typeof(MoveVmToItemsSourceConverter))]
        public class MoveVmToItemsSourceExtension : MarkupExtension
        {
            public override object ProvideValue(IServiceProvider serviceProvider)
            {
                return MoveVmToItemsSourceConverter.Instance;
            }
        }
    }
    
    using System;
    using System.Globalization;
    using System.Text;
    using System.Windows.Data;
    using System.Windows.Markup;
    
    namespace FlatTreeViewExample
    {
        [ValueConversion(typeof(MoveViewModel), typeof(string))]
        public class MoveVmToTextConverter : IValueConverter
        {
    
            public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
            {
                MoveViewModel? currentVM = value as MoveViewModel;
                StringBuilder builder = new StringBuilder(currentVM?.Text);
                while (currentVM?.Children.Count == 1)
                {
                    currentVM = currentVM.Children[0];
                    builder.Append(currentVM.Text);
                }
                return builder.ToString();
            }
    
            public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
            {
                throw new NotImplementedException();
            }
    
            #region Singleton
            private MoveVmToTextConverter() { }
            public static MoveVmToTextConverter Instance { get; } = new();
            #endregion
    
        }
        [MarkupExtensionReturnType(typeof(MoveVmToTextConverter))]
        public class MoveVmToTextSourceExtension : MarkupExtension
        {
            public override object ProvideValue(IServiceProvider serviceProvider)
            {
                return MoveVmToTextConverter.Instance;
            }
        }
    
    }
    
    <UserControl x:Class="FlatTreeViewExample.MoveExplorer"
                 xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                 xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                 xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
                 xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
                 xmlns:local="clr-namespace:FlatTreeViewExample"
                 mc:Ignorable="d" 
                 d:DesignHeight="450" d:DesignWidth="800" d:DataContext="{d:DesignInstance local:MoveExplorerVM}">
        <TreeView ItemsSource="{Binding Moves}">
            <TreeView.ItemTemplate>
                <HierarchicalDataTemplate ItemsSource="{Binding Converter={local:MoveVmToItemsSource}}">
                    <TextBlock Text="{Binding Converter={local:MoveVmToTextSource}}"/>
                </HierarchicalDataTemplate>
            </TreeView.ItemTemplate>
        </TreeView>
    </UserControl>
    

    An example of using a ListBox to create a list of items in the context of a TreeItem, including adding a context menu for each list item:

    using System;
    using System.Collections.Generic;
    using System.Globalization;
    using System.Windows.Data;
    using System.Windows.Markup;
    
    namespace FlatTreeViewExample
    {
        [ValueConversion(typeof(MoveViewModel), typeof(List<MoveViewModel>))]
        public class MoveVmToListVMConverter : IValueConverter
        {
            public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
            {
                MoveViewModel currentVM = (MoveViewModel) value;
                List<MoveViewModel> list = new List<MoveViewModel>(10) { currentVM };
                while (currentVM?.Children.Count == 1)
                {
                    currentVM = currentVM.Children[0];
                    list.Add(currentVM);
                }
                return list;
            }
    
            public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
            {
                throw new NotImplementedException();
            }
    
            #region Singleton
            private MoveVmToListVMConverter() { }
            public static MoveVmToListVMConverter Instance { get; } = new();
            #endregion
    
        }
    
        [MarkupExtensionReturnType(typeof(MoveVmToTextConverter))]
        public class MoveVmToListVMExtension : MarkupExtension
        {
            public override object ProvideValue(IServiceProvider serviceProvider)
            {
                return MoveVmToListVMConverter.Instance;
            }
        }
    
    }
    
    <UserControl x:Class="FlatTreeViewExample.MoveExplorer"
                 xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                 xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                 xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
                 xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
                 xmlns:local="clr-namespace:FlatTreeViewExample"
                 mc:Ignorable="d" 
                 d:DesignHeight="450" d:DesignWidth="800" d:DataContext="{d:DesignInstance local:MoveExplorerVM}">
        <TreeView ItemsSource="{Binding Moves}">
            <TreeView.ItemTemplate>
                <HierarchicalDataTemplate ItemsSource="{Binding Converter={local:MoveVmToItemsSource}}">
                    <!--<TextBlock Text="{Binding Converter={local:MoveVmToText}}"/>-->
                    <ListBox ItemsSource="{Binding Converter={local:MoveVmToListVM}}">
                        <ListBox.ItemsPanel>
                            <ItemsPanelTemplate>
                                <StackPanel Orientation="Horizontal"/>
                            </ItemsPanelTemplate>
                        </ListBox.ItemsPanel>
                        <ListBox.ItemTemplate>
                            <DataTemplate DataType="{x:Type local:MoveViewModel}">
                                <TextBlock Text="{Binding Text}">
                                    <TextBlock.ContextMenu>
                                        <ContextMenu>
                                            <TextBlock Text="{Binding Text, StringFormat='Hi, {0}!'}"/>
                                        </ContextMenu>
                                    </TextBlock.ContextMenu>
                                </TextBlock>
                            </DataTemplate>
                        </ListBox.ItemTemplate>
                    </ListBox>
                </HierarchicalDataTemplate>
            </TreeView.ItemTemplate>
        </TreeView>
    </UserControl>