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.