Wednesday, January 23, 2013

C# WPF TreeView File Explorer

How to explore file system using WPF TreeView control?

In C#, one of the most exciting topic is Shell. As what we've discuss in Displaying System Icon in C#, shelling is a technique of manipulating file system objects in the computer's operating system using the system APIs provided. In the previous version of Microsoft programming language like VB6.0, the common and popular name for this is FSO (or FileSystemObject). With the advent of the technology inventions, doing Shell is very easy compare to what we're doing with the older version of programming languages.

In this blog, we preferred to use the WPF technology since we already engage in some project that already using the WPF. In short, we already have our environment ready for it.

See below the image as what are our target output.


In the above Image, you can see a WPF TreeView control that already filled with the File System information. We just simply get the root drives and get the child directories and files of the drives in a lazy load technique. See below our actual code implementation and explanation.

TreeView Explorer Implementation

First and foremost, we created a new Window and place a TreeView control inside it. See the codes below.

<Window x:Class="CodesDirectory.WIN_TreeView"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="WIN TreeView" Height="300" Width="300">
    <TreeView Name="treeView" Margin="5"></TreeView>
</Window>

In order for us to display the list of the available
Drive in the file system, we need to use the System.IO.DriveInfo class to get the list of drives. The method GetDrives method will give us the information of the active system drives. See the code below our implementation.

public void LoadDirectories()
{
    var drives = DriveInfo.GetDrives();
    foreach (var drive in drives)
    {
        this.treeView.Items.Add(this.GetItem(drive));
    }
}

In our code above, we created the method LoadDirectories so it can be called during the construction of the Window. You will also notice that there is a method named GetItem passing the drive information. This method will actually return the actual TreeViewItem that binds the DriveInfo object into the TreeViewItem's DataContext and Tag property. See below our implementation.

private TreeViewItem GetItem(DriveInfo drive)
{
    var item = new TreeViewItem
    {
        Header = drive.Name,
        DataContext = drive,
        Tag = drive
    };
    this.AddDummy(item);
    item.Expanded += new RoutedEventHandler(item_Expanded);
    return item;
}

Again, the AddDummy method and the item_Expanded method is needed in this implementation. The AddDummy method is used to create a temporary dummy item inside each TreeViewItem (those bound to Directory and Drive). The purpose of this is to let the TreeViewItem be collapsible (you will notice there is an arrow before the item) WITHOUT loading the child directories and files. In the other side, the item_Expanded method is just an event handler to the Expanded event of the TreeViewItem. Later we'll explain how did we used this event when loading the child directories and files.

Additionally, in order for us to determine whether the item is dummy or not we are forced to create a new class that inherits the
TreeViewItem. We called it DummyTreeViewItem. See the code below our implementation.

public class DummyTreeViewItem : TreeViewItem
{
    public DummyTreeViewItem()
        : base()
    {
        base.Header = "Dummy";
        base.Tag = "Dummy";
    }
}

Now, since we are exploring the File System Objects, we are not just limited for a drive. We have to address also the other object like File and Directory. In this case, we need to create an overloaded method of GetItem that returns the actual TreeViewItem that binds the corresponding object. See below our implementation.

private TreeViewItem GetItem(DirectoryInfo directory)
{
    var item = new TreeViewItem
    {
        Header = directory.Name,
        DataContext = directory,
        Tag = directory
    };
    this.AddDummy(item);
    item.Expanded += new RoutedEventHandler(item_Expanded);
    return item;
}

private TreeViewItem GetItem(FileInfo file)
{
    var item = new TreeViewItem
    {
        Header = file.Name,
        DataContext = file,
        Tag = file
    };
    return item;
}

What you can see above is just an overloaded method of what like the GetItem method of the DriveInfo. The only difference is just we used the DirectoryInfo and FileInfo object instead. Notice that in the FileInfo GetItem method there is no AddDummy and Expanded event listener. This is because the File object is not expandable and doesn't contain any children.

Looks like very surprising, till now we're not yet written the
AddDummy implementation. We just only would like to be in the proper order before doing so. The code is below is our implementation for the AddDummy method.

private void AddDummy(TreeViewItem item)
{
    item.Items.Add(new DummyTreeViewItem());
}

private bool HasDummy(TreeViewItem item)
{
    return item.HasItems && (item.Items.OfType<TreeViewItem>().ToList().FindAll(tvi => tvi is DummyTreeViewItem).Count > 0);
}

private void RemoveDummy(TreeViewItem item)
{
    var dummies = item.Items.OfType<TreeViewItem>().ToList().FindAll(tvi => tvi is DummyTreeViewItem);
    foreach (var dummy in dummies)
    {
        item.Items.Remove(dummy);
    }
}

