Friday, January 25, 2013

PART 2: C# WPF TreeView File Explorer with System Icons

This blog is just a continuation of our previous blog PART 1: C# WPF TreeView File Explorer with System Icons. So please visit it first before continuing in this blog.

And here, we are expecting that you already read the previous blog. Now, you're ready to go in this blog.

Actual Wrapper Implementation

Below you can see the actual implementation of the FileSystemObjectInfo wrapper class that we will going to use in our application.

public class FileSystemObjectInfo : BaseObject
{
    public FileSystemObjectInfo(FileSystemInfo info)
    {
        if (this is DummyFileSystemObjectInfo) return;
        this.Children = new ObservableCollection<FileSystemObjectInfo>();
        this.FileSystemInfo = info;
        if (info is DirectoryInfo)
        {
            this.ImageSource = FolderManager.GetImageSource(info.FullName, ItemState.Close);
            this.AddDummy();
        }
        else if (info is FileInfo)
        {
            this.ImageSource = FileManager.GetImageSource(info.FullName);
        }
        this.PropertyChanged += new System.ComponentModel.PropertyChangedEventHandler(FileSystemObjectInfo_PropertyChanged);
    }

    public FileSystemObjectInfo(DriveInfo drive)
        : this(drive.RootDirectory)
    {
        this.Drive = drive;
    }

    #region Properties

    public ObservableCollection<FileSystemObjectInfo> Children
    {
        get { return base.GetValue<ObservableCollection<FileSystemObjectInfo>>("Children"); }
        private set { base.SetValue("Children", value); }
    }

    public ImageSource ImageSource
    {
        get { return base.GetValue<ImageSource>("ImageSource"); }
        private set { base.SetValue("ImageSource", value); }
    }

    public bool IsExpanded
    {
        get { return base.GetValue<bool>("IsExpanded"); }
        set { base.SetValue("IsExpanded", value); }
    }

    public FileSystemInfo FileSystemInfo
    {
        get { return base.GetValue<FileSystemInfo>("FileSystemInfo"); }
        private set { base.SetValue("FileSystemInfo", value); }
    }

    private DriveInfo Drive
    {
        get { return base.GetValue<DriveInfo>("Drive"); }
        set { base.SetValue("Drive", value); }
    }

    #endregion

    #region Methods

    private void AddDummy()
    {
        this.Children.Add(new DummyFileSystemObjectInfo());
    }

    private bool HasDummy()
    {
        return !object.ReferenceEquals(this.GetDummy(), null);
    }

    private DummyFileSystemObjectInfo GetDummy()
    {
        var list = this.Children.OfType<DummyFileSystemObjectInfo>().ToList();
        if (list.Count > 0) return list.First();
        return null;
    }

    private void RemoveDummy()
    {
        this.Children.Remove(this.GetDummy());
    }

    private void ExploreDirectories()
    {
        if (!object.ReferenceEquals(this.Drive, null))
        {
            if (!this.Drive.IsReady) return;
        }
        try
        {
            if (this.FileSystemInfo is DirectoryInfo)
            {
                var directories = ((DirectoryInfo)this.FileSystemInfo).GetDirectories();
                foreach (var directory in directories.OrderBy(d => d.Name))
                {
                    if (!object.Equals((directory.Attributes & FileAttributes.System), FileAttributes.System) &&
                        !object.Equals((directory.Attributes & FileAttributes.Hidden), FileAttributes.Hidden))
                    {
                        this.Children.Add(new FileSystemObjectInfo(directory));
                    }
                }
            }
        }
        catch
        {
            /*throw;*/
        }
    }

    private void ExploreFiles()
    {
        if (!object.ReferenceEquals(this.Drive, null))
        {
            if (!this.Drive.IsReady) return;
        }
        try
        {
            if (this.FileSystemInfo is DirectoryInfo)
            {
                var files = ((DirectoryInfo)this.FileSystemInfo).GetFiles();
                foreach (var file in files.OrderBy(d => d.Name))
                {
                    if (!object.Equals((file.Attributes & FileAttributes.System), FileAttributes.System) &&
                        !object.Equals((file.Attributes & FileAttributes.Hidden), FileAttributes.Hidden))
                    {
                        this.Children.Add(new FileSystemObjectInfo(file));
                    }
                }
            }
        }
        catch
        {
            /*throw;*/
        }
    }

    #endregion

    void FileSystemObjectInfo_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
    {
        if (this.FileSystemInfo is DirectoryInfo)
        {
            if (string.Equals(e.PropertyName, "IsExpanded", StringComparison.CurrentCultureIgnoreCase))
            {
                if (this.IsExpanded)
                {
                    this.ImageSource = Shell.FolderManager.GetImageSource(this.FileSystemInfo.FullName, ItemState.Open);
                    if (this.HasDummy())
                    {
                        this.RemoveDummy();
                        this.ExploreDirectories();
                        this.ExploreFiles();
                    }
                }
                else
                {
                    this.ImageSource = Shell.FolderManager.GetImageSource(this.FileSystemInfo.FullName, ItemState.Close);
                }
            }
        }
    }

