That is a truly hard thing to interact directly with controls in WPF. And I don't know the answer keeping your development approach. But know how it can be done in other way.
I suggest using MVVM and Binding
instead. I've created a demo project showing how it can be done.
It's not a Silver Bullet but a demo to start from.
Due to MVVM pattern approach we need a couple of helper classes.
// INPC Interface implementation for deriving in ViewModels
public class NotifyPropertyChanged : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName]string propertyName = null)
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
// ICommand interface implementation for easy commands use
public class RelayCommand : ICommand
{
private readonly Action<object> _execute;
private readonly Func<object, bool> _canExecute;
public event EventHandler CanExecuteChanged
{
add { CommandManager.RequerySuggested += value; }
remove { CommandManager.RequerySuggested -= value; }
}
public RelayCommand(Action<object> execute, Func<object, bool> canExecute = null)
{
_execute = execute;
_canExecute = canExecute;
}
public bool CanExecute(object parameter) => _canExecute == null || _canExecute(parameter);
public void Execute(object parameter) => _execute(parameter);
}
App features
- Find and select node by name or part of name
- Remove selected node
- Add child node to selected node
You may improve it:
- Add and store Parent property to avoid upper-level search things
The data hierarchical class (means that it contains a collection of itself)
public class MyTreeNode : NotifyPropertyChanged
{
private ObservableCollection<MyTreeNode> _items = new ObservableCollection<MyTreeNode>();
private string _nodeName;
private bool _isSelected;
public ObservableCollection<MyTreeNode> Items
{
get => _items;
set
{
_items = value;
OnPropertyChanged();
}
}
public string NodeName
{
get => _nodeName;
set
{
_nodeName = value;
OnPropertyChanged();
}
}
public bool IsSelected
{
get => _isSelected;
set
{
_isSelected = value;
OnPropertyChanged();
}
}
}
Then the very important main class MainViewModel
it will provide properties to MainWindow
.
public class MainViewModel : NotifyPropertyChanged
{
private ObservableCollection<MyTreeNode> _treeItems;
private ICommand _searchCommand;
private ICommand _addCommand;
private ICommand _removeCommand;
private string _text;
public string Text
{
get => _text;
set
{
_text = value;
OnPropertyChanged();
}
}
public MainViewModel()
{
TreeItems = new ObservableCollection<MyTreeNode>();
// demo values for initial test
TreeItems.Add(new MyTreeNode { NodeName = "Node1" });
MyTreeNode node = new MyTreeNode { NodeName = "Node2", IsSelected = true };
TreeItems.Add(node);
node.Items.Add(new MyTreeNode { NodeName = "SubNode1.1" });
node.Items.Add(new MyTreeNode { NodeName = "SubNode1.2" });
node.Items.Add(new MyTreeNode { NodeName = "SubNode1.3" });
TreeItems.Add(new MyTreeNode { NodeName = "Node3" });
TreeItems.Add(new MyTreeNode { NodeName = "Node4" });
}
public ObservableCollection<MyTreeNode> TreeItems
{
get => _treeItems;
set
{
_treeItems = value;
OnPropertyChanged();
}
}
// search by node name implementation
private MyTreeNode SearchItemByName(ObservableCollection<MyTreeNode> nodes, string searchText)
{
if (searchText?.Length > 0)
{
foreach (MyTreeNode node in nodes)
{
if (node.NodeName.Contains(searchText, StringComparison.InvariantCultureIgnoreCase))
{
return node;
}
if (node.Items.Count > 0)
{
MyTreeNode result = SearchItemByName(node.Items, searchText);
if (result != null) return result;
}
}
}
return null;
}
// need for remove action to find the collection that contains the required item
private ObservableCollection<MyTreeNode> FindParentCollection(ObservableCollection<MyTreeNode> nodes, MyTreeNode searchNode)
{
if (searchNode != null)
{
foreach (MyTreeNode node in nodes)
{
if (node.Equals(searchNode))
{
return nodes;
}
if (node.Items.Count > 0)
{
ObservableCollection<MyTreeNode> result = FindParentCollection(node.Items, searchNode);
if (result != null) return result;
}
}
}
return null;
}
// Commands where buttons are attached to.
public ICommand SearchCommand => _searchCommand ?? (_searchCommand = new RelayCommand(parameter =>
{
MyTreeNode result = SearchItemByName(TreeItems, Text);
if (result != null)
result.IsSelected = true;
}));
public ICommand AddCommand => _addCommand ?? (_addCommand = new RelayCommand(parameter =>
{
MyTreeNode newNode = new MyTreeNode { NodeName = Text };
if (parameter is MyTreeNode node)
node.Items.Add(newNode);
else
TreeItems.Add(newNode);
}));
public ICommand RemoveCommand => _removeCommand ?? (_removeCommand = new RelayCommand(parameter =>
{
MyTreeNode node = parameter as MyTreeNode;
ObservableCollection<MyTreeNode> nodes = FindParentCollection(TreeItems, node);
nodes.Remove(node);
}, parameter => parameter is MyTreeNode));
}
And full markup that will help to reproduce the entire app
<Window x:Class="WpfApp2.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WpfApp2"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Window.DataContext>
<local:MainViewModel/><!-- MainViewModel instantiated here -->
</Window.DataContext>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition/>
</Grid.RowDefinitions>
<StackPanel Orientation="Horizontal">
<TextBox Text="{Binding Text}" Margin="5" Width="300"/>
<Button Margin="5" Content="Search" Command="{Binding SearchCommand}"/>
<Button Margin="5" Content="Add" Command="{Binding AddCommand}" CommandParameter="{Binding SelectedItem, ElementName=MyTreeView}"/>
<Button Margin="5" Content="Remove" Command="{Binding RemoveCommand}" CommandParameter="{Binding SelectedItem, ElementName=MyTreeView}"/>
</StackPanel>
<TextBlock Grid.Row="1" Margin="5" Text="{Binding SelectedItem.NodeName, ElementName=MyTreeView}"/>
<TreeView x:Name="MyTreeView" Grid.Row="2" Margin="5" ItemsSource="{Binding TreeItems}">
<TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Items}">
<TextBlock Text="{Binding NodeName}"/>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
<TreeView.Resources>
<Style TargetType="TreeViewItem">
<Setter Property="IsSelected" Value="{Binding IsSelected}"/>
<Setter Property="IsExpanded" Value="True"/>
</Style>
</TreeView.Resources>
</TreeView>
</Grid>
</Window>

And the traditionally for MVVM newcomers: code-behind class
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
}