2
votes

I have a requirement to change the selected node in a treeview which is hosted in a separate tab. Further more, if the parent node is not expanded, I wish to expand the node.

After about an hour of fruitless searching through SO, Google, et al, I have decided to post a question.

I can find and expand the required node when it is all visible, but when the treeveiw is obscured by another tab item, it doesn't update. I'm also not entirely sure that the item is being 'selected' - in the debugger it says ISelected is true, and the IsExpanded property of the parent is also true.

I have simplified down my actual problem in the following lines of code:

XAML (Tab control which has two items, one is a button to reproduce the problem, and a treeview which should be updated):

<Window x:Class="TreeviewTest.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="MainWindow" Height="350" Width="525">
<Grid>
    <TabControl>
        <TabItem Header="Select">
            <Button Content="Select Delta!" Click="ButtonBase_OnClick" />
        </TabItem>
        <TabItem Header="Tree">
            <TreeView ItemsSource="{Binding NodesDisplay}" Name ="treTest">
                <TreeView.ItemTemplate>
                    <HierarchicalDataTemplate ItemsSource="{Binding Path=ChildrenDisplay}">
                        <TextBlock Text="{Binding Path=Name}" FontWeight="Bold" />
                        <HierarchicalDataTemplate.ItemTemplate>
                            <HierarchicalDataTemplate>
                                <TextBlock Text="{Binding Path=Name}" />
                            </HierarchicalDataTemplate>
                        </HierarchicalDataTemplate.ItemTemplate>
                    </HierarchicalDataTemplate>
                </TreeView.ItemTemplate>
            </TreeView>

        </TabItem>
    </TabControl>

</Grid>

MainWindow code:

namespace TreeviewTest
{
    using System.Collections.Generic;
    using System.Collections.ObjectModel;
    using System.ComponentModel;
    using System.Linq;
    using System.Reflection;
    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Controls.Primitives;
    using System.Windows.Data;

    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow 
    {
        public ObservableCollection<TreeNode> Nodes { get; set; }

        public ICollectionView NodesDisplay { get; set; }

        public MainWindow()
        {
            InitializeComponent();

            Nodes = new ObservableCollection<TreeNode>
            {
                new TreeNode(new List<TreeLeaf>
                    {
                        new TreeLeaf{Name = "Alpha"},
                        new TreeLeaf{Name = "Beta"}
                    }){ Name = "One" },
                new TreeNode(new List<TreeLeaf>
                    {
                        new TreeLeaf{Name = "Delta"},
                        new TreeLeaf{Name = "Gamma"}
                    }){ Name = "Two" }

            };
            NodesDisplay = CollectionViewSource.GetDefaultView(Nodes);
            DataContext = this;
        }

        public class TreeNode
        {
            public string Name { get; set; }
            public ObservableCollection<TreeLeaf> Children { get; private set; }

            public ICollectionView ChildrenDisplay { get; private set; }

            public TreeNode(IEnumerable<TreeLeaf> leaves)
            {
                Children = new ObservableCollection<TreeLeaf>(leaves);
                ChildrenDisplay = CollectionViewSource.GetDefaultView(Children);
            }
        }

        public class TreeLeaf
        {
            public string Name { get; set; }
        }

        private void ButtonBase_OnClick(object sender, RoutedEventArgs e)
        {
            EnsureCanIterateThroughCollection(treTest);
            var rootLevelToSelect = Nodes.First(x => x.Name == "Two");
            TreeViewItem root = treTest.ItemContainerGenerator.ContainerFromItem(rootLevelToSelect) as TreeViewItem;

            EnsureCanIterateThroughCollection(root);
            var leafLevelToSelect = rootLevelToSelect.Children.First(x => x.Name == "Delta");
            TreeViewItem leaf = root.ItemContainerGenerator.ContainerFromItem(leafLevelToSelect) as TreeViewItem;

            if (!root.IsExpanded)
                root.IsExpanded = true;

            leaf.IsSelected = true;
            ReflectivelySelectTreeviewItem(leaf);
        }

        //Got this from another SO post - not sure is setting IsSelected on the node is actually doing what I think it is...
        private static void ReflectivelySelectTreeviewItem(TreeViewItem node)
        {
            MethodInfo selectMethod = typeof(TreeViewItem).GetMethod("Select", BindingFlags.NonPublic | BindingFlags.Instance);
            selectMethod.Invoke(node, new object[] { true });
        }

        private static void EnsureCanIterateThroughCollection(ItemsControl itemsControl)
        {
            if (itemsControl.ItemContainerGenerator.Status == GeneratorStatus.NotStarted)
            ForceGenerateChildContent(itemsControl);
        }

        private static void ForceGenerateChildContent(ItemsControl itemsControl)
        {
            itemsControl.ApplyTemplate();

            IItemContainerGenerator generator = itemsControl.ItemContainerGenerator;

            GeneratorPosition position = generator.GeneratorPositionFromIndex(0);
            using (generator.StartAt(position, GeneratorDirection.Forward, true))
            {
                for (int i = 0; i < itemsControl.Items.Count; i++)
                {
                    DependencyObject dp = generator.GenerateNext();
                    generator.PrepareItemContainer(dp);
                }
            }
        }
    }
}

Also - another XAML snippet which does the same thing, but where the treeview is visible - you should be able to see the treeview expand and the item selected