    private class DummyFileSystemObjectInfo : FileSystemObjectInfo
    {
        public DummyFileSystemObjectInfo()
            : base(new DirectoryInfo("DummyFileSystemObjectInfo"))
        {
        }
    }
}

Now, let's go the XAML and WPF stuff bindings.

We need to create a new Window object in our solution and add a new TreeView inside it. See below the code.

<Window x:Class="CodesDirectory.WIN_TreeViewWithIcon"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:classes="clr-namespace:CodesDirectory.Classes"
        Title="WIN TreeView with System Icons" Height="300" Width="300">
    <TreeView Name="treeView" Margin="5"></TreeView>
</Window>

We also need to override the default style of the items. In this case the ItemContainerStyle value should be modified. But unlike with other Style we only bind the IsExpanded property of the TreeViewItem into the IsExpanded property of FileSystemObjectInfo class (two-way direction). So every user action in the TreeViewItem state will also be applied in the bound objects. See below our new code.

<Window x:Class="CodesDirectory.WIN_TreeViewWithIcon"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:classes="clr-namespace:CodesDirectory.Classes"
        Title="WIN TreeView with System Icons" Height="300" Width="300">
    <TreeView Name="treeView" Margin="5">
        <TreeView.ItemContainerStyle>
            <Style TargetType="{x:Type TreeViewItem}">
                <Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}" />
                <Setter Property="KeyboardNavigation.AcceptsReturn" Value="True" />
            </Style>
        </TreeView.ItemContainerStyle>
    </TreeView>
</Window>

After that in the Resources property of the TreeView object. We have to use the HierarchicalDataTemplate object and targets the actual FileSystemObjectInfo class in the DataType property. With the use of this object, we can set the actual template that the TreeViewItem has participated. In our case, we need to create a template where there is an Image in the left and Label in the right. The image will do display the actual icon of the file system, it binds the ImageSource property of the FileSystemObjectInfo class, and the Label will be bind in the Name property of the FileSystemInfo (of type System.IO.FileSystemInfo) property of the FileSystemObjectInfo class. See below our actual codes now.

<Window x:Class="CodesDirectory.WIN_TreeViewWithIcon"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:classes="clr-namespace:CodesDirectory.Classes"
        Title="WIN TreeView with System Icons" Height="300" Width="300">
    <TreeView Name="treeView" Margin="5">
        <TreeView.ItemContainerStyle>
            <Style TargetType="{x:Type TreeViewItem}">
                <Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}" />
                <Setter Property="KeyboardNavigation.AcceptsReturn" Value="True" />
            </Style>
        </TreeView.ItemContainerStyle>
        <TreeView.Resources>
            <HierarchicalDataTemplate DataType="{x:Type classes:FileSystemObjectInfo}" ItemsSource="{Binding Path=Children}">
                <StackPanel Orientation="Horizontal">
                    <Image Source="{Binding Path=ImageSource, UpdateSourceTrigger=PropertyChanged}" Margin="0,1,8,1"></Image>
                    <TextBlock Text="{Binding Path=FileSystemInfo.Name}"></TextBlock>
                </StackPanel>
            </HierarchicalDataTemplate>
        </TreeView.Resources>
    </TreeView>
</Window>

Lastly, in the code behind, we need to explore the top level drives and add each drive in the FileSystemObjectInfo wrapper class and append it to the TreeView.Items property. See below the codes on how to do it.

public partial class WIN_TreeViewWithIcon : Window
{
    public WIN_TreeViewWithIcon()
    {
        InitializeComponent();
        var drives = DriveInfo.GetDrives();
        foreach (var drive in drives)
        {
            this.treeView.Items.Add(new FileSystemObjectInfo(drive));
        }
    }
}

And, congratulations to you for finishing this blog. You are now equipped with a new interesting programming technique called Shell.

Please follow us and be part of this blog site for the more very interesting stuff.

8 comments:

  1. Could you please post the full project code for download?
    Thanks a lot.

    ReplyDelete
  2. Useless without Project to download... what a shame, your solution looked quite interesting

    ReplyDelete
  3. Thank you for this tutorial. It was very informative and it eventually worked beautifully.

    Having the source code to download would have been extremely helpful, primarily because this project required so many namespaces not mentioned in your post and it was very time consuming hunting them down.

    ReplyDelete
  4. Just wanted to say "THANK YOU" :) works like a charm!
    It realy looks great and is well designed.

    ReplyDelete
  5. I really enjoyed with this lesson.
    Big like :)


    Thanks.

    ReplyDelete
  6. thanks for the sample, gave me a quick jump start on the topic and my project

    ReplyDelete
  7. I sincerely apologize for not being active here. I have moved my blogging to Medium and Twitter. Though, I tried my best to accommodate you on here.

    Here is the link to the working project:
    https://github.com/mikependon/Tutorials/tree/master/WPF/TreeViewFileExplorer

    Moving forward, I will place everything in my Github account.

    ReplyDelete
  8. Here is the new medium tutorial.
    https://medium.com/@mikependon/designing-a-wpf-treeview-file-explorer-565a3f13f6f2

    ReplyDelete

Place your comments and ideas