Introduction

FileExplorer is a WPF control that allow user to browse filesystem contents.  FileExplorer1 (ver1) consist of a directory tree and file list, although it's ver1 it supports shell directory, context menu, drag and drop, multi-select in file list. 

However, in ver1, the base currency is defined as System.IO.FileSystemInfoEx (Ex), that means the control communicate using a DirectoryInfoEx.  As I maintained both Ex and COFE(ExA), I had to maintain two copies of the source code, which create additional workloads and inconsistancy.  The original design also disallow the implementation of other File System entities.  

As Windows 8 metro application will disallow Win32, and will require to use Windows Runtime (WinRT) to access the file system, a new FileExplorer has to be coded to work with Metro or other kind of file systems.

ver2 is a rewrite of the controls using the same Model-View-ViewModel (MVVM) pattern, and solved some of the issues in ver1.

Index


The Demo

FileExplorer2

The demo is a WPF window with only one control named Explorer2.

To use the control you have to include the files in Explorer2TestProj\Ex directory, and reference the following libraries

Then you can use the control in xaml:

<uc:Explorer2 x:Name="explr" Style="{DynamicResource qz_Explorer_Style}"                    
    Background="{Binding Background, ElementName=mainWindow}" />           
and set the code-behind view model :
explr.DataContext = new ExExplorerViewModel(new ExProfile());

Noticed that unlike ver1, there is only a full Explorer component (no DirectoryTree or FileList component) provided, if you want an individual control, just create your own style based on  "qz_Explorer_Style" in \Themes\Explorer.xaml, and remove the unwanted controls.

Whats new?

Extra Controls

In addition to Directory tree and File list, FileExplorer2 (v2) include Breadcrumb, Statatusbar and Notification bar.
All data and control templates of the control and now reside under Themes directory.

Generic EntryModels

ver1 was tightly coupled with FileSystemInfoEx, which makes it hard to work with othert type of entity.

Another issue is that, because the currency is a Directory (DirectoryInfoEx) instead of a Model (DirectoryModel), it's common that for a single request (browsing a directory), the directory may loads multiple times by the DirectoryTree and FileList.

The dependency of the FileExplorer and the entity (FileSystemInfoEx) is removed.   User now define their own DirectoryModel/FileModel, which inherited from EntryModel.  The model is defined as Generic, e.g. EntryModel<FI,DI,FSI>. 

One Large ViewModel

ClassDiagram -
        ViewModels

In  v1, each control (DirectoryTree and FileList) are implemented as a stand-alone control, and the currency (CurrentDirectory), which is a DirectoryInfoEx, and is bound among controls using dependency property.  this make the development simple. 

However it's getting hard to control when there's more controls (e.g. breadcrumb, navigator) bind each other using BindingMode.TwoWay, so it became unrealiable if more than one control try to change the current directory.

The another change is that the multiple control approach is replaced by one large view model (see the diagram above), ExplorerViewModel now consists of the following view models:

Asynchronous loading and lookup

v1 introduce asynchronous loading by using the AsyncObservableCollection, which load the entries (in a linq statement) in background thread, and notify UI thread to add or remove an item in UI thread.  This is required because UI thread shouldn't be blocked by a slow operation, and the UI code fails if an UI object is changed by a non-UI thread.

v2 uses the same loading mechanism too, but they use a different mechanism to find and select an entry.

To lookup an entry, v1 create a new thread (backgroundworker) to load the appropriate entries down the hierarchy tree, till it found the requested entry.