Yes, that's it. Very simple! We only added a new DummyTreeViewItem (the class we created above). The other methods named HasDummy and RemoveDummy, they are usable once the item Expanded will be triggered. For the explanation, the HasDummy is used to check whether the TreeViewItem has child DummyTreeViewItem, and the RemoveDummy is to simply remove the DummyTreeViewItem from the parent's TreeViewItem children.

Explore Directories and Files

Exploring directories and files is very easy in C#. With the use of the DirectoryInfo object from System.IO namespace, we can get the list of child directories and files. Please visit Microsoft documentation for your reference.

private void ExploreDirectories(TreeViewItem item)
{
    var directoryInfo = (DirectoryInfo)null;
    if (item.Tag is DriveInfo)
    {
        directoryInfo = ((DriveInfo)item.Tag).RootDirectory;
    }
    else if (item.Tag is DirectoryInfo)
    {
        directoryInfo = (DirectoryInfo)item.Tag;
    }
    else if (item.Tag is FileInfo)
    {
        directoryInfo = ((FileInfo)item.Tag).Directory;
    }
    if (object.ReferenceEquals(directoryInfo, null)) return;
    foreach (var directory in directoryInfo.GetDirectories())
    {
        var isHidden = (directory.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden;
        var isSystem = (directory.Attributes & FileAttributes.System) == FileAttributes.System;
        if (!isHidden && !isSystem)
        {
            item.Items.Add(this.GetItem(directory));
        }
    }
}

private void ExploreFiles(TreeViewItem item)
{
    var directoryInfo = (DirectoryInfo)null;
    if (item.Tag is DriveInfo)
    {
        directoryInfo = ((DriveInfo)item.Tag).RootDirectory;
    }
    else if (item.Tag is DirectoryInfo)
    {
        directoryInfo = (DirectoryInfo)item.Tag;
    }
    else if (item.Tag is FileInfo)
    {
        directoryInfo = ((FileInfo)item.Tag).Directory;
    }
    if (object.ReferenceEquals(directoryInfo, null)) return;
    foreach (var file in directoryInfo.GetFiles())
    {
        var isHidden = (file.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden;
        var isSystem = (file.Attributes & FileAttributes.System) == FileAttributes.System;
        if (!isHidden && !isSystem)
        {
            item.Items.Add(this.GetItem(file));
        }
    }
}

The method above named ExploreDirectories and ExploreFiles is used to explore the directory's child directories and files. It accepts one argument TreeViewItem (in our case the one that was returned by GetItem method bound by File, Directory or Drive), from there we start the exploration. You will also notice that in both method we used the GetFiles() and GetDirectories() method and iterate it. These two methods are very important to explore the child object of the directory.

Note: The GetFiles() and GetDirectories() method includes the System or Hidden objects in their result. So we must be aware of that before displaying it in the UI.

Also, you will notice that we used the
Bitwise comparison for the object Attributes. We first determined whether it is a System or a Hidden object, and if it is not we add it into the parent's TreeViewItem Items property.

Expanded Event

Now that we're almost done with our topic. One important thing we need to do is to manage how are we going to load the children of the file system object. We need to determine in what event and what our code is doing. In the TreeViewItem.Expanded event, as you remember, we added the item_Expanded event handler and there we did our job. We first checked whether the current item has a Dummy item and remove it. After that, we loaded the child directories and files with the methods we implemented above. See below our codes.

void item_Expanded(object sender, RoutedEventArgs e)
{
    var item = (TreeViewItem)sender;
    if (this.HasDummy(item))
    {
        this.Cursor = Cursors.Wait;
        this.RemoveDummy(item);
        this.ExploreDirectories(item);
        this.ExploreFiles(item);
        this.Cursor = Cursors.Arrow;
    }
}

You will also notice that we only call the RemoveDummy, ExploreDirectories and ExploreFiles method only if there is a Dummy node. This is because we used the Dummy object as a flag to the TreeViewItem state whether it is explored or not.

Lastly, we need to call the first method we created named LoadDirectories in the window's constructor so everything is initialized. See below the code.

public WIN_TreeView()
{
    InitializeComponent();
    this.LoadDirectories();
}

And that's it. We just finish the WPF TreeView file system explorer topic. Next topic is about the thing of loading the file system image into the TreeView.


Related topics:

3 comments:

  1. This comment has been removed by the author.

    ReplyDelete
  2. This is an awesome article... I've been away from coding for a little while so getting a well explained article like this speaks volume for me. I will read more of your topics.

    ReplyDelete
  3. One more thing...do you have an article that shows how to display the files in a separate panel. Once I select the directory the files are located it will show in the right panel or list box. Also I only want CSV or XLS files to display in that panel.

    ReplyDelete

Place your comments and ideas