Table of Contents
C# WPF Treeview
Quick post showing how to create a nice little Treeview for a folder structure.
Folder Model
Lets start off with the Folder Model
public class FolderModel : INotifyPropertyChanged
{
private string _name;
public string Name
{
get => _name;
set
{
_name = value;
OnPropertyChanged(nameof(Name));
}
}
private ObservableCollection<FolderModel> _subFolders;
public ObservableCollection<FolderModel> SubFolders
{
get => _subFolders ?? (_subFolders = new ObservableCollection<FolderModel>());
set
{
_subFolders = value;
OnPropertyChanged(nameof(SubFolders));
}
}
public FolderModel()
{
SubFolders = new ObservableCollection<FolderModel>();
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}Folder View Model
Since we like MVVM lets create a simple view model.
public class FolderViewModel : INotifyPropertyChanged
{
private FolderModel _rootFolder;
public FolderModel RootFolder
{
get => _rootFolder;
set
{
_rootFolder = value;
OnPropertyChanged(nameof(RootFolder));
}
}
public FolderViewModel(string rootPath)
{
LoadFolderStructure(rootPath);
}
private void LoadFolderStructure(string path)
{
RootFolder = new FolderModel { Name = System.IO.Path.GetFileName(path) };
LoadSubFolders(RootFolder, path);
}
private void LoadSubFolders(FolderModel parentFolder, string path)
{
try
{
foreach (var directory in System.IO.Directory.GetDirectories(path))
{
try
{
var folderModel = new FolderModel { Name = System.IO.Path.GetFileName(directory) };
parentFolder.SubFolders.Add(folderModel);
LoadSubFolders(folderModel, directory);
}
catch (UnauthorizedAccessException)
{
// Handle inaccessible folder
var inaccessibleFolder = new FolderModel { Name = System.IO.Path.GetFileName(directory) + " (Access Denied)" };
parentFolder.SubFolders.Add(inaccessibleFolder);
}
}
}
catch (UnauthorizedAccessException)
{
// Handle the case where we can't even list the contents of the parent folder
parentFolder.Name += " (Access Denied)";
}
catch (Exception ex)
{
// Handle other potential exceptions
System.Diagnostics.Debug.WriteLine($"Error accessing folder {path}: {ex.Message}");
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}WPF XAML
Now we can put it together.
<Window x:Class="WpfApp11.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:WpfApp11"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Window.Resources>
<Style x:Key="ExpandCollapseToggleStyle" TargetType="ToggleButton">
<Setter Property="Focusable" Value="False"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ToggleButton">
<Border Width="16" Height="16" Background="Transparent">
<Path x:Name="ExpandPath" Stroke="#FF989898" Fill="Transparent"
Data="M0,0 L0,6 L6,0 z"
Width="8" Height="8" Stretch="Uniform"
HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsChecked" Value="True">
<Setter Property="Data" TargetName="ExpandPath" Value="M0,0 L8,0 L4,4 z"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="CustomContextMenuStyle" TargetType="{x:Type ContextMenu}">
<Setter Property="SnapsToDevicePixels" Value="True" />
<Setter Property="OverridesDefaultStyle" Value="True" />
<Setter Property="Grid.IsSharedSizeScope" Value="true" />
<Setter Property="HasDropShadow" Value="True" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ContextMenu}">
<Border
x:Name="Border"
Background="#F5F5F5"
BorderBrush="#ACACAC"
BorderThickness="1"
CornerRadius="4">
<StackPanel IsItemsHost="True" KeyboardNavigation.DirectionalNavigation="Cycle" />
</Border>
<ControlTemplate.Triggers>
<Trigger Property="HasDropShadow" Value="true">
<Setter TargetName="Border" Property="Padding" Value="0,3,0,3"/>
<Setter TargetName="Border" Property="Background" Value="#F5F5F5"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="CustomMenuItemStyle" TargetType="{x:Type MenuItem}">
<Setter Property="OverridesDefaultStyle" Value="True"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type MenuItem}">
<Border x:Name="Border"
Background="Transparent"
BorderBrush="Transparent"
BorderThickness="1"
CornerRadius="2">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="Icon"/>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" SharedSizeGroup="Shortcut"/>
<ColumnDefinition Width="13"/>
</Grid.ColumnDefinitions>
<ContentPresenter x:Name="Icon" Margin="6,0,6,0" VerticalAlignment="Center" ContentSource="Icon"/>
<ContentPresenter x:Name="HeaderHost" Grid.Column="1" ContentSource="Header" RecognizesAccessKey="True" VerticalAlignment="Center"/>
<ContentPresenter x:Name="InputGestureText" Grid.Column="2" Margin="5,2,0,2" ContentSource="InputGestureText" VerticalAlignment="Center"/>
</Grid>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsHighlighted" Value="true">
<Setter TargetName="Border" Property="Background" Value="#E0E0E0"/>
<Setter TargetName="Border" Property="BorderBrush" Value="#A0A0A0"/>
</Trigger>
<Trigger Property="IsEnabled" Value="false">
<Setter Property="Foreground" Value="#888888"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Window.Resources>
<Grid>
<TreeView ItemsSource="{Binding RootFolder.SubFolders}" Width="200">
<TreeView.Resources>
<HierarchicalDataTemplate DataType="{x:Type local:FolderModel}" ItemsSource="{Binding SubFolders}">
<StackPanel Orientation="Horizontal">
<Image Source="/WpfApp11;component/Assets/Icons/Folder.png"
Width="32" Height="32" Margin="0,0,10,0"/>
<TextBlock Text="{Binding Name}" VerticalAlignment="Center" FontSize="14"/>
</StackPanel>
</HierarchicalDataTemplate>
</TreeView.Resources>
<TreeView.ItemContainerStyle>
<Style TargetType="{x:Type TreeViewItem}">
<Setter Property="ContextMenu">
<Setter.Value>
<ContextMenu Style="{StaticResource CustomContextMenuStyle}">
<MenuItem Header="Open" Style="{StaticResource CustomMenuItemStyle}"
Command="{Binding DataContext.OpenCommand, RelativeSource={RelativeSource AncestorType=TreeView}}"
CommandParameter="{Binding}"/>
<MenuItem Header="Rename" Style="{StaticResource CustomMenuItemStyle}"
Command="{Binding DataContext.RenameCommand, RelativeSource={RelativeSource AncestorType=TreeView}}"
CommandParameter="{Binding}"/>
<MenuItem Header="Delete" Style="{StaticResource CustomMenuItemStyle}"
Command="{Binding DataContext.DeleteCommand, RelativeSource={RelativeSource AncestorType=TreeView}}"
CommandParameter="{Binding}"/>
</ContextMenu>
</Setter.Value>
</Setter>
<Setter Property="Background" Value="Transparent"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type TreeViewItem}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition/>
</Grid.RowDefinitions>
<Border x:Name="Bd"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
Background="{TemplateBinding Background}"
Padding="5,2,5,2"
SnapsToDevicePixels="true"
CornerRadius="4">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" MinWidth="19"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<ToggleButton x:Name="Expander"
Style="{StaticResource ExpandCollapseToggleStyle}"
IsChecked="{Binding IsExpanded, RelativeSource={RelativeSource TemplatedParent}}"
ClickMode="Press"/>
<ContentPresenter x:Name="PART_Header"
Grid.Column="1"
ContentSource="Header"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"/>
</Grid>
</Border>
<ItemsPresenter x:Name="ItemsHost" Grid.Row="1" Margin="20,1,0,0"/>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsSelected" Value="true">
<Setter Property="Background" TargetName="Bd" Value="#E0E0E0"/>
<Setter Property="BorderBrush" TargetName="Bd" Value="#A0A0A0"/>
<Setter Property="BorderThickness" TargetName="Bd" Value="1"/>
</Trigger>
<Trigger Property="IsExpanded" Value="false">
<Setter Property="Visibility" TargetName="ItemsHost" Value="Collapsed"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</TreeView.ItemContainerStyle>
</TreeView>
</Grid>
</Window>
Code Behind
Now to instantiate the ViewModel as the DataContext.
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext = new FolderViewModel(@"C:\Users\me\AppData\Roaming\something");
}
}Folder Icon
Here is the Folder icon i used.

Final Result
Here is a nice little treeview it needs commands and more but its a good start.

Quick Update
If we add this to the FolderModel we can then set which folder is active and affect it visually.
private bool _isActive = false;
public bool IsActive
{
get => _isActive;
set
{
_isActive = value;
OnPropertyChanged(nameof(IsActive));
}
}To affect it visually we need to add this Style trigger to the XAML.
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}" />
<Style.Triggers>
<DataTrigger Binding="{Binding IsActive}" Value="True">
<Setter Property="Background" Value="LightBlue"/>
</DataTrigger>
</Style.Triggers>Now we can do something like this.
