This is how I'd implement this. (Actually, implemented for this answer.)
I'm using the CommunityToolkit.Mvvm NuGet package to implement the MVVM design.
ItemProvider.cs
using System.Collections.Generic;
namespace WpfAppTreeViewTest;
public class Item
{
public string Name { get; set; }
public string Path { get; set; }
}
public class FileItem : Item
{
}
public class DirectoryItem : Item
{
public List<Item> Items { get; set; }
public DirectoryItem()
{
Items = new List<Item>();
}
}
public class ItemProvider
{
public List<Item> GetItems(string path)
{
return new List<Item>()
{
new DirectoryItem()
{
Name = "comedy",
Items = new List<Item>()
{
new FileItem() { Name = "1001.PDF" },
new FileItem() { Name = "1002.PDF" },
new FileItem() { Name = "1003.PDF" },
},
},
new DirectoryItem()
{
Name = "horror",
Items = new List<Item>()
{
new FileItem() { Name = "1005.PDF" },
new FileItem() { Name = "1006.PDF" },
},
},
new DirectoryItem()
{
Name = "suspense",
Items = new List<Item>()
{
new FileItem() { Name = "1002.PDF" },
new FileItem() { Name = "1005.PDF" },
new FileItem() { Name = "1006.PDF" },
},
},
};
}
}
MainWindowViewModel.cs
using CommunityToolkit.Mvvm.ComponentModel;
using System.Collections.Generic;
using System.Linq;
namespace WpfAppTreeViewTest;
// This class needs to be partial for the CommunityToolkit's source generators.
public partial class MainWindowViewModel : ObservableObject
{
// The source generators will auto-create a "FilterText" property.
[ObservableProperty]
private string filterText = string.Empty;
// The source generators will auto-create a "FilteredItems" property.
[ObservableProperty]
private IEnumerable<Item>? filteredItems;
private List<Item>? items;
public MainWindowViewModel()
{
var itemProvider = new ItemProvider();
this.items = itemProvider.GetItems("");
FilteredItems = this.items;
}
// This is a "partial" method created by the source generator.
// This method will be called everytime "FilterText" changes.
partial void OnFilterTextChanged(string value)
{
FilteredItems = this.items?
.OfType<DirectoryItem>()
.Select(d => new DirectoryItem()
{
Name = d.Name,
Path = d.Path,
Items = d.Items.Where(x => x.Name.Contains(value)).ToList(),
})
.Where(x => x.Items.Any() is true);
}
}
MainWindow.cs
using System.Windows;
using System.Windows.Controls;
namespace WpfAppTreeViewTest;
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
public MainWindowViewModel ViewModel { get; } = new();
}
MainWindow.xaml
<Window
x:Class="WpfAppTreeViewTest.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:local="clr-namespace:WpfAppTreeViewTest"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
x:Name="ThisWindow"
Title="MainWindow"
Width="800"
Height="450"
mc:Ignorable="d">
<StackPanel>
<StackPanel.Resources>
<HierarchicalDataTemplate
DataType="{x:Type local:DirectoryItem}"
ItemsSource="{Binding Items}">
<TextBlock
Text="{Binding Path=Name}"
ToolTip="{Binding Path=Path}" />
</HierarchicalDataTemplate>
<DataTemplate DataType="{x:Type local:FileItem}">
<TextBlock
Text="{Binding Path=Name}"
ToolTip="{Binding Path=Path}" />
</DataTemplate>
</StackPanel.Resources>
<TextBox Text="{Binding ElementName=ThisWindow, Path=ViewModel.FilterText, Mode=OneWayToSource, UpdateSourceTrigger=PropertyChanged}" />
<TreeView ItemsSource="{Binding ElementName=ThisWindow, Path=ViewModel.FilteredItems}">
<TreeView.Resources>
<Style TargetType="TreeViewItem">
<Setter Property="IsExpanded" Value="True" />
</Style>
</TreeView.Resources>
</TreeView>
</StackPanel>
</Window>
TreeView?Itemclass for both files and folders, you might wanna add a flag, something likebool IsFileor something to be able to apply the filter only to files.