(DirTreeViewModel.LookupChild() method)
public override TreeViewItemViewModel LookupChild(IFileSystemInfoExA node, Func cancelCheck)
{
    ...
    foreach (TreeViewItemViewModel subModel in tvm)
    {
        if (subModel.Equals(node))
            return subModel;
        else
            if (subModel is DirTreeViewModel)
            {
                DirTreeViewModel subDirModel = subModel as DirTreeViewModel;

                IDirectoryInfoExA subDir = subDirModel.EmbeddedDirModel.EmbeddedDirEntry;
                if (node.Equals(subDir))
                    return subDirModel;
                else if (subDirModel.CanLookUp && COFETools.HasParent(node, subDir))
                    return subDirModel.LookupChild(node, cancelCheck);
            }
	...
}
The problem is that:
  1. Because the lookup is done synchronously, only one lookup can be run at a time, and the directory tree may look freezing while lookuping.
  2. Only one level of hierarchy can be processed at a time, so if a directory contains 2048 sub-directories, it will continues to load the next 2047 sub-directories even if the requested entry is under the first sub-directory, before continue the lookup in the first sub-directory.

Bounty system

v2 solved the problem by reversing the process, the root model now contains a property named Bounty, which represents the entry requested to be selected, a PlaceBounty() method is available to set this property:
public virtual void PlaceBounty(EntryModel bountyModel)
{
    if (bountyModel is DirectoryModel)
    {

        if (SelectedNavViewModel != null &&
            SelectedNavViewModel.EmbeddedDirModel.EmbeddedDir.Equals(bountyModel))
        {
            //False alarm, already Selected
            SelectedNavViewModel.IsSelected = true;
            return;
        }
        else
            if (SelectedNavViewModel != null &&
            SelectedNavViewModel.EmbeddedDirModel.HasChild(bountyModel.EmbeddedEntry))
            {
                //Fast mode, item is subentry of current selected
                Bounty = (DirectoryModel)bountyModel;
                SelectedNavViewModel.IsSelected = false;
                SelectedNavViewModel.PlaceBounty();

            }
            else
            {
                //Slow mode, iterate from root
                if (SelectedNavViewModel != null)
                    SelectedNavViewModel.IsSelected = false;

                Bounty = (DirectoryModel)bountyModel;

                foreach (NavigationItemViewModel subDir in
                    _subDirectories.OrderBy((nivm) => { return -nivm.EmbeddedEntryModel.CustomPosition; }))
                    if (subDir.EmbeddedDirModel.EqualsOrHasChild(bountyModel))
                    {
                        subDir.PlaceBounty();
                        break;
                    }
                    else subDir.CollapseAll();
            }
    }
}
The subdir's PlaceBouny() method notify them a new bounty is available, so they will perform a lookup:
public void PlaceBounty()
{
    if (_rootModel == null || _rootModel.Bounty == null || !IsDirectory || _isSeparator)
        return;

    if (EmbeddedDirModel.EqualsOrHasChild(_rootModel.Bounty))
    {
        if (EmbeddedDirModel.Equals(_rootModel.Bounty))
        {
            //Exchange bounty if cached.
            if (!EmbeddedDirModel.IsCached && _rootModel.Bounty.IsCached)
                _embeddedDirModel = _rootModel.Bounty;
            _rootModel.RequestBountyReward(this);
        }
        else
        {
            IsSelected = false;
            if (_isInited)
            {
                IsExpanded = true;
                foreach (NavigationItemViewModel subDirVM in
                    _subDirectories.OrderBy((nivm) 
                     => { return -nivm.EmbeddedEntryModel.CustomPosition; }))
                if (subDirVM.EmbeddedDirModel.EqualsOrHasChild(_rootModel.Bounty))
                    {
                        subDirVM.PlaceBounty(); //Tell sub-directory bounty is up.
                        break;
                    }
                    else subDirVM.CollapseAll(); //NOYB, collapse!
            }
            else
            {
                //Let greed does all the work....
                IsExpanded = true;
            }
        }
    }
    else IsSelected = false;
}

Notice that, instead of loading the subentries, it change the IsExpanded property (in background thread, which is bound to the UI), which will trigger the subdirectory in UI to be expanded.  UI thread(not the background thread) will then load sub-folders, and continue looking up for bounty.

When an entry figured out that it's the bounty, it calls RootModel's RequestBountyReward(this) method to notify the requested entry is found, so RootModel can clear the Bounty.

So instead of using a thread to perform a lookup from top to bottom of hierarchy, expanding them manually,  v2 allows each level of child item to use their IsExpanded property to locate the entry, and thread issue is avoided.




How to use?

This version uses EntryVM as its base currency, which remove the dependency to Ex, but because of the another layer, user is responsible to write their own EntryModels(EntryM), Profile and IconExtractor. This has been implemented in the demo if you are going to use Ex only.

EntryModel<FI,DI,FSI>

ClassDiagram - Entry

EntryM is a Model which represents a File or Directory entity, it contains a property named EmbeddedEntry which returns the actual entry.

Most Models and ViewModels in FileExplorer2 are generic classes, for examples, EntryModel :
public class EntryModel<FI, DI, FSI> { ... }
where FI means FileInfo (non-container) , DI means DirectoryInfo (container) and FSI means FileSystemInfo (common ancestor for FI/DI).

As a user of FileExplorer, you shall extends the FileModel(FileM) and DirectoryModel(DirM), which is inherits from EntryM.

In the case of PIDL, it's FileInfoEx, DirectoryInfoEx and FileSystemInfoEx.
public class ExFileModel: FileModel<FileInfoEx,DirectoryInfoEx,FileSystemInfoEx> { .... }
public class ExDirectoryModel: DirectoryModel<FileInfoEx,DirectoryInfoEx,FileSystemInfoEx> { .... }
In the case of COFE, it's IFileInfo, IDirectoryInfo and IFileSystemInfo.
public class ExAFileModel: FileModel<IFileInfo,IDirectoryInfo,IFileSystemInfo> { .... }
public class ExADirectoryModel: DirectoryModel<IFileInfo,IDirectoryInfo,IFileSystemInfo> { .... }

EntryModel contains a number of methods to be overrided, e.g.:

EntryVM is a ViewModel which add functionalities to EntryM.  One examples is DirVM.SubEntries, SubFiles and SubDirectories, which list it's sub-contents asynchronously.
User do not have to extends EntryVM, EntryM has a ToViewModel() method to convert itself to a EntryVM. 


Profile

ClassDiagram -
        Profile

While EntryModel represents an entity, Profile act as a utility class for your implementation, you have to override the following methods:


Drag and Drop

Drag n Drop

NavigationRootVM (Directory tree root), NavigationItemVM (Drectory tree item) and DndDirectoryViewerVM (File list) implements ISupportDrag<EntryM> and ISupportDrop<EntryM>, which contain drag and drop related methods, like SelectedItems (items being dragged), CurrentDropTarget (to know the destination)  and Drop() method (initialize the Dnd).

A DragDropHelper is a static class, once registered, will look for the interfaces (ISupportDrag and ISupportDrop) in the View Models (from the control's DataContext), and uses the methods to facilitate Drag and Drop. 

FileDragDropHelper is used to handle File based Drag and Drop, which uses the FileDrop from the incoming DataObject.  To improve performance, FileDragDropHelper also store the dragging item in a private property named _dataObj, so inter-application drag and drop is done based on that local variable.  (_dataObj is a VirtualDataObject, it delay the construction of the files till they are dropped). 

To use FileDragDropHelper, one has to call the ExplorerVM.RegisterDragAndDrop() method, and override the Profile.GetSupportedAddAction() method, which  is used to determine if the source (array of FI) and be added to the target (a DI), you can return an AddActions enum (All, Move, Copy, Link, None). 

If the AddActions is not None, DragDropHelper, will call Profile.Put() method, which calls Profile.Copy() and Link() method.

Search

Suggestion

The input section of Navigation bar display suggestion based on input, to enable this feature it uses the Profile.Lookup() method:

The following examples return the typed path concated with "_FileExplorer2".

public override IEnumerable<Suggestion> Lookup(string lookupText)
{
    yield return new Suggestion(lookupText + "_FileExplorer2");
}

You can implement your own search mechanism here (e.g. Windows Search).

Status bar

ClassDiagram -
          Metadata

Notification bar uses profile.GetMetadata() method to obtain metadata of one or multiple EntryModels.  GetMetadata() return a IEnumerable of GenericMetadataM.  GenericMetadataMlet you specify a type, different type will be displayed differently:

Type
Display as
int,uint,long,ulong,float,double
String
short
Percentage bar
DateTime
Format as DateTime String
string
String
string[]
Comma separated string

The type is identified by MetadataVM (which is constructed by using the ToViewModel() method), using the IsDateTime/Number/Percent/String/StringArray properties.  Then this property is accessed by Statusbar's DataTemplate (Statusbar.xaml) to determine what to show.

Statusbar sample

EntryMetadaM is inherited from GenericMetadataM, which specify that the metadata is related to one or more EntryMs, the following code construct the statusbar item shown above.

//Without the 4th parameter, so no header, and occupy whole line.
yield return new EntryMetadataModel<String, FileInfoEx, DirectoryInfoEx, FileSystemInfoEx>(
appliedModels,"ddddd (1).txt", "Key_Label");

//With the 4th parameter
yield return new EntryMetadataModel<String, FileInfoEx, DirectoryInfoEx, FileSystemInfoEx>(
appliedModels,UITools.SizeInK((ulong)model.Length), "Key_Size", "Selected size");

//If the generic type is short, display as percentage progress bar.
yield return new EntryMetadataModel<short, FileInfoEx, DirectoryInfoEx, FileSystemInfoEx>(
appliedModels, 13, "Key_Percent", "13 Percent");

//If the generic type is DateTime, display as datetime.
yield return new EntryMetadataModel<DateTime, FileInfoEx, DirectoryInfoEx, FileSystemInfoEx>(
appliedModels, DateTime.Now, "Key_Now", "Now");

Notification bar

ClassDiagram
          - Notification

Notification bar display NotificationItemVM at the top of the Statusbar, these NotificationItemVM are generated by NotificationSourceVM.  NotificationItemVM have a list of properties for customization:

CanDoWork
If true, double click the NotificationItem will execute DoWork() method.
IsProgressShown
Circular progress bar on the left side of a NotificationItem, use PercentCompleted to set it's progress.
Icon
Bitmap of the item.
Header
Header text.
HintMessage / HintTitle
Shown when mouse over the NotificationItem.
Priority
Determine the position of an item.
GetCommands() method.
Return a list of commands (in CommandModel), which shown in dropdown menu.

One can define a NotificationSourceM, which implements GetNotificationItems() and NotifyItemUpdate() methods.   These NotificationSourceM can be returned in the Profile.GetNotificationSources() method.

public override IEnumerable<NotificationSourceModel> GetNotificationSources()
{
    yield return ExWorkNotificationSourceModel.Instance; }

Tool bar

ClassDiagram -
          Command


Each button (include the dropdown) on the toolbars are actually a CommandModel in the code. When selection changes, toolbar calls the Profile.GetCommands() method. 

CommandModel is an abstract class, there are a number of usable derived class (see the diagram), the model that derived from DirectoryCommandModel have IsDirectory set to true, which will shown on the toolbar as a dropdown (or sub-menu if it's already in a dropdown).

The following sample inserted a close button in the main menu:

FileExplorer -
        Toolbar

public override IEnumerable<CommandModel> GetCommands(EntryModel<FileInfoEx, DirectoryInfoEx, FileSystemInfoEx>[] appliedModels)
{
    yield return new GenericCommandModel(ApplicationCommands.Close);
}

Noted that organize, toggle preview and viewmode dropdown is predefine as default, if you want to remove them you will have to modify DirectoryViewerVM.getActionModel() and remove them manually.

IconExtractor

ClassDiagram -
        IconExtractor

Profile.IconExtractor property return an IconExtractor which responsible to return icon for one entity, retrieve an icon is a two step process,
  1. It calls IconExtractor.GetIconKey(), which return a string representation for the icon
  2. Then it calls IconExtractor.GetIcon() with the key, and retrieved the icon.
Because constructing an icon (using GetIcon(), which usually leads to WinAPI) method is slower than load from cache, entity with same IconKey can share the same icon resource.

Conclusion

v2 does decoupled the dependency of FileSystemInfoEx and improve the lookup and overall response speed,

As you see, there still a number off issues:
  1. Profile class is overly complicated, it should be splitted into multiple interface when it's refactored.
  2. IconExtractor still rely on Win32, FileExplorer should ask profile for icon instead of having it's own implementation.