<Window x:Class="TreeviewTest.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="MainWindow" Height="350" Width="525">
<Grid>
    <Grid.RowDefinitions>
        <RowDefinition Height="30" />
        <RowDefinition Height="*" />
    </Grid.RowDefinitions>
    <Button Content="Select Delta!" Click="ButtonBase_OnClick" />
    <TreeView ItemsSource="{Binding NodesDisplay}" Name ="treTest" Grid.Row="1">
        <TreeView.ItemTemplate>
            <HierarchicalDataTemplate ItemsSource="{Binding Path=ChildrenDisplay}">
                <TextBlock Text="{Binding Path=Name}" FontWeight="Bold" />
                <HierarchicalDataTemplate.ItemTemplate>
                    <HierarchicalDataTemplate>
                        <TextBlock Text="{Binding Path=Name}" />
                    </HierarchicalDataTemplate>
                </HierarchicalDataTemplate.ItemTemplate>
            </HierarchicalDataTemplate>
        </TreeView.ItemTemplate>
    </TreeView>
</Grid>

I would appreciate any help - My understanding of treeview's and WPF and databinding is that it doesn't work as nicely as something that gives you IsSynchronisedWithCurrentItem, which is why I am trying to handle updates to the treeview manually and am also trying to select items in the tree programmatically. If I am wrong on that front I would love a pointer to show me how to do it in a more 'WPF' way!

1
Not sure if this will help or not as it is Winforms: stackoverflow.com/questions/14075841/…John Bartels
@JohnBartels, Thanks for the info but using winforms is something I really don't want to do (I'm actually migrating a winforms component to WPF!) I know I could use a winforms host control to host a treeview, but I really want to avoid that if possible.Jay

1 Answers

3
votes

When a tab page is not visible, the controls are not created. Only once you switch to it are the controls created. Add a Boolean to your TreeNode viewmodel and bind the IsSelected property.

TreeNodeVm.cs:

using Microsoft.Practices.Prism.ViewModel;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;

namespace TreeViewSelectTest
{
    public class TreeNodeVm : NotificationObject
    {
        private TreeNodeVm Parent { get; set; }

        private bool _isSelected = false;
        public bool IsSelected
        {
            get { return _isSelected; }
            set
            {
                _isSelected = value;
                RaisePropertyChanged(() => IsSelected);
            }
        }

        private bool _isExpanded = false;
        public bool IsExpanded
        {
            get { return _isExpanded; }
            set
            {
                _isExpanded = value;
                RaisePropertyChanged(() => IsExpanded);
            }
        }

        public ObservableCollection<TreeNodeVm> Children { get; private set; }

        public string Header { get; set; }

        public TreeNodeVm()
        {
            this.Children = new ObservableCollection<TreeNodeVm>();
            this.Children.CollectionChanged += Children_CollectionChanged;
        }

        void Children_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
        {
            if (e.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Add)
            {
                foreach (var newChild in e.NewItems.Cast<TreeNodeVm>())
                {
                    newChild.Parent = this;
                }
            }
        }

        public TreeNodeVm(string header, IEnumerable<TreeNodeVm> children)
            : this()
        {
            this.Header = header;
            foreach (var child in children)
                Children.Add(child);
        }

        public void MakeVisible()
        {
            if (Parent != null)
            {
                Parent.MakeVisible();
            }
            this.IsExpanded = true;
        }

        public void Select()
        {
            MakeVisible();

            this.IsSelected = true;
        }
    }
}

MainWindow.xaml:

<Window x:Class="TreeViewSelectTest.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
    <DockPanel>
        <StackPanel Orientation="Horizontal" HorizontalAlignment="Left">
            <Button Content="Select B1" Click="btSelectB1_Click" />
        </StackPanel>
        <TabControl>
            <TabItem Header="treeview">
                <TreeView ItemsSource="{Binding Path=RootNode.Children}">
                    <TreeView.Resources>
                        <Style TargetType="{x:Type TreeViewItem}">
                            <Setter Property="IsSelected" Value="{Binding Path=IsSelected,Mode=TwoWay}" />
                            <Setter Property="IsExpanded" Value="{Binding Path=IsExpanded,Mode=TwoWay}" />
                        </Style>
                    </TreeView.Resources>
                    <TreeView.ItemTemplate>
                        <HierarchicalDataTemplate ItemsSource="{Binding Children}">
                            <TextBlock Text="{Binding Header}" />
                        </HierarchicalDataTemplate>
                    </TreeView.ItemTemplate>
                </TreeView>
            </TabItem>
            <TabItem Header="the other item">
                <Button />
            </TabItem>
        </TabControl>
    </DockPanel>
</Window>

MainWindow.xaml.cs:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace TreeViewSelectTest
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            RootNode = new TreeNodeVm("Root", new[]
            {
                new TreeNodeVm("A", new [] {
                    new TreeNodeVm("A1", new TreeNodeVm[0]),
                    new TreeNodeVm("A2", new TreeNodeVm[0]),
                    new TreeNodeVm("A3", new TreeNodeVm[0])
                }),
                new TreeNodeVm("B", new [] {
                    new TreeNodeVm("B1", new TreeNodeVm[0])
                })
            });

            InitializeComponent();
            this.DataContext = this;
        }

        public TreeNodeVm RootNode { get; private set; }

        private void btSelectB1_Click(object sender, RoutedEventArgs e)
        {
            RootNode.Children[1].Children[0].Select();
        }
    }
}

When you call TreeNodeVm.Select(), it will update the future state of the visuals. Once the tab page gets switched back, the template is applied and the visuals are created as expanded and selected.