Search code examples
wpfxamltreeview

How do I invert the order of a TreeView?


This question seems so simple, yet I didn't find any feasible solution after searching for it all day long.

I have a standard TreeView:

<TreeView x:Name="MyUpsideDownTree" ItemsSource="{Binding Items}" />

Items is a list (I need the list to stay in this order) like

  • A
  • B
    • B1
    • B2
  • C

I want my TreeView to show the items (including all subitems!) in an inverted order, i.e.

  • C
  • B
    • B2
    • B1
  • A

Sorting doesn't seem to be the solution since I don't want to compare the items' value.

A converter...?

Thanks for your help!


Solution

  • This is a great example of the fundamental theorem of software engineering - any problem can be solved with an extra layer of abstraction.

    Since you can't modify the way the underlying collection operates, and you can't force TreeView to reverse the items on its own (well, you could, but not without retemplating it), a solution is to introduce an intermediate "view" (more like a database view rather than the MVVM kind) that wraps the underlying collection and sits between it and the TreeView.

    Such a "view" could be implemented like this:

    public class ReverseListView : ICollection, INotifyCollectionChanged
    {
        private IList _collection;
    
        public ReverseListView(IList collection)
        {
            _collection = collection;
            if (collection is INotifyCollectionChanged incc)
                incc.CollectionChanged += this.OnSourceCollectionChanged;
        }
    
        public IEnumerator GetEnumerator()
        {
            return _collection.Cast<object>().Reverse().GetEnumerator();
        }
    
        public event NotifyCollectionChangedEventHandler? CollectionChanged;
    
        public int Count => this._collection.Count;
    
        public bool IsSynchronized => this._collection.IsSynchronized;
    
        public object SyncRoot => this._collection.SyncRoot;
    
        public void CopyTo(Array array, int index)
        {
            this._collection.CopyTo(array, index);
        }
    
        private void OnSourceCollectionChanged(
            object? sender,
            NotifyCollectionChangedEventArgs e)
        {
            NotifyCollectionChangedEventArgs reversedArgs = e;
            if (e.Action == NotifyCollectionChangedAction.Add)
            {
                reversedArgs = new NotifyCollectionChangedEventArgs(
                    e.Action,
                    e.NewItems,
                    this._collection.Count - e.NewStartingIndex - 1);
            }
            else if (e.Action == NotifyCollectionChangedAction.Remove)
            {
                reversedArgs = new NotifyCollectionChangedEventArgs(
                    e.Action,
                    e.OldItems,
                    this._collection.Count - e.OldStartingIndex);
            }
            else
            {
                // handle other collection changed actions if needed
            }
            this.CollectionChanged?.Invoke(this, reversedArgs);
        }
    }
    

    And a value converter:

    public class ReverseListConverter: IValueConverter
    {
        public object Convert(
            object value, 
            Type targetType, 
            object parameter, 
            CultureInfo culture)
        {
            if (!(value is IList list))
                return Binding.DoNothing;
            return new ReverseListView(list);
        }
    
        public object ConvertBack(
            object value, 
            Type targetType, 
            object parameter, 
            CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }
    

    Unlike the other answers this approach doesn't modify the original collection or create and maintain a separate copy - which in one of the other answers would lead to errors if the converter instance is used in the HierarchicalDataTemplate for multiple tree members, as it isn't stateless. Instead it operates as a view or abstraction of the underlying collection but just inverts the order during enumeration. If the underlying collection implements INotifyCollectionChanged (it doesn't have to) then it also subscribes to the collection change events and raises its own events with the inverted indices. Note I didn't try to handle multi-item changes as these aren't supported by ObservableCollection or WPF.

    A quick test demonstrates that the TreeView displays the items in the reverse order and correctly responds to changes in the underlying collection, and no special binding conditions like OneTime are necessary:

    XAML

    <Grid DataContext="{Binding ElementName=_window, Path=ViewModel}">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>
    
        <StackPanel Grid.Row="0"
                    Orientation="Horizontal">
            <Button Click="AddPersonClick">Add Person</Button>
            <Button Click="RemovePersonClick">Remove Person</Button>
        </StackPanel>
        
        <TreeView Grid.Row="1"
                  ItemsSource="{Binding People, Converter={StaticResource ReverseListConverter}}">
            <TreeView.ItemTemplate>
                <HierarchicalDataTemplate ItemsSource="{Binding Children, Converter={StaticResource ReverseListConverter}}">
                    <TextBlock Text="{Binding Name}" />
                </HierarchicalDataTemplate>
            </TreeView.ItemTemplate>
        </TreeView>
        
    </Grid>
    

    C#

    public class Person
    {
        public Person(string name, params string[] children)
        {
            this.Name = name;
            foreach (var child in children ?? Array.Empty<string>())
            {
                this.Children.Add(new Person(child));
            }
        }
    
        public string Name { get; set; }
    
        ObservableCollection<Person> _Children;
        public ObservableCollection<Person> Children
        {
            get => _Children ?? (_Children = new ObservableCollection<Person>());
        }
    }
    
    public class ViewModel
    {
        public ViewModel()
        {
            this.People.Add(new Person("Daenerys", "Drogon", "Rhaegal", "Viserion"));
            this.People.Add(new Person("Ned", "Robb", "Sansa", "Arya", "Bran", "Rickon"));
            this.People.Add(new Person("Cersei", "Joffrey", "Myrcella", "Tommen"));
        }
    
        ObservableCollection<Person> _People;
        public ObservableCollection<Person> People
        {
            get => _People ?? (_People = new ObservableCollection<Person>());
        }
    }
    
    public partial class ReverseListDemo : Window
    {
        public ReverseListDemo()
        {
            InitializeComponent();
        }
    
        public ViewModel ViewModel { get; } = new ViewModel();
    
        private void AddPersonClick(object sender, RoutedEventArgs e)
        {
            this.ViewModel.People.Add(
                new Person($"Person {this.ViewModel.People.Count}"));
        }
    
        private void RemovePersonClick(object sender, RoutedEventArgs e)
        {
            this.ViewModel.People.RemoveAt(0);
        }
    }
    

    Initial state:

    enter image description here

    After adding a new person to the end of the list:

    enter image description here