Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Playing with a MVVM Tabbed TreeView for a File Explorer

0.00/5 (No votes)
23 May 2012 1  
This article describes one way of using the WPF TreeView to create tabbed Navigational Trees using MVVM for use in a File Explorer. In a next planned article a Tabbed Folder Plane will be added.

       

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.

// Highlights abstract class NavTreeItem
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"); }
    }
    ...

    // We will define these Methods in other derived classes ...
    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.

// Sample derived class: DriveItem, child of DriveRootItem
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)
{
    // Set default System.Type
    Type selectedType = typeof(DriveRootItem);
    string selectedName = "Drive";

    // Can you find other type given the conventions ..RootItem name and iRootNr
    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++;
        }
    }

    // Use selectedType to create root ..         
    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.

// From ViewModel NavTreeVM. 
// Take snapshot expanded items, clear RootItems, create new RootItems, expand snapshot 
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.

// Pseudo code for properties and functions in Classes ViewModel

// // Class NavTreeVM
public string TreeName
public int RootNr
public ObservableCollection<INavTreeItem> RootChildren
public void RebuildTree(int pRootNr = -1, bool pIncludeFileChildren = false)
// Constructor
public NavTreeVm(int pRootNumber = 0, bool pIncludeFileChildren = false)


// // Class TabbedNavTreesVm
public List<string> listNamesNavTrees    
public ObservableCollection<NavTreeVm> NavTrees
public NavTreeVm SelectedNavTree
... MaxRowsNavTrees, TabsPerRow = 3 see comment in source  
// Constructor, determines what trees are placed in Tabs
public TabbedNavTreesVm()


// // Class MainVm

// For Single Tree demo:
public NavTreeVm SingleTree
public int RootNr
public bool IncludeFiles
public ICommand RebuildTreeCommand

// For TabbedNavTree demo: 
public TabbedNavTreesVm TabbedNavTrees { get; set; }
public string SelectedPath

// For now SelectedPath common to all trees
// Sample of ICommand using RelayCommand
RelayCommand selectedPathFromTreeCommand;
public ICommand SelectedPathFromTreeCommand
{
    get
    {
        return selectedPathFromTreeCommand ??
                (selectedPathFromTreeCommand =
                        new RelayCommand(x => SelectedPath = (x as string) ) );
    }
}

// constructor, sets SingleTree and TabbedNavTrees on creation instance MainVm,
// creation here because class MainVm is used as DataContext in MainViewWindow
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.

    <!-- Snippets from MainViewWindow.xaml. Grids, buttons, selected path omitted -->

    <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.

// Form UserControl NavTreeView.xaml
.... 
<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

  1. A DataTemplate for NavTreeVm is defined that only presents the TreeName.
  2. 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.
  3. 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.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here