Skip to main content

How to: Work with TreeView

This guide covers common TreeView scenarios: hierarchical data binding, lazy loading, selection handling, and customization.

Basic Hierarchical Binding

Bind a TreeView to a tree of view model objects using HierarchicalDataTemplate:

public class FolderItem
{
public string Name { get; set; } = "";
public ObservableCollection<FolderItem> Children { get; } = new();
}
<TreeView ItemsSource="{Binding RootFolders}">
<TreeView.ItemTemplate>
<TreeDataTemplate ItemsSource="{Binding Children}">
<TextBlock Text="{Binding Name}" />
</TreeDataTemplate>
</TreeView.ItemTemplate>
</TreeView>

TreeDataTemplate is the key: its ItemsSource property tells the TreeView where to find child items for each node. The same template is applied recursively at every level.

Multiple node types

Use DataTemplateSelector patterns with DataType to display different node types:

public class FolderNode
{
public string Name { get; set; } = "";
public ObservableCollection<object> Children { get; } = new();
}

public class FileNode
{
public string Name { get; set; } = "";
public long Size { get; set; }
}
<TreeView ItemsSource="{Binding RootItems}">
<TreeView.DataTemplates>
<TreeDataTemplate DataType="local:FolderNode" ItemsSource="{Binding Children}">
<StackPanel Orientation="Horizontal" Spacing="4">
<PathIcon Data="{StaticResource FolderIcon}" />
<TextBlock Text="{Binding Name}" />
</StackPanel>
</TreeDataTemplate>
<DataTemplate DataType="local:FileNode">
<StackPanel Orientation="Horizontal" Spacing="4">
<PathIcon Data="{StaticResource FileIcon}" />
<TextBlock Text="{Binding Name}" />
<TextBlock Text="{Binding Size, StringFormat='{}{0:N0} bytes'}"
Foreground="Gray" />
</StackPanel>
</DataTemplate>
</TreeView.DataTemplates>
</TreeView>

FileNode uses a regular DataTemplate (no ItemsSource) because files have no children. FolderNode uses TreeDataTemplate to allow expansion.

Selection

Single selection

Bind SelectedItem to track the selected node:

<TreeView ItemsSource="{Binding Items}"
SelectedItem="{Binding SelectedNode}">
[ObservableProperty]
private object? _selectedNode;

partial void OnSelectedNodeChanged(object? value)
{
if (value is FolderNode folder)
LoadFolderContents(folder);
}

Multiple selection

Enable multiple selection with SelectionMode:

<TreeView ItemsSource="{Binding Items}"
SelectionMode="Multiple">

Access selected items through the SelectedItems property in code-behind, or use the SelectionChanged event:

private void OnSelectionChanged(object? sender, SelectionChangedEventArgs e)
{
var tree = (TreeView)sender!;
var selectedItems = tree.SelectedItems;
// Process selected items
}

Lazy Loading (Load on Expand)

For large trees where loading all children up front is expensive, load children on demand when the user expands a node:

public partial class LazyFolderNode : ObservableObject
{
private bool _isLoaded;

public string Name { get; }
public string Path { get; }
public ObservableCollection<LazyFolderNode> Children { get; } = new();

// Start with a dummy child so the expand arrow appears
public LazyFolderNode(string name, string path, bool hasChildren = true)
{
Name = name;
Path = path;
if (hasChildren)
Children.Add(new LazyFolderNode("Loading...", "", false));
}

[ObservableProperty]
private bool _isExpanded;

partial void OnIsExpandedChanged(bool value)
{
if (value && !_isLoaded)
{
_isLoaded = true;
LoadChildren();
}
}

private void LoadChildren()
{
Children.Clear();
foreach (var dir in Directory.GetDirectories(Path))
{
var name = System.IO.Path.GetFileName(dir);
Children.Add(new LazyFolderNode(name, dir));
}
}
}

Bind IsExpanded in the TreeDataTemplate:

<TreeView ItemsSource="{Binding RootFolders}">
<TreeView.Styles>
<Style Selector="TreeViewItem">
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}" />
</Style>
</TreeView.Styles>
<TreeView.ItemTemplate>
<TreeDataTemplate ItemsSource="{Binding Children}">
<TextBlock Text="{Binding Name}" />
</TreeDataTemplate>
</TreeView.ItemTemplate>
</TreeView>

The TreeViewItem style binds IsExpanded on the container to the view model property. When the user expands a node, the setter triggers OnIsExpandedChanged, which loads the children.

Async Lazy Loading

For loading children from a database or API:

partial void OnIsExpandedChanged(bool value)
{
if (value && !_isLoaded)
{
_isLoaded = true;
_ = LoadChildrenAsync();
}
}

private async Task LoadChildrenAsync()
{
var items = await _service.GetChildrenAsync(Id);

Children.Clear();
foreach (var item in items)
Children.Add(new LazyFolderNode(item));
}

Since LoadChildrenAsync is async, it returns to the UI thread after await, so updating Children is safe without explicit dispatcher calls.

Expanding and Collapsing Programmatically

To expand or collapse all nodes, walk the tree:

private void ExpandAll(IEnumerable<LazyFolderNode> nodes)
{
foreach (var node in nodes)
{
node.IsExpanded = true;
ExpandAll(node.Children);
}
}

private void CollapseAll(IEnumerable<LazyFolderNode> nodes)
{
foreach (var node in nodes)
{
node.IsExpanded = false;
CollapseAll(node.Children);
}
}

Search and Filter

Filter the tree by hiding nodes that do not match a search term. Since TreeView does not have built-in filtering, rebuild the visible tree from the source data:

[ObservableProperty]
private string _searchText = "";

partial void OnSearchTextChanged(string value)
{
FilteredItems.Clear();
foreach (var root in _allItems)
{
var filtered = FilterNode(root, value);
if (filtered is not null)
FilteredItems.Add(filtered);
}
}

private FolderNode? FilterNode(FolderNode node, string search)
{
// Check if this node matches
var matches = node.Name.Contains(search, StringComparison.OrdinalIgnoreCase);

// Recursively filter children
var filteredChildren = node.Children
.Select(c => FilterNode(c, search))
.Where(c => c is not null)
.ToList();

// Include this node if it matches or has matching children
if (matches || filteredChildren.Count > 0)
{
var result = new FolderNode { Name = node.Name };
foreach (var child in filteredChildren)
result.Children.Add(child!);
return result;
}

return null;
}

Drag and Drop in TreeView

Enable drag-and-drop to rearrange tree nodes:

<TreeView ItemsSource="{Binding Items}"
DragDrop.AllowDrop="True">
<TreeView.Styles>
<Style Selector="TreeViewItem">
<Setter Property="DragDrop.AllowDrop" Value="True" />
</Style>
</TreeView.Styles>
</TreeView>

Handle the drag events in code-behind or use behaviors. See Drag and Drop for the full API.

Styling TreeViewItem

Customize the appearance of tree nodes:

<TreeView.Styles>
<!-- Change the expand/collapse icon -->
<Style Selector="TreeViewItem:empty /template/ ToggleButton#PART_ExpandCollapseChevron">
<Setter Property="IsVisible" Value="False" />
</Style>

<!-- Highlight selected items -->
<Style Selector="TreeViewItem:selected /template/ ContentPresenter#PART_HeaderPresenter">
<Setter Property="Background" Value="{DynamicResource SystemAccentColor}" />
<Setter Property="Foreground" Value="White" />
</Style>

<!-- Add indentation -->
<Style Selector="TreeViewItem">
<Setter Property="Padding" Value="4" />
</Style>
</TreeView.Styles>

See Also