Figure 0. Playing with TreeView, demo. Note in Single Tree demo
dropdownlist to select the Root (Drive, DriveNoChild, "Favorites", Special Places).
Right plane Tabbed Trees with fixed Tab positions. Demo program works only for Windows 7.
Table of Contents
- Introduction
- Background
- Model
- RootItems to define the tree
- Interface and abstract class NavTreeItem
- Example DriveItem, a derived class
- Using reflection and convention for listing and creation of RootItems
- Rebuilding the tree
- ViewModel
- View
- MainViewWindow
- NavTreeView
- TabbedNavTreesView
- Points of Interest
- Limitations
- History
Introduction
This article describes just another way of using a WPF TreeView to make a Tabbed
TreeView that can be used in a File Explorer.
The plan is to write two articles. The second article will be about adding a tabbed
FolderPlane to get a minimal toy "File Explorer". The resulting program can serve
as a mock-up to experience how well tabs work in a File Explorer.
My main purpose is not to write a fully functional file explorer but to play, learn
a little bit more C#/WPF (I read some books but I am still in the copy/paste phase)
and have my first hands-on MVVM programming experience. Please report main flaws
so that I can learn.
A lot of articles already have been written about using the WPF tree view, this
is just another way of using the TreeView for a specific application. Some topics
might be of interest:
- NavTreeItems: Abstract base class and derived classes to define Icon and Children.
- Design of trees by defining their RootItems and their children.
- Listing all RootItems and creating one given a number using reflection and some
convention.
- Implementation of DriveRootItem, DriveItem, FolderItem, FileItem as an example.
- Rebuild the tree by taking a snapshot in the Model of all expanded items using the
NavTreeItem names and use it to rebuild a new tree.
- Set selected path by using a button and command. In the tabbed File Explorer the
tree is used for selection and the selected path must always be set, even if the
current selected TreeItem is clicked a second time.
- Have Trees in fixed Tabs, here we do not want the Tab to move to the bottom row
when selected.
Background
Note that for the most direct result the first choice would be to use ShellItems
(many examples in Code Project articles) or
the Windows API pack to deal with the Operating System (Windows 7 Libraries,
W7 File Explorer Favorites etc.). See for a sample of a more functional File Explorer
a recently updated article
WPF x FileExplorer x MVVM by Leung Yat Chun.
For an introduction to WPF of
Sacha Barber see
here. For some code samples what WPF can do from Microsoft see
here or the only paper WPF book I own "WPF
Control Development Unleashed", see the download tab on
that page.
Many articles over the TreeView have been written. I was inspired and intrigued
by the minimal code to
lazy-load the TreeView by Karl Shiflett. When I read the article I thought
it was a kind of trick, but actually it is standard WPF to define a (hierarchical)
DataTemplate that determines how a Class is presented.
One topic in the TreeView is to bring an item into view, see for example
the article of Bea Stollnitz.
The second topic is the selected item. This is property is read-only so it is not
possible to bind to it as you should expect, see for example the following
Stackoverflow question about that. Josh Smith has written some articles
of how to use the TreeView in a MVVM way and using the IsSelected
property to set
and retrieve the selected item, see his article
"Simplifying the WPF TreeView by Using the ViewModel Pattern"
MVVM (Model-View-ViewModel) is a design pattern for writing WPF programs when separation
of concern and unit testing come into play, a classic article is here "WPF
Apps With The Model-View-ViewModel Design Pattern" by Josh Smith.
The concern of the view (often "XAML" only, preferable no code behind) is only how
to present data and describe the user generated events. The View could be
separately designed using Blend. Elements of the View bind to the elements of the
ViewModel: INotifyPropertyChanged
properties (or Dependency properties), ObservableCollections
and ICommands
. The ViewModel exposes data (for example from the Model or representation
of properties of the View), and deals with changes in the data or executes commands
on events from the View.
I will not explain MVVM further but instead I will try to use just the standard
basic MVVM techniques as I understand them as a MVVM beginner in a simple desktop
application. See the figure below that illustrates the separation of concern. Note
that in this simple application I will not use common practices as Blend interactive
behaviour to trigger commands from events, attached dependency properties, a MVVM
framework or external DLL's.
Figure 1. M-V-VM Separation of concern
Model
RootItems to define the tree
It should be possible to design arbitrary trees (so in theory create a tree with
hierarchical Favorites or the current history) and it should be easy to add a new
tabbed tree.
Currently we use the TreeView using MVVM in the following way. A RootItem is always
the root node of a Tree. A RootItem defines its children so it defines the Model
of the tree. The children of the RootItem are copied to the observable collection
RootChildren in the ViewModel. The RootChildren of the ViewModel are bound to the
TreeView in XAML in the View.
Currently I defined a few RootItems: the most important is the DriveRootItem and
for getting quickly some extra RootItems I added DriveNoChildRootItem
, "FavoritesRootItem
"
and SpecialFolderRootItem
. The DriveNoChildRootItem
children are DriveNoChild
(no
children), so we only get a list of not expandable drives which could be useful
if the number of drives is large. The SpecialFolderRootItem
is just used as an extra
and its relevance (or as alternative KnownFolders) for a file explorer is questionable.
In the Windows 7 File Explorer Tree we have standard nodes/groups Favorites, Libraries,
HomeGroup, Computer (= DriveRootItem) and Network so these are possible candidates
to be added.
I consider the Favorites the most important. You can drag drives and folders to
this group (will be stored as shortcuts), downloads and desktop are for example
default Favorites. However I did not find documentation for Favorites yet (probably
have to look into source Windows API pack). I made a short hack and used a fixed
location and name. Shortcuts are shown as normal icons, the items are ordered in
a standard way and the order can not be specified. Note that it should be possible
(for non Windows 7 users) to delete the class FavoriteRootItem
in code or change
the hard coded folder name and place some folder shortcuts in that folder.
Interface and abstract class NavTreeItem
For all Items in the Tree (including the RootItems) I decided to define classes.
See the code below for the interface these classes implement. Note that here the
Model exposes FriendlyName, MyIcon (both for display TreeItems in the TreeView),
FullPathName (for selection) and Children (for building the TreeView). The IncludeFileChildren
is specific for a Tree showing folders and files.
public interface INavTreeItem : INotifyPropertyChanged
{
string FriendlyName { get; set; }
BitmapSource MyIcon { get; set; }
string FullPathName { get; set; }
ObservableCollection<INavTreeitem> Children { get; }
bool IsExpanded { get; set; }
bool IncludeFileChildren { get; set; }
void DeleteChildren();
}
Next I define one abstract class NavTreeItem
, see code below. Derived classes act
as the actual Items in the Tree. Note that for assigning a value to the properties
MyIcon
and Children
abstract functions are used. This means that for defining a
derived class, only these procedures have to be specified, we will show a sample
later.
The following choices are open for discussion. Most properties are considered constant
during the lifetime of the tree and in XAML Mode=OneTime
binding is used. The IsExpanded
and Children properties are considered to be changeable during the lifetime of the
tree and we implement a INotifyPropertyChanged
for them. Note that in the code the
SetProperty()
implements the notification, see discussion
here. I have some doubt to implement INotifyPropertyChanged
in the Model
too but without a supporting MVVM framework this seems the best choice.
Initially we had a property IsInitiallyExpanded
without notification to control
only the initial expansion of the TreeItems. To rebuild the Tree from the Tree in
Model or ViewModel the IsExpanded
property it must be in sync with the TreeView
in the View so we implemented a notification and standard binding.
I doubt if it is necessary for a ObservableCollection
to implement a INotifyPropertyChanged
but for reason of symmetry I did. I show here the code of ObservableCollection
Children
for demonstration and discussion. DeleteChildren
is for setting the children to
null, in theory this should reset the tree.
public abstract class NavTreeItem : ViewModelBase, INavTreeItem
{
...
protected BitmapSource myIcon;
public BitmapSource MyIcon
{
get { return myIcon ?? (myIcon = GetMyIcon()); }
set { myIcon = value; }
}
...
protected ObservableCollection <INavTreeItem> children;
public ObservableCollection<INavTreeItem> Children
{
get { return children ?? (children = GetMyChildren()); }
set { SetProperty(ref children, value, "Children"); }
}
...
private bool isExpanded;
public bool IsExpanded
{
get { return isExpanded; }
set { SetProperty(ref isExpanded, value, "IsExpanded"); }
}
...
public abstract BitmapSource GetMyIcon();
public abstract ObservableCollection<INavTreeItem> GetMyChildren();
}
Example DriveItem, a Derived Class
As mentioned the most important RootItem is the DriveRootItem
with as children DriveItems.
As an example we will show the code to define the DriveItem class. Note that the
folders are not filtered in any way.
public class DriveItem : NavTreeItem
{
public override BitmapSource GetMyIcon()
{
return myIcon = Utils.GetIconFn.GetIconDll(this.FullPathName);
}
public override ObservableCollection<INavTreeItem> GetMyChildren()
{
ObservableCollection<INavTreeItem> childrenList = new ObservableCollection<INavTreeItem>() { };
INavTreeItem item1;
DriveInfo drive = new DriveInfo(this.FullPathName);
if (!drive.IsReady) return childrenList;
DirectoryInfo di = new DirectoryInfo(((DriveInfo)drive).RootDirectory.Name);
if (!di.Exists) return childrenList;
foreach (DirectoryInfo dir in di.GetDirectories())
{
item1 = new FolderItem();
item1.FullPathName = FullPathName + "\\" + dir.Name;
item1.FriendlyName = dir.Name;
item1.IncludeFileChildren = this.IncludeFileChildren;
childrenList.Add(item1);
}
if (this.IncludeFileChildren) .... FileItems added here
return childrenList;
}
}
Using Reflection and Convention for Listing and Creation of RootItems
The names of all RootItem classes end by convention with "RootItem". Using reflection
we can generate a list of all defined RootItem classes in the Model or create a
RootItem given a RootNr
, see code below. In the constructor of the TabbedNavTreesVM
we use this list and then fill up the remaining Tabs using DriveRootItems. This
means when an extra RootItem is defined in the model, it will appear in the TabbedTree.
public static NavTreeItem ReturnRootItem(int iRootNr, bool includeFileChildren = false)
{
Type selectedType = typeof(DriveRootItem);
string selectedName = "Drive";
var entityTypes =
from t in System.Reflection.Assembly.GetAssembly(typeof(NavTreeItem)).GetTypes()
where t.IsSubclassOf(typeof(NavTreeItem)) select t;
int i = 0;
foreach (var tt in entityTypes)
{
if (tt.Name.EndsWith(LastPartRootItemName))
{
if (i == iRootNr)
{
selectedType = Type.GetType(tt.FullName);
selectedName = tt.Name.Replace(LastPartRootItemName, "");
break;
}
i++;
}
}
NavTreeItem rootItem = (NavTreeItem)Activator.CreateInstance(selectedType);
rootItem.FriendlyName = selectedName;
rootItem.IncludeFileChildren = includeFileChildren;
return rootItem;
}
Rebuilding the Tree
Normally TreeViews are used with datasources that generate a notification when changed.
At first I considered the file system/tree as constant, but in a last minute refactoring
I introduced an explicit rebuild command (can be triggered by a FileSystemWatcher
).
This allows to bring into view changes, such as an externally created new folder.
I considered Union, Intersect and Except to update the tree but that became too
complex for me. I choose for a simple robust but not so elegant solution: take a
snapshot of the expanded items of the tree, delete current RootChildren in the ViewModel,
create new RootChildren and expand all items from the snapshot, see code below.
The assumption is here that in a (Tabbed) file explorer the number of folders expanded
by the user is limited.
For the snapshot we use the FullPathName naming scheme and all current expanded
items and their children are recurrently visited. For the naming scheme of the drive
and its children it should be sufficient to add the FullPathName of each expanded
item to the snapshot stringlist. In the SpecialFolders one item can imply a longer
path of drive and folder names so all FullPathNames to the root are added with separators
to the snapshot string. This implies that each naming scheme where the FullPathName
is unique for children is supported.
public void RebuildTree(int pRootNr = -1, bool pIncludeFileChildren = false)
{
List<String> SnapShot = NavTreeUtils.TakeSnapshot(rootChildren);
...
NavTreeItem treeRootItem = NavTreeRootItemUtils.ReturnRootItem(RootNr, pIncludeFileChildren);
...
foreach (INavTreeItem item in treeRootItem.Children) { RootChildren.Add(item); }
...
NavTreeUtils.ExpandSnapShotItems(SnapShot, treeRootItem);
}
ViewModel
The ViewModel consists of tree classes: NavTreeVm
, TabbedNavTreesVm
and MainVm
,
see the pseudo code below. In the MainVm
the property setters of RootNr
and IncludeFiles
include a function call to SingleTree.RebuildTree(RootNr, IncludeFiles)
and the
RebuildTreeCommand
makes an extra call to TabbedNavTrees.SelectedNavTree.RebuildTree()
.
In this sample the code is relatively small but in the File Explorer where there
is a larger amount of simple code with more interaction going around it is clarifying
to have properties and commands instead of having a lot of code behind on all kinds
of Events.
Note in the code below that the ICommand SelectedPathFromTreeCommand
is implemented
in a standard MVVM way using the Relay command, see
here again. For the single statement we used a lambda expression "x => SelectedPath
= (x as string)" but we can replace that with "x => OnSelectedPathFromTree(x),x
=> TestCanExecuteThis(x)" to call a function and to add a test for CanExecute.
In writing the code I started with a single tree with all code in MainVm.cs and
MainViewWindow.xaml. When it worked I refactored the code to NavTreeVm.cs and NavTreeView.xaml.
Same for the TabbedTrees, code was moved to TabbedNavTreesVm
and TabbedNavTreesView
.
By using one common SelectedPathFromTreeCommand
there is not much interaction between
the ViewModel classes. Interaction of several ViewModels can be supported by a MVVM
framework.
public string TreeName
public int RootNr
public ObservableCollection<INavTreeItem> RootChildren
public void RebuildTree(int pRootNr = -1, bool pIncludeFileChildren = false)
public NavTreeVm(int pRootNumber = 0, bool pIncludeFileChildren = false)
public List<string> listNamesNavTrees
public ObservableCollection<NavTreeVm> NavTrees
public NavTreeVm SelectedNavTree
... MaxRowsNavTrees, TabsPerRow = 3 see comment in source
public TabbedNavTreesVm()
public NavTreeVm SingleTree
public int RootNr
public bool IncludeFiles
public ICommand RebuildTreeCommand
public TabbedNavTreesVm TabbedNavTrees { get; set; }
public string SelectedPath
RelayCommand selectedPathFromTreeCommand;
public ICommand SelectedPathFromTreeCommand
{
get
{
return selectedPathFromTreeCommand ??
(selectedPathFromTreeCommand =
new RelayCommand(x => SelectedPath = (x as string) ) );
}
}
public MainVm()
View
MainViewWindow
We have several XAML files: MainViewWindow, "User Controls" NavTreeView and TabbedNavTreesView
and finally MainResources (some common brushes and scrollbar). Note that I used
the "User Controls" just for organization of the XAML, they require the specific
Classes from the ViewModel. See code below for some snapshots of MainViewWindow.
Looking at the image of the demo and the properties in MainVm already gives a hint
what bindings will be used in the View.
In the code below first MainVm is defined as DataContext of the Window. WPF will
try in the rest of the XAML code of bindings try to bind to (sub)properties of the
DataContext. Next a NavTreeView is specified and bound to property SingleTree
of
Class MainVm and finally a TabbedNavTreesView
is specified and bound to the property
TabbedNavTrees
of class MainVm. As extra the standard binding of the ComboBox to
RootNr is shown.
To my understanding on execution an instance of class MainVm, named MyMainVm, is
created and assigned to the DataContext property of the instance of the Window.
<!---->
<Window.DataContext>
<vm:MainVm x:Name="MyMainVm"/>
</Window.DataContext>
...
<vw:NavTreeView DataContext="{Binding SingleTree}" />
...
<vw:TabbedNavTreesView DataContext="{Binding TabbedNavTrees}" />
...
// ComboBox shows standard binding RootNr
<ComboBox
DisplayMemberPath=""
ItemsSource="{Binding Path=TabbedNavTrees.listNamesNavTrees, Mode=OneTime}"
SelectedIndex="{Binding RootNr}" ToolTip="Choose a RootItem">
</ComboBox>
NavTreeView
See the code below for NavTreeView, we see a HierarchicalDatatemplate and a TreeView.
The
TreeView class is derived from class
ItemsControl and handles Items/Childs. Properties can be set for the
Control itself and for its children. For the TreeView ItemsSource, ItemTemplate
and ItemContainerStyle
are specified. ItemsSource specifies to use the RootChildren
property of the bound data (of class MainVm) to generate the items of the TreeView.
ItemTemplate is the (Hierarchical)DataTemplate used to display each item. The ItemContainerStyle
is used to bind to the IsExpanded
property.
The Hierarchical DataTemplate contains also an ItemsSource, bound to the
Children property. Next it defines a Button with a Command, an Image and a TextBlock.
All are bound to the appropriate properties of class NavTreeItem from the tree.
All bindings are very basic except the Command which uses a binding far beyond my
expertise. I use the "User Controls" just to organise the Xaml code but
to make the command more flexible it might be possible to introduce a TreeItemClickCommand
property to the NavTreeView
and bind the Button command to the TreeItemClickCommand
,
like the question
here.
....
<UserControl.Resources>
<HierarchicalDataTemplate x:Key="NavTreeTempl" ItemsSource="{Binding Path=Children}" >
<Button
Command="{Binding Path=DataContext.SelectedPathFromTreeCommand,
RelativeSource = {RelativeSource FindAncestor, AncestorType={x:Type Window}}}"
CommandParameter="{Binding FullPathName}"
Background="{x:Null}" BorderBrush="{x:Null}" Padding="0" Height="20"
Focusable="False" ClickMode="Press">
<StackPanel Orientation="Horizontal" Margin="0" VerticalAlignment="Stretch" >
<Image Source="{Binding Path=MyIcon, Mode=OneTime}" Stretch="Fill" />
<TextBlock Margin="5,0,0,0" Text="{Binding FriendlyName,Mode=OneTime}"/>
</StackPanel>
</Button>
</HierarchicalDataTemplate>
</UserControl.Resources>
<TreeView
ItemsSource="{Binding Path=RootChildren}"
ItemTemplate="{StaticResource NavTreeTempl}" >
<TreeView.ItemContainerStyle>
<Style TargetType="{x:Type TreeViewItem}">
<Setter Property="IsExpanded" Value="{Binding Path=IsExpanded, Mode=TwoWay}"/>
</Style>
</TreeView.ItemContainerStyle>
</TreeView>
TabbedNavTreesView
For the Tabbed Navigational Trees I had 2 requirements. First if we return to a
Tab with an expanded tree that tree must be show as we left it. Second the tabs
must be in fixed position. I always get confused when a Tab moves to the bottom
row when selected.
For historic reasons I mimic a TabControl by a ListBox and a NavTreeView. (I started
with a
TabControl but without notification for IsExpanded that did not work because
the TabControl does not preserve the visual tree. So I initially used IsExpanded
without notification and 2 coupled Listboxes). See the code for the TabbedNavTreesView
below, we see the following items
- A DataTemplate for NavTreeVm is defined that only presents the TreeName.
- A style specifies the ItemsPanel for the first Listbox. Normally each derived class
of ItemsControl has its own default ItemsPanel. Most standard WPF controls are lookless
(for lookless example see
here) and we can override the default, so we can choose a Canvas or a horizontal
StackPanel. Here we choose a UniformGrid. To prevent extra rows we use a binding
to MaxRowsNavTrees.
- Finally the ListBox and NavTreeView are specified.
.....
<UserControl.Resources>
<DataTemplate x:Key="templateNavTreeHeader" DataType="{x:Type vm:NavTreeVm}">
<!-- use stack panel and show in icon RootItem here -- -->
<TextBlock Margin="3,2,3,2" FontSize="10" Text="{Binding TreeName}"/>
</DataTemplate>
<Style x:Key="mimicTabControlHeader" TargetType="ListBox">
<Setter Property="ItemsPanel">
<Setter.Value>
<ItemsPanelTemplate>
<UniformGrid Rows="{Binding MaxRowsNavTrees}"/>
</ItemsPanelTemplate>
</Setter.Value>
</Setter>
</Style>
</UserControl.Resources>
... Grid definition omitted here
<ListBox
x:Name="NavTabHeaderLookAlike"
Grid.Row="0"
ItemsSource="{Binding NavTrees}"
SelectedItem="{Binding SelectedNavTree}"
Style="{StaticResource mimicTabControlHeader}"
ItemContainerStyle="{StaticResource selectedItemUseBrusch}"
ItemTemplate="{StaticResource templateNavTreeHeader}"
/>
<vw:NavTreeView Grid.Row="1" DataContext="{Binding SelectedNavTree}" />
Points of Interest
- With our choice of the interface of the INavTreeItem we have emphasized the elements
represented in the TreeView. This makes it easier to handle other kind of trees
(like hierarchical favorites) but requires some conversion compared to a Interface
based on ShellItems. The use of derived classes keeps the code organized. The conversions
from the File System are in the Model, which makes some sense in a File Explorer.
- When in the Windows 7 File Explorer Favorites are used and expanded we sometimes
have to scroll to reach an open folder in Computers/Drive. For this reason I wanted
to play a little bit with TabbedNavTrees (another solution is introducing an easily
accessible collapse/expand command). With tabs you can select and skip with one
mouse click to 3-12 Tabbed Trees. However the tree from the Windows 7 File Explorer
has a nicer, more spacious look.
- A refinement could be to design an Icon for each RootItem. By minor adoptions in
XAML we could add these Icons to the names in the tabs, or use the tabs with Icons
only in a vertical way.
- My intention is to write another, less detailed, article about adding Tabbed Folder
Planes using the same basic MVVM and WPF techniques. Because there is a little bit
more interaction the added value for MVVM will be more obvious.
Limitations
- The idea of this exercise is a first MVVM learning experience. The number of RootItems
is limited, I did not find documentation about Favorites.
- I did only limited testing on 1 Windows 7 PC. I understood from a remote Windows
XP friend that the program hangs, but I could not test what is causing the trouble.
Some comment is welcome.
History
2012-05-20 First Version article submitted.