Search code examples
c#wpf

WPF TreeView reverts changes to the selected item


I'm trying to implement a TreeView with a TextBox that gets/sets the TreeView's SelectedItem using a path.

treeview-selection-with-path-example

When I enter the path to a node in a branch that hasn't been selected/expanded previously, it works every time. But when I try to enter the path to a branch or node that has already been selected, the TreeView immediately reselects the previous item for seemingly no reason.

I've tried every possible solution I could find on MSDN & SO, and nothing I've tried has any effect! This is the 3rd project (using .NET Core 6) that I've tried this on, each time rewriting the entire thing from scratch to see if I missed anything. There are 4 files in my example project but it's still pretty long for SO, so here's the link to full solution.

I've tried manually focusing/unfocusing the TreeViewItems as they're (un)selected as noted in similar SO posts, but that doesn't have any effect.

In an effort to figure out what was happening, I added breakpoints in MainWindowVM.TreeView_SelectedItemChanged & the TreeNode.IsSelected setter to write to the output log.
This is what I see when I click on items in the TreeView:

"Root/Node0".IsSelected = true
SelectedItemChanged: null => "Root/Node0"   {System.Windows.Controls.TreeView Items.Count:4}
//                   ^^^^    ^^^^^^^^^^^^   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
//              prev. path     new path        e.OriginalSource

//   clicking another item:
"Root/Node0".IsSelected = false
"Root/Node1".IsSelected = true
SelectedItemChanged: "Root/Node0" => "Root/Node1"   {System.Windows.Controls.TreeView Items.Count:4}

Everything works as expected, and the log shows exactly what you'd expect to see. The previous item (if there is one) gets deselected, the new item gets selected, and the SelectedItemChanged event fires.
This is what happens when I use the TextBox to set the selected item:

// Continuing from the previous log, "Root/Node1" is selected & expanded.
// I then append "/Node2" to the path. It's visible but hasn't been selected before:  
"Root/Node1/Node2".IsSelected = true
"Root/Node1".IsSelected = false
SelectedItemChanged: "Root/Node1" => "Root/Node1/Node2"   {System.Windows.Controls.TreeView Items.Count:4}
// Success. Nothing abnormal occurred.
// And then I try to remove "/Node2" from the path...
"Root/Node1".IsSelected = true
"Root/Node1/Node2".IsSelected = false
"Root/Node1".IsSelected = false
"Root/Node1/Node2".IsSelected = true
SelectedItemChanged: "Root/Node1" => "Root/Node1/Node2"   {System.Windows.Controls.TreeView Items.Count:4}
// ...and it gets instantly reverted.

The tree view just reverts the selection change, and fires the SelectedItemChanged event afterwards. The same thing happens when I try to select a subnode that's already been visible:

// Again continuing from the previous log, I click on "Node1" with the mouse
"Root/Node1/Node2".IsSelected = false
"Root/Node1".IsSelected = true
SelectedItemChanged: "Root/Node1/Node2" => "Root/Node1"   {System.Windows.Controls.TreeView Items.Count:4}
// And again, nothing abnormal occurs.
// Then I try appending "/Node2" to the path:
"Root/Node1/Node2".IsSelected = true
"Root/Node1".IsSelected = false
"Root/Node1/Node2".IsSelected = false
"Root/Node1".IsSelected = true
SelectedItemChanged: "Root/Node1/Node2" => "Root/Node1"   {System.Windows.Controls.TreeView Items.Count:4}
// and it happens again!

I suspect it has something to do with the control already existing since the TreeView creates the subcontrols as needed, but I don't see why that would matter. I don't understand why this happens or what causes it, and I'm losing my mind trying to figure this out. Any help would be greatly appreciated!

To reproduce the problem with the provided solution file, do the following:

  1. Type Root/Node0/Node1/Node2 into the textbox at the top & press Tab.
    (The specified node will be selected)
  2. Change the text to Root/Node0 & press Tab.
    (The selected item will be immediately changed back)
  3. Change the text to Root/Node1/Node1/Node2 & press Tab.
    (The specified node will be selected)
  4. Change the text to Root/Node1/Node1/Node3 & press Tab.
    (The selected item will be immediately changed back)

Solution

  • The problem is not in searching and selecting a node. But in the layout. I didn't even notice this because I thought you just missed something when creating the minimal example.
    To fix the layout, I added another TextBox (can be any other element) that can take focus after the TextBox with the path.

        <Grid>
            <Grid.RowDefinitions>
                <RowDefinition Height="Auto" />
                <RowDefinition />
            </Grid.RowDefinitions>
    
            <TextBox Text="{Binding VM.CurrentPath}" />
            <TextBox MinWidth="20" HorizontalAlignment="Right"/>
    
            <TreeView
                x:Name="TreeView"
                Grid.Row="1"
                ItemsSource="{Binding VM.RootNode.Children}">
                <TreeView.ItemContainerStyle>
    

    This happens because the processing of obtaining Focus and Selecting a node in the VM occurs in one “quantum” of processing of the main thread. And they interfere with each other.
    Therefore, the second way to solve this problem is to separate the node selection execution in the VM from the quantum that performs the TreeView focus acquisition.
    This can be done, for example, by using a delay in an asynchronous method.

            public string CurrentPath
            {
                get => _currentPath;
                set
                {
                    _currentPath = value;
                    NotifyPropertyChanged();
                    SelectNode();
    
                    async void SelectNode()
                    {
                        await Task.Delay(10);
                        if (FindNode(_currentPath) is TreeNode targetNode)
                        {
                            _mainWindow.TreeView.SelectedItemChanged -= this.TreeView_SelectedItemChanged;
                            targetNode.IsSelected = true;
                            _mainWindow.TreeView.SelectedItemChanged += this.TreeView_SelectedItemChanged;
                            targetNode.ForEachBranchNode(node => node.IsExpanded = true);
                        }
                    }
                }
            }
    

    P.S. But a better implementation would be to create an Attached Property for the TreeView that would take a path and select a node. This property will need to be bound to VM.CurrentPath.