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

WPF x FileExplorer x MVVM

0.00/5 (No votes)
25 Nov 2012 3  
This article describe how to construct FileExplorer controls included DirectoryTree and FileList, using Model-View-ViewModel (MVVM) pattern.
MVVMFileExplorer

This article describe an obsoluted usercontrol which was released in 2009, it demonstrates how to construct FileExplorer controls included DirectoryTree and FileList, using Model-View-ViewModel (MVVM) pattern, and is no longer being maintained. A newer version of the explorer control is described in this article here in codeproject.

Introduction

After C# FileExplorer, VB.Net ExplorerTree, you may think it's easier to write an explorer tree in WPF. But in fact it's not, and thats why this article takes so many pages. Although WPF is supposed to allow you to construct custom interface with minium effort, WPF does not even provide the basic functionality of ListView in .Net2.0 :
  • ViewMode? Small Icon? What is that?
  • Multi-Select? Sure! all you need is to press <shift> and select each item.
  • Virtual ListView? ImageList? Rename? sure, write your own.
But anyone who run a WPF application should know WPF let you to customize everything, but you wont know these shortcoming until you are going implement one. There are a number of solutions available on various web sites, I combined these ideas as well as my code and present you my finhsied FileExplorer controls.

Further more, as these controls are written in Model-View-ViewModel (MVVM) pattern, so this article is also a brief tutorial for how to create the controls using the pattern.

Features :

  • Shell
    • List directories and files (start from Desktop)
    • Context menu
    • Rename inside the control
    • Drag and Drop support to and from other application
    • Monitor file system so automatically refresh when file system is changed
  • Performance
    • Sub-items are loaded in background
    • Lookup directory in DirectoryTree in background
    • Construction of ListViewItems and TreeViewItems are virtualized
  • DirectoryTree
  • FileList


Index


How to use?

Although the internal part is MVVM, communication between the UserControls are through Dependency properties, e.g.
 <TextBox Text="{Binding SelectedDirectory, ElementName=dirTree, 
 Mode=TwoWay, Converter={StaticResource ets}}" Grid.ColumnSpan="2" />      
 <!-- ets = EntryToStringConverter -->  

  <uc:DirectoryTree Grid.Column="0" Grid.Row="1" x:Name="dirTree"  >
     <uc:DirectoryTree.SelectedDirectory>
        <uc:Ex />
     </uc:DirectoryTree.SelectedDirectory>
 </uc:DirectoryTree>

 <uc:FileList Grid.Column="1" Grid.Row="1" ViewMode="vmIcon"
 CurrentDirectory="{Binding SelectedDirectory, ElementName=dirTree, Mode=TwoWay}" />

DirectoryInfoEx (Component)

Please keep in mind that the UserControls associated with this article is based on System.IO.DirectoryInfoEx, not System.IO.DirectoryInfo, it's a custom dotNet2.0 component which uses IShellFolder2 to list shell items, and provide similar syntax as DirectoryInfo, the current version (0.17) does the following :
  • List Shell Items (sync/async)
  • File System Operations (sync/async), e.g. Create, Copy, Move , Delete, Rename
  • Display Shell Context Menu, allow Insert/Delete/Disable Menu Items
  • Monitor File System Changes
  • Obtain File Properties from Shell
The DirectoryInfoEx on this page may not be the most update, please check it's page for update.

DirectoryTree (TreeView)

  • DirectoryTree
    • RootDirectory
    • SelectedDirectory
FileList

You can set the RootDirectory and SelectedDirectory using the ExExtension. Ex is a MarkupExtension, which return the
appropriate FileSystemInfoEx, one can specify the FullName via the extension as well e.g. :
<uc:DirectoryTree Grid.Column="0" Grid.Row="1" x:Name="dirTree" >
 <uc:DirectoryTree.RootDirectory>
 <uc:Ex FullName="::{20D04FE0-3AEA-1069-A2D8-08002B30309D}" /> 
 <!--MyComputer-->
 </uc:DirectoryTree.RootDirectory>
 <uc:DirectoryTree.SelectedDirectory>
 <uc:Ex FullName="C:\Temp" />
 </uc:DirectoryTree.SelectedDirectory> 
</uc:DirectoryTree> 

FileList (ListView)

  • FileList
    • SelectedEntries
    • CurrentDirectory
    • ViewMode
    • ViewSize
    • SortBy (0.2)
    • SortDirection (0.2)
Unlike normal ListView, this FileList support multi-select by dragging, for more information you can take a look to this article.
You can get the highlight count when the FileList is dragging, using the an attached property named SelectionHelper.HighlightCount, e.g. : (Statusbar not included in this publish)
<uc:Statusbar Grid.Column="0" Grid.Row="4" Grid.ColumnSpan="3" x:Name="sbar"
 FileCount="{Binding RootModel.CurrentDirectoryModel.FileCount, ElementName=fileList1}"
 DirectoryCount="{Binding RootModel.CurrentDirectoryModel.DirectoryCount, 
 ElementName=fileList1}"
 HighlightCount="{Binding Path=(uc:SelectionHelper.HighlightCount), 
 ElementName=fileList1}"
 SelectedEntries="{Binding SelectedEntries, ElementName=fileList1}" />

SelectedEntries is oneway currently, it can return the items selected on the file list, so you can bind it with StatusBar or other UserControl.

(0.2) You can change the item sequence using SortBy and SortDirection property, or it can be changed by double-click the header of GridView :

<uc:FileList SortBy="sortByLastWriteTime" SortDirection="Descending" />

ViewMode and ViewSize both represent how the file list represent the items, the modes and it's size as follows.

LargeIconView

<uc:FileList ViewMode="vmLargeIcon" ViewSize="80" /> 
public enum ViewMode : int
{
 vmTile = 13,
 vmGrid = 14,
 vmList = 15,
 vmSmallIcon = 16,
 vmIcon = 48,
 vmLargeIcon = 80,
 vmExtraLargeIcon = 120
} 
If one set the ViewSize to any value between 13 to 120, the filelist will apply the view automatically.
Noted that TileView is not implemented yet.


The Design

The UserControls are developed using Model-View-ViewModel approach (MVVM), I first learned about this approach from Dan Crevier's Blog, but now you can find a simplified explanation here. Using MVVM improve the application responsivenss, as most work can be threaded (and able to update back to UI).

My Implementation included the followings :
  • Model
    • Represent a DirectoryInfoEx object
  • ViewModel - 2 kinds
    • Model of a DirectoryTree / FileList
    • Model of a DirectoryTreeItem / FileListItem / FileListCurrentDirectory (which embedded a Model)
  • View
    • The DirectoryTree / FileList itself, no code-behind except DependencyProperties and some EventHandlers

These UserControls uses Sacha Barber's Cinch MVVM framework. I am not sure if it's the best framework I can get, as I haven't tested many of them, but it reduce a lot of my work load. My only complaints is that his article is lack of a central index, every time I wish to lookup for a description for a class I have to look through 5 articles.

Unlike most MVVM projects these are UserControls instead of Windows, I have to write a number of DependencyProperties to interface between the UserControls, and because of this, performance may suffer, but I found it simplier to divide a large projects into smaller managable pieces.


Model

  • Cinch.ValidatingObjet
    • Cinch.EditableValidatingObjet
    • ExModel
      • FileModel
      • DirectoryModel
      • DriveModel (0.3)
FileSystemInfoEx properties rarely change. so actually FileSystemInfoEx itself can be a model, but to support rename I added another layer : ExModel.

Cinch contains two classes for implementing Model, ValidatingObject and it's derived class named EditableValidatingObject, the difference is that EditableValidatingObject have transaction support, using BeginEdit() / CancelEdit() and EndEdit() methods. To support this, your properties have to be implemented as DataWrapper class.

Because the only one field is required to support rename, I implement ExModel as ValidatingObject instead of EditableValidatingObject.

ExModel (abstract)

ExModel contains a FileSystemInfoEx entry (accessable using EmbeddedEntry property).
It is an abstract class, use FileModel and DirectoryModel and DriveModel(0.3) which is inherited from ExModel instead.

Name is one of it's important property to support rename :
static PropertyChangedEventArgs nameChangeArgs =
 ObservableHelper.CreateArgs<ExModel>(x => x.Name);
public string Name
{
 get { return _name; }
 set
 {
 if (!String.IsNullOrEmpty(_name) && _name != value)
 {
 string newName = PathEx.Combine(PathEx.GetDirectoryName(FullName), value);
 FileSystemInfoEx entry = EmbeddedEntry;
 string origName = _name;
 string origFullName = _fullName;
 _name = value;
 _fullName = newName;

 try
 {
 IOTools.Rename(entry.FullName, PathEx.GetFileName(_fullName));
 FullName = newName;
 Label = EmbeddedEntry.Label;
 }
 catch (Exception ex)
 {
 MessageBox.Show(ex.Message, "Rename failed");
 _name = origName;
 _fullName = origFullName;
 return;
 }
 }
 else _name = value; 
 
 NotifyPropertyChanged(nameChangeArgs);
 }
}
As you see, it will revert back to original if the rename process failed, otherwise update the internal _name field and call NotifyPropertyChanged() method so the UI is notified about the changes.


ViewModel

  • Cinch.ViewModelBase
    • ExViewModel
      • DirectoryViewModel
        • CurrentDirectoryViewModel (FileList)
      • HierarchyViewModel
        • DirectoryTreeViewItemViewModel (TreeViewItem)
      • FileListItemViewModel (ListViewItem)
    • RootModelBase
      • DirectoryTreeViewModel (DirectoryTree which is a TreeView)
      • FileListViewModel (FileList which is a ListView)
All my ViewModels are inherited from Cinch.ViewModelBase. ViewModelBase, as a ViewModel communicating with View, contains some Window life-cycle related commands and method (e.g. CloseCommand and OnWindowClose() method), but as the file explorer components are not Window most of them are not used. Instead, I shall develop my own set of commands, like RefreshCommand for FileList. There are a number of unimplemented commands that I can think of, like Cut/CopyPaste/SelectAllCommand for FileList, if you need them immediately you may need to write them yourself. e.g.

private SimpleCommand _refreshCommand = new SimpleCommand
 {
 CanExecuteDelegate = x => true,
 ExecuteDelegate = x => Refresh()
 };
public SimpleCommand RefreshCommand { get { return _refreshCommand; } }

Then the command is usable by binding from the UI, e.g. :
<anotherControl RefreshCommand="{Binding RootModel.RefreshCommand, 
 ElementName=fileList1}" /> 

ExViewModel (abstract)



ExViewModel are a base class for all List/TreeViewItem, it contains a FileModel or DirectoryModel in it, which can be accessed by a readonly property named EmbeddedModel. Each ExViewModel is for representing one FileInfoEx or DirectoryInfoEx only, to represent another FileSystemInfoEx entry one would have to create another ExModel and ExViewModel.
Similarly, DirectoryViewModel and HierarchyViewModel have EmbeddedDirectoryModel. So, if one want to access the contained Directory, one can use EmbeddedDirectoryModel.EmbeddedDirectory property.

RootModelBase (abstract)



RootModelBase is a base class for DirectoryTreeViewModel and FileListViewModel, which is the ViewModel of the whole UserControl, it contains an event named OnProgress, which is listened by the UserControl. When UserConstrol receive this event, it will raise again as DependencyEvent, which will bubble up until asked to stop (args.Handled = true;).
public static readonly RoutedEvent ProgressEvent = ProgressRoutedEventArgs.ProgressEvent.AddOwner(typeof(DirectoryTree));
...
RootModel.OnProgress += (ProgressEventHandler)delegate(object sender, ProgressEventArgs e)
{
 this.Dispatcher.BeginInvoke(DispatcherPriority.ApplicationIdle, new ThreadStart(delegate
 {
 RaiseEvent(new ProgressRoutedEventArgs(ProgressEvent, e));
 }));
};
I am not sure if this will cause memory leak, e.g. not GC when the UserControl is free. If so, I will have to implement WeakEvent.

FileList - FileListViewModel, CurrentDirectoryViewModel and FileListItemViewModel

Actually both ViewModel should be combined if FileListViewModel not inherited from RootModelBase, and CurrentDirectoryViewModel is not inherited from ExViewModel, but as they do, I have to leave them as two separate class. One FileList can have one FileListViewModel only, but it may have multiple CurrentDirectoryViewModels (when changing directory).

FileListViewModel (RootModelBase)

  • RootModelBase
    • FileListViewModel
      • RefreshCommand (which calls CurrentDirectoryModel.Refresh())
      • CurrentDirectory
      • CurrentDirectoryModel
      • IsLoading
      • SortBy (0.2)
      • SortDirection (0.2)
FileListViewModelis a middle layer between CurrentDirectoryModel (which changes regularly) and the FileList. In FileListViewModel, CurrentDirectory and CurrentDirectoryModel links to each other, if you change one of them it will change the another, the reason to have both properties is that, CurrentDirectory is for outside the FileList (e.g. from DirectoryTree, or user code), CurrentDirectoryModel is for internal use as ViewModel.

(0.2) SortBy and SortDirection enable sort by calling CurrentDirectoryModel.ChangeSortMethod() method, which change the sort to CustomSort :
public void ChangeSortMethod(ExComparer.SortCriteria sortBy, ListSortDirection sortDirection)
{ 
 ListCollectionView dataView = (ListCollectionView)(CollectionViewSource.GetDefaultView(_subEntries.View));

 dataView.SortDescriptions.Clear(); //Disable previous sorting method.
 dataView.CustomSort = null;

 //conversion from ListSortDirection to ExComparer.SortDirection 
 //ExComparer cannot use ListSortDirection as it's .Net2.0 component
 ExComparer.SortDirectionType direction = sortDirection == ListSortDirection.Ascending ? 
 ExComparer.SortDirectionType.sortAssending : ExComparer.SortDirectionType.sortDescending;
 
 dataView.CustomSort = new ExModelComparer(sortBy, direction); //IComparer
}
In this case, although the CollectionViewSource discussed in CurrentDirectoryViewModel is still used, it's sorting method is overrided by my own implementation.

Most ViewModel has IsLoading property, which will be later bound to UI, so when it's true, the UI shows loading animation.

CurrentDirectoryViewModel (ExViewModel)

  • ExViewModel
    • DirectoryViewModel
      • CurrentDirectoryModel
        • RefreshCommand
        • ListFiles, ListDirectories
        • IsLoading
        • Filter
        • BgWorker (bgWorker_LoadSubEntries and bgWorker_FilterSubEntries)
        • FileCount, DirectoryCount
        • HasSubEntries
        • SubEntries, SubEntriesInternal
CurrentDirectoryViewModelresponsible for listing the contents of current directory, to do this, it contains a Cinch.BackgroundTaskManager named bgWorker_LoadSubEntries, BackgroundTaskManager as it's name implies, it allows running a task in background, using the RunBackgroundTask() method, the advantage of using this class is that you can specify the task and how to update back to ViewModel in one place.
bgWorker_LoadSubEntries = new BackgroundTaskManager<List<FileListViewItemViewModel>>(
() =>
{
 IsLoading = true;
 return getEntries();
},
(result) =>
{
 updateSubEntries(result);                  
 IsLoading = false;
});    
The first section is TaskFunc (Func<FileListViewItemViewModel>), which does the time-consuming work, and the second section is CompleteAction (Action<FileListViewItemViewModel>), which update the UI (run in UI thread). Noted that only the first section is run in background.

The fiest section return the result of getEntries() methods, getEntries() is a method with a linq command, the _cachedSubEntries is used again when Filter needed.
private List<FileListViewItemViewModel> getEntries()
{
 var retVal = from entry in EmbeddedDirectoryModel.EmbeddedDirectoryEntry.EnumerateFileSystemInfos()
 where (entry is IDirectoryInfoExA && ListDirectories) || (entry is IFileInfoExA && ListFiles)
 select new FileListViewItemViewModel(_rootModel, ExAModel.FromExAEntry(entry)); ;
 _cachedSubEntries = retVal.ToArray();
 return new List<FileListViewItemViewModel>(_cachedSubEntries);
} 
Action<List<FileListViewItemViewModel>> updateSubEntries =
(result) =>
{
 List<FileListViewItemViewModel> delList = new List<FileListViewItemViewModel>(SubEntriesInternal.ToArray());
  List<FileListViewItemViewModel> addList = new List<FileListViewItemViewModel>();

 foreach (FileListViewItemViewModel model in result)
   if (delList.Contains(model))
 delList.Remove(model);
 else addList.Add(model);
 
 foreach (FileListViewItemViewModel model in delList)
 SubEntriesInternal.Remove(model); 
 foreach (FileListViewItemViewModel model in addList)
 SubEntriesInternal.Add(model);

  DirectoryCount = (uint)(from model in SubEntriesInternal where
 model.EmbeddedModel is DirectoryModel select model).Count();
  FileCount = (uint)(SubEntriesInternal.Count - DirectoryCount);
 HasSubEntries = SubEntriesInternal.Count > 0;              
}; 
Basically the second section is to identify the difference of result and the output (SubEntriesInternal), and changes the ObservableCollection. Instead of replacing the ObservableCollection completely, this reduce the overhead needed to destroy and create of ListViewItem in the UI side, and most importantly, it allow the FileList to maintain the selection as long as possible.


FileList support Filter, when the property is changed, bgWorker_FilterSubEntries will be run in background, and change the SubEntriesInternal when completed, it's similar as bgWorker_LoadSubEntries, except it uses filterEntries() method (as below)
public List<FileListViewItemViewModel> filterEntries()
{
 if (_cachedSubEntries == null)
 _cachedSubEntries = getEntries().ToArray();
 var retVal = from entry in _cachedSubEntries where (String.IsNullOrEmpty(Filter) ||
 IOTools.MatchFileMask(entry.Name, Filter + "*"))
 select entry;
 return new List<FileListViewItemViewModel>(retVal);
} 

So why it's called SubEntriesInternal instead of SubEntries? It's because there's another layer, SubEntries is a CollectionViewSource, which can group or sort the SubEntriesInternal without changing it (e.g. if I want to sort SubEntriesInternal directly I will have to do a lot of work, Deleting and Inserting, you cant call Array.Sort() method on ObservableCollection), SubEntriesInternal is sorted using it's IsDirectory property, then it's FullName property.
_subEntries = new CollectionViewSource();           
_subEntries.Source = SubEntriesInternal;
_subEntries.SortDescriptions.Add(new SortDescription("IsDirectory", ListSortDirection.Descending));
_subEntries.SortDescriptions.Add(new SortDescription("FullName", ListSortDirection.Ascending));
Because it's CollectionViewSource, when Binding ItemsSource, one have to bind it's SubEntries.View property insteading of the SubEntries property directly. (btw, you can still bind to SubEntries.Source as well)


Lastly, there's a FileSystemWatcherEx, which refresh the FileList when there's a change :
FileSystemWatcherEx watcher = new FileSystemWatcherEx(model.EmbeddedDirectoryEntry); 
var handler = (FileSystemEventHandlerEx)delegate(object sender, FileSystemEventArgsEx args)
 {
 if (args.FullPath.Equals(model.FullName))
 Refresh();
 };
var renameHandler = (RenameEventHandlerEx)delegate(object sender, RenameEventArgsEx args)
 {
 if (args.OldFullPath.Equals(model.FullName))
 Refresh();
 };
watcher.OnChanged += handler;
watcher.OnCreated += handler;
watcher.OnDeleted += handler;
watcher.OnRenamed += renameHandler;

FileListItemViewModel (ExViewModel)

  • ExViewModel
    • FileListItemViewModel
      • ExpandCommand
      • IsSelected
ExpandCommand run the selected item (via Process.Start() method) or change the current directory (via _rootModel.CurrentDirectory) depend what is selected. If the selected file is a link (*.lnk) it uses VBaccelerator's ShellLink to find the linked item, then run the file or change directory based on the linked item.
if (PathEx.GetExtension(entry.Name).ToLower() == ".lnk")
 using (ShellLink sl = new ShellLink(entry.FullName))
 {
 string linkPath = sl.Target;
 if (DirectoryEx.Exists(linkPath) && sl.Arguments == "")
 _rootModel.CurrentDirectory = FileSystemInfoEx.FromString(linkPath) as DirectoryInfoEx;
 else Run(linkPath, sl.Arguments);
 }
But how to hook the ExpandCommand? ListViewItem itself does not have DoubleClickCommand, you can :

1. either monitor the MouseDoubleClickEvent and do the action when raised :
#region ExpandHandler
this.AddHandler(ListViewItem.MouseDoubleClickEvent, (RoutedEventHandler)delegate(object sender, RoutedEventArgs e)
{
 DependencyObject lvItem = getListViewItem(e.OriginalSource as DependencyObject);
 if (lvItem != null)
 {
 FileListViewItemViewModel model =
 (FileListViewItemViewModel)ItemContainerGenerator.ItemFromContainer(lvItem);
 if (model != null) 
 model.Expand(); 
 }
});
#endregion

2. or you can create the DoubleClickCommand your own, and hook them together (more elegant)
<Style x:Key="{x:Type ListViewItem}" TargetType="{x:Type ListViewItem}" >
 <Setter Property="uc:CommandProvider.DoubleClickCommand" Value="{Binding ExpandCommand}" />
</Style>
CommandProvider is a FrameworkElement which have an attached property named DoubleClickCommand, and when it's set, CommandProvider will hook to the control's MouseLeftButtonDown event, in this case, the ListViewItem. It will invoke the command when it receive a double click (clickcount = 2) event.

In normal case it should be called this way :
<Label uc:CommandProvider.DoubleClickCommand="{Binding ExpandCommand}" /> 
Beside DoubleClickCommand, it also included other commands like Click, RightClick, EnterPress, Prev and TreeViewSelectionChanged as well.


IsSelected is binded with ListViewItem.IsSelected, it have no use currently, but it can be used by the FileListViewModel to change the item's selected state.
It's intended for allowing FileList to setting the selected items externally (just like DirectoryTree).

DirectoryTree - DirectoryTreeViewModel and DirectoryTreeItemViewModel

TreeView can be interpreted as multi-level ListView, that means unlike WindowsForms you cannot get a TreeViewItem by using listView1.Items[0].Items[1], you cannot use TreeView's ItemsContainerGenerator to get the ListViewItem either, because each level has it's own ItemsContainerGenerator, you have to use ListViewItem.Parent's ItemsContainerGenerator..

HierarchyViewModel (ExViewModel)

  • Cinch.ViewModelBase
    • ExViewModel
      • HierarchyViewModel
        • IsExpanded
        • IsSelected
One of the basic functionality of the DirectoryTree is to display the selected directory properly, in WPF, it's easy to obtain the selected value in TreeView, but changing it is another story. In earlier version I uses DaWanderer's method which navigate through the TreeViewItem layer by layer to find the required item, and change it's IsSelected property, but this hog the UIThread, and this disallow Virtualizing TreeView to be used, because if the item is not shown, it's not created, and if it's not created, it cannot be selected. This method is used when I develop the control using MVP (Model-View-Presenter) pattern.

Until one day I read Josh Smith's article mentioned how to support selection in MVVM, the problem solved itself. Basically it's to have IsExpanded and IsSelected in the ViewModel (in my case, HierarchyViewModel), and bind the TreeViewItem's IsExpanded and IsSelected to it. The issue of MVP pattern is the lack of another layer (ViewModel) i.e. I cannot add IsExpanded/IsSelected to FileSystemInfoEx.

<Style x:Key="{x:Type local:DirectoryTree}" TargetType="{x:Type local:DirectoryTree}" 
 BasedOn="{StaticResource {x:Type TreeView}}">
  <Setter Property="ItemTemplate" Value="{StaticResource TreeItemTemplate}" />
 <Setter Property="ItemContainerStyle">
     <Setter.Value>
        <Style TargetType="{x:Type TreeViewItem}">
 <Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}" />
 <Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" />
 <Setter Property="FontWeight" Value="Normal" />
 </Setter.Value>
 </Setter>
</Style> 
So if you change the IsExpanded/IsSelected field in the ViewModel, it will affect the ListViewItem.

DirectoryTreeViewModel (RootModelBase)

  • RootModelBase
    • DirectoryTreeViewModel
      • BgWorker_FindChild
      • RootDirectoryModelList
      • RootDirectory
      • SelectedDirectoryModel
      • SelectedDirectory
      • IsLoading

Similar to FileList, there are linked property : SelectedDirectory and SelectedDirectoryModel property, so changing one will change the other as well. The another pair is RootDirectory and RootDirectoryModelList, except it's a List, because TreeView.Items property accept a List only. DirectoryTree also have a FileSystemWatcherEx to refresh the tree when needed.

One important feature is when SelectedDirectory is set externally, it will update the Selected Item in DirectoryTree, this is done in background using the BgWorker_FindChild (which is a Cinch.BackgroundTaskManager), as follows :

bgWorker_findChild = new BackgroundTaskManager<DirectoryTreeItemViewModel>(
() =>
{
 IsLoading = true;
 DirectoryTreeItemViewModel lookingUpModel = _selectedDirectoryModel;
 Func<bool> cancelNow = () => //Stop search when SelectedDirectory changed AGAIN.
 {
 bool cont = lookingUpModel != null && lookingUpModel.Equals(_selectedDirectoryModel); 
 return !cont;
 };
 DirectoryTreeItemViewModel newSelectedModel = null;
 {
 DirectoryInfoEx newSelectedDir = _selectedDirectory;
 if (newSelectedDir != null)
 {
 foreach (DirectoryTreeItemViewModel rootModel in _rootDirectoryModelList)
 {
 newSelectedModel = rootModel.LookupChild(newSelectedDir, cancelNow);
 if (newSelectedModel != null)
 return newSelectedModel;
 }
  }
 }
 return _rootDirectoryModelList.Count == 0 ? null : _rootDirectoryModelList[0];
},
(result) =>
{
 if (result != null) 
 {  
 if (result.Equals(_selectedDirectoryModel))
 result.IsSelected = true;  
 //This will trigger TreeViewItem.IsSelected, see HierarchyViewModel above                      
 }
 IsLoading = false;
});
DirectoryTreeItemViewModel.LookupChild() method return the lookup DirectoryTreeItemViewModel, or it's parent's model if it cannot be found (which is rare). It will expand if it's not already expanded, because the expand is not run in UIThread it wont make your application freeze.

The cancelCheck will cancel the iteration if the SelectedDirectory is changed again when the lookup is working, it's pointless to continue because another BgWorker_FindChild is already started.
public DirectoryTreeItemViewModel LookupChild(DirectoryInfoEx directory, Func<bool> cancelCheck)
{ 
 if (cancelCheck != null && cancelCheck())
 return null;
 if (Parent != null)
 Parent.IsExpanded = true;
 if (directory == null)
 return null;
 
 if (!IsLoaded) //If SubEntries not loaded, load it
 {
 IsLoading = true;
 SubDirectories = getDirectories();
 HasSubDirectories = SubDirectories.Count > 0;
 IsLoading = false;
 }

 foreach (DirectoryTreeItemViewModel subDirModel in SubDirectories)
 {
 if (!subDirModel.Equals(dummyNode)) //dummyNode is added if not loaded and HasSubDirectories
 {
 DirectoryInfoEx subDir = subDirModel.EmbeddedDirectoryModel.EmbeddedDirectoryEntry;
 //ViewModel.Model.ExEntry
 if (directory.Equals(subDir))
 return subDirModel;
 else if (IOTools.HasParent(directory, subDir))
 return subDirModel.LookupChild(directory, cancelCheck);
 }
 }
 return null;
}

DirectoryTreeItemViewModel (HierarchyViewModel)

  • ExViewModel
    • HierarchyViewModel
      • DirectoryTreeItemViewModel
        • RefreshCommand
        • SubDirectories
        • HasSubDirectories
        • IsLoading
        • bgWorker_loadSub

DirectoryTreeItemViewModel contains a Cinch.BackgroundTaskManager (bgWorker_loadSub), It's similar to CurrentDirectoryViewModel except it loads subdirectory only instead of both subdirectory and files.

To make the application even more responsive some developer may want to list the sub-items asynchronously, it's now possible as DirectoryInfoEx 0.17 included DirectoryInfoEx.EnumerateDirectories() method (as well as EnumerateFiles() and EnumerateFileSystemInfos() methods), which return a IEnumerable instead of a List, like the method with same name in DirectoryInfo in .Net 4.0, , In this case you may use a Linq query instead of Cinch.BackgroundTaskManager. I havent implement this yet.

Like FileList, DirectoryTree also contains a FileSystemWatcherEx which montior the Desktop directory and it's sub-folders (instead of creating a watcher for each directory). When there's a change, it will call RootDirectoryModelList[0]'s BroadcastChange() method, which will iterate through all created folders.
internal void BroadcastChange(string parseName, WatcherChangeTypesEx changeType)
{
 if (IsLoaded) //If SubDirectories loaded
 foreach (DirectoryTreeItemViewModel subItem in SubDirectories)
 subItem.BroadcastChange(parseName, changeType);

 switch (changeType)
 {
 case WatcherChangeTypesEx.Created:
 case WatcherChangeTypesEx.Deleted:
 if (EmbeddedDirectoryModel.FullName.Equals(PathEx.GetDirectoryName(parseName)))
 Refresh();
 break;
 default:
 if (EmbeddedDirectoryModel.FullName.Equals(parseName))
 Refresh();
 break;
 }
}


View



Most of the View I created are inherited from UserControl, but not FileList and DirectoryTree. The major reason is that these class exposed a lot of properties thats required by other class, if I inherit from UserControl I will have to re-write many DependencyProperties. The another reason is that I have a DragDropHelper which have to be plugged to TreeView/ListView only.

Both FileList and DirectoryTree have some code-behind, as it's easier to code that way. To make it easier to be edit by Expression, true MVVM project shouldnt have any View, and MVVM components should be connected via ViewModel instead of Dependency properties.

DragDropHelper work with DirectoryInfoEx related controls only, you can enable the support by adding a few lines in xaml :
<ListView x:Class="QuickZip.IO.PIDL.UserControls.FileList"
 xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
 xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
 xmlns:uc="http://www.quickzip.org/UserControls"
 xmlns:local="clr-namespace:QuickZip.IO.PIDL.UserControls"
 SelectionMode="Extended" 
 VirtualizingStackPanel.IsVirtualizing="True" //Virtual File List support.
 VirtualizingStackPanel.VirtualizationMode="Recycling" 
 uc:SelectionHelper.EnableSelection="True" //Enable Multi-Select by dragging.
 local:DragDropHelperEx.EnableDrag="True" //Hook drag related events (drag-FROM file list)
 local:DragDropHelperEx.EnableDrop="True" //Hook drop related events (drop-TO file list)
 local:DragDropHelperEx.ConfirmDrop="False" //Display an ugly confirm dialog when drop
 local:DragDropHelperEx.CurrentDirectory=
 "{Binding RootModel.CurrentDirectory, RelativeSource={RelativeSource self}}" 
 //Where do the files DROPPED to?
 local:DragDropHelperEx.Converter=
 "{Binding ModelToExConverter, RelativeSource={RelativeSource self}}"
 //Connected to FileList.ModelToExConverter,
 //which convert ExAViewModel to FileSystemInfoEx
 />

OT: How to create custom control in ClassLibrary?

The FileList and DirectoryTree class are created using UserControl template (Add...\User Control), then rename the UserControl to TreeView/ListView. This will generate both .cs and .xaml file.

The another way is to
  • Create a new Class (Add \ Class),
  • Change it so it's inherited from TreeView/ListView.
  • If you want to style it, Add a Style in Themes\Generic.xaml,
    <Style x:Key="{x:Type local:YourControl}" TargetType="{x:Type local:YourControl}" 
     BasedOn="{x:Type local:ListViewBase}">
     <Setter Property="Template">
     <Setter.Value>
     <ControlTemplate TargetType="{x:Type local:YourControl}">
     <ItemsPresenter />
     </ControlTemplate>
     </Setter.Value>
     </Setter>
     </Style>
    
    If you need a specific ControlTemplate, lets say ListView, just google "ControlTemplate ListView" and you can find the template from msdn.
  • Update AssemblyInfo.cs, add the following : (which make it load your generic.xaml, you only have to do it once per Class Library)
    [assembly: ThemeInfo(
     ResourceDictionaryLocation.ExternalAssembly, //where theme specific resource dictionaries are located
     //(used if a resource is not found in the page, 
     // or application resource dictionaries)
     ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
     //(used if a resource is not found in the page, 
     // app, or any theme specific resource dictionaries)
    )] 
    
  • You have to override the default style too :
    static YourControl()
    {
     DefaultStyleKeyProperty.OverrideMetadata(typeof(YourControl), new FrameworkPropertyMetadata(typeof(YourControl)));
    }
    
    protected override DependencyObject GetContainerForItemOverride()
    {
     return new YourControlItem(); 
    }
    

Common in DirectoryTree/FileList

  • DirectoryTree/FileList
    • RootModel
    • ProgressEvent
    • IsEditing (Attached property)
RootModel is the top level RootModelBase (which is ViewModel), which provide properties for the UserControls to bind. As it's MVVM RootModel is also the DataContext, eg:
DataContext = RootModel = new FileListViewModel(); 
Bcause the ViewModel is DataContext, binding them become very easy :
<Style x:Key="{x:Type local:FileList}" TargetType="{x:Type local:FileList}" 
 BasedOn="{StaticResource {x:Type ListBox}}" >
 <Setter Property="ItemsSource" 
 Value="{Binding CurrentDirectoryModel.SubEntries.View}" />
 <Setter Property="View" Value="{StaticResource GridView}" /> 
</Style>
ListView.ItemsSource is bound to RootModel.CurrentDirectoryModel.SubEntries.View. (remember, it's CollectionViewSource)


ContextMenu handling is done in UserControl level, DirectoryInfoEx has a static class named ContextMenuWrapper, which can generate the shell context menu for specified entries, all you need is to provide the entries (in FileSystemInfoEx) and the coordinate (in System.Drawing.Point):
_cmw = new ContextMenuWrapper();
this.AddHandler(TreeViewItem.MouseRightButtonUpEvent, new MouseButtonEventHandler(
 (MouseButtonEventHandler)delegate(object sender, MouseButtonEventArgs args)
 {
 if (SelectedValue is FileListViewItemViewModel)
 {
 var selectedItems = (from FileListViewItemViewModel model in SelectedItems
 select model.EmbeddedModel.EmbeddedEntry).ToArray();
 Point pt = this.PointToScreen(args.GetPosition(this));

 string command = _cmw.Popup(selectedItems, new System.Drawing.Point((int)pt.X, (int)pt.Y));
 switch (command)
 {
 case "rename":
 if (this.SelectedValue != null)
 SetIsEditing(ItemContainerGenerator.ContainerFromItem(this.SelectedValue), true);
 break;
 case "refresh":
 RootModel.CurrentDirectoryModel.Refresh();
 break;
 }
 }
})); 

ProgressEvent is ProgressRoutedEventArgs.ProgressEvent, which is a RoutedEvent, it is raised if RootModel.OnProgress event is raised by the ViewModel.

IsEditing is an attached property, when rename is issued (from the Shell Context Menu) in FileList, it will set IsEditing of the specified ListViewItem to true. On the another side, the ListViewItem.IsEditing is bound to it's enclosed EditBox.

The following sample is taken from DirectoryTree, as it looks simplier :

<HierarchicalDataTemplate x:Key="TreeItemTemplate" DataType="{x:Type vm:DirectoryTreeItemViewModel}" ItemsSource="{Binding SubDirectories}">
 <StackPanel Orientation="Horizontal" x:Name="itemRoot"> 
 <Image x:Name="img" Source="{Binding Converter={StaticResource amti}}" Width="16"/>
 <uc:EditBox x:Name="eb" Margin="5,0" DisplayValue="{Binding EmbeddedModel.Label}" 
 ActualValue="{Binding EmbeddedModel.Name, Mode=TwoWay}" 
 IsEditable="{Binding EmbeddedModel.IsEditable}"
 IsEditing="{Binding Path=(local:DirectoryTree.IsEditing),
 RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type TreeViewItem}}, Mode=TwoWay}"
 />
 <Grid Width="50" Margin="15, 5, 0, 5" Visibility="{Binding IsLoading, Converter={StaticResource btv}}">
 <ProgressBar IsIndeterminate="True" />
 <TextBlock Text="Loading" FontSize="6" TextAlignment="Center" />
 </Grid>
 </StackPanel>
 <!-- ... -->
</HierarchicalDataTemplate>

EditBox is a replacement of TextBox + Label, which display the Label when not IsEditing, and TextBox (EditBoxAdorner) when IsEditing, it's a technique learned from ATC Avalon Team, but the EditBox used in this project is a rewrite one, with the following changes :

  • No longer bound to ListView

  • Has two value instead of one,

    • DisplayValue (display on the label) and

    • ActualValue (for editing),

    which is required because a FileSystemInfoEx item's label and name may be different.

The converter (amti) is ExModelToIconConverter which, similar to FileNameToIconConverter, is a IValueConverter can convert ExModel/ExViewModel to ImageSource.

FileList (ListView)

  • FileList
    • SelectedEntries
    • CurrentDirectory
    • ViewMode
    • ViewSize
    • IsLoading
    • RefreshCommand
    • View
    • FileListLookupBoxAdorner
Most of the properties here are explained in the ViewModel, except these are DependencyProperties, those are bound to the RootModel's related properties.



FileList can have different Views, a View can define ListView's Orientation, ItemContainerStyle, ItemTemplate, HorizontalContentAlignment and whatever Listview's properties in one place, if you look at VirtualWrapPanelView.cs you can find the following :
public class VirutalWrapPanelView : ViewBase
{
 public static readonly DependencyProperty ItemContainerStyleProperty =
 ItemsControl.ItemContainerStyleProperty.AddOwner(typeof(VirutalWrapPanelView));
 public Style ItemContainerStyle
 {
 get { return (Style)GetValue(ItemContainerStyleProperty); }
 set { SetValue(ItemContainerStyleProperty, value); }
 }
}
then in VirtualWrapPanelView.xaml, you can find the property is bound to the ListView
<Style x:Key="{ComponentResourceKey TypeInTargetAssembly={x:Type uc:VirutalWrapPanelView}, 
 ResourceId=virtualWrapPanelViewDSK}" 
 TargetType="{x:Type ListView}" BasedOn="{StaticResource {x:Type ListBox}}">
 ...
 <Setter Property="ItemContainerStyle" 
 Value="{Binding (ListView.View).ItemContainerStyle,
 RelativeSource={RelativeSource Self}}"/>
 ....
</Style>
To support multiple Views, It's a good idea to construct View instead of setting each properties individually.

There are 7 ViewModes, so you may think there are 6 Views in FileList (and TileView is not implemented implemented in 0.3), actually there are just VirtualWrapPanelView and GridView. There are only 3 different Views derived from VirtualWrapPanelView, which is SmallIconView, ListView and IconView. The following is IconView, which is used by 3 similar ViewModes (vmIcon, vmLargeIcon and vmExtraLargeIcon).
<uc:VirutalWrapPanelView x:Key="IconView" ItemHeight="..." ItemWidth="..." HorizontalContentAlignment="Left" > 
 <uc:VirutalWrapPanelView.ItemTemplate>
 <DataTemplate> 
 <StackPanel Orientation="Vertical">
 <Image x:Name="img" HorizontalAlignment="Center" Stretch="Fill" Source="..."
 Height="{Binding RelativeSource={RelativeSource AncestorType=local:FileList}, Path=ViewSize}" 
 Width="{Binding RelativeSource={RelativeSource AncestorType=local:FileList}, Path=ViewSize}" /> 
 <uc:EditBox x:Name="eb" Margin="5,0" ... />
 </StackPanel>
 </DataTemplate>
 </uc:VirutalWrapPanelView.ItemTemplate>
</uc:VirutalWrapPanelView>
Once you completed the View, changing ViewMode is just one line of code:
 this.View = (ViewBase)(this.TryFindResource(IconView)); 

When you use the FileList, remember leave some space below the control (not necessary to be empty), because thats where the FileListLookupAdorner placed in. FileListLookupAdorner is used to filter listed element by name, remember CurrentDirectoryViewModel has a property named Filter which use runs a Cinch.BackgroundTaskManager to filter the data. FileListLookupAdorner can be bring up by pressing any key in file list.

It's harder to make the control as adorner, as I have to write the code in cs instead of xaml, the advantage is that
  • it's separate from your main control, so you can reuse it in other controls you made,
  • as all the unrelated logic code, like close the adorner when user press the x button, is placed on the FileListLookupAdorner.cs instead of the FileList.cs.
  • the FileListLookupAdorner is shown on the AdornerLayer, which usually is the topmost, and it wont interfere other parts of the main control.
I originally wanted to bind them together, but not sure why it's not working, so I monitor the changes and update when the text of the adorner is changed.
DependencyPropertyDescriptor descriptor = DependencyPropertyDescriptor.FromProperty
 (FileListLookupBoxAdorner.TextProperty, typeof(FileListLookupBoxAdorner));
descriptor.AddValueChanged
 (_lookupAdorner, new EventHandler(delegate { 
 RootModel.CurrentDirectoryModel.Filter = _lookupAdorner.Text; }));

DirectoryTree (TreeView)

  • DirectoryTree
    • AutoCollapse (0.4)
    • RootDirectory
    • SelectedDirectory
    • SelectedDirectoryPath
    • LoadingAdorner



LoadingAdorner is shown when RootModel's BgWorker_FindChild (see DirectoryTreeViewModel) is working (or IsLoading equals to true).

The DataTemplate of DirectoryTree is HierarchicalDataTemplate, which is a DataTemplate that allow you to set the ItemsSource (or subItems). The complete template can be found above in "Common in DirectoryTree/FileList" section,
<HierarchicalDataTemplate ...>
 <Grid Width="50" Margin="15, 5, 0, 5" Visibility="{Binding IsLoading, Converter={StaticResource btv}}">
 <ProgressBar IsIndeterminate="True" />
 <TextBlock Text="Loading" FontSize="6" TextAlignment="Center" />
 </Grid>
</HierarchicalDataTemplate>
I just want to point out there are two IsLoading property:
  • IsLoading above is linked to DirectoryTreeItemViewModel (which shown when loading a specific subdirectory),
  • LoadingAdorner one is linked to DirectoryTreeViewModel (which shown when looking up for a specific DirectoryTreeItemViewModel)

When an item is Selected, by setting HierarchyViewModel .IsSelected property, which linked to TreeViewItem.IsSelected dependency property, the DirectoryTree will try to bring it to view.
this.AddHandler(TreeViewItem.SelectedEvent, new RoutedEventHandler(
 (RoutedEventHandler)delegate(object obj, RoutedEventArgs args)
 {
 if (SelectedValue is DirectoryTreeItemViewModel)
 {
 DirectoryTreeItemViewModel selectedModel = SelectedValue as DirectoryTreeItemViewModel;
 SelectedDirectory = selectedModel.EmbeddedDirectoryModel.EmbeddedDirectoryEntry;
 if (args.OriginalSource is TreeViewItem)
 (args.OriginalSource as TreeViewItem).BringIntoView();

 _lastSelectedContainer = (args.OriginalSource as TreeViewItem);
 }
 }));
_lastSelectedConiner is later use when user try to rename an item, it's too hard to get the container (I meant TreeViewItem, not it's model) from TreeView.
switch (command)
{
 case "rename":
 if (this.SelectedValue != null)
 {
 if (_lastSelectedContainer != null)
 SetIsEditing(_lastSelectedContainer, true); //Tell EditBox to show it's Adorner(TextBox) for user to edit 
 }
 break;
}


Other Components

There are a number of components developed for the FileList and DirectoryTree:
  • Components
    • DragDropHelperEx - Add drag and drop functionality to ListView or TreeView, it support DirectoryInfoEx entries only.
    • ExExtension - Construct DirectoryInfoEx in xaml markup. e.g.
      <uc:Ex ::{20D04FE0-3AEA-1069-A2D8-08002B30309D} /> 
    • SelectionHelper - Add multi-select by dragging functionality to ListView, more information here
    • VirtualWrapPanel/VirtualStackPanel - As the name mention it's virtual version (generate on demand) of specified panel, noted that they are fixed-size only.
      If you are interested, Thiago de Arruda has developed a VirtualizingWrapPanel that support variable size.
    • VirtualWrapPanelView - A ViewBase (ListView.View) that uses VirtualWrapPanel.
  • Converters
    • DynamicConverter - A converter that do the convert using two lamba statement, e.g. :
      _intToStringConverter = new DynamicConverter<int, string>(x => x.ToString(), x => Int32.Parse(x));
    • ExToIconConverter - Convert DirectoryInfoEx entries to it's icon in ImageSource format
      <local:ExToIconConverter x:Key="ati" />
    • ExModelToIconConverter - Inherited from ExToIconConverter, Convert ExModel/ExViewModel to it's icon in ImageSource format
      <local:ExModelToIconConverter x:Key="amti" />
    • FileNameToIconConverter - Convert string to it's icon in ImageSource format, more information here.
    • StringToExConverter - Convert a string to DirectoryInfoEx entry.
    • ModelToExConverter - Extract DirectoryInfoEx entry from a ExModel/ExViewModel .

  • UserControls
    • EditBox- a replacement of TextBox + Label, which display the Label when not IsEditing, and TextBox (EditBoxAdorner) when IsEditing, more information here.
    • EditBoxAdorner - the TextBox part of the EditBox
    • LoadingAdorner - a circular animation that shows when DirectoryTree is finding child.
    • SelectionAdorner - display a rectangle when user is dragging on FileList.

Conclusion


I have described how did I constructed the FileList and DirectoryTree using the MVVM pattern, compared with NO pattern or MVP pattern, using MVVM pattern have these advantages :
  • split the code to simplify the developing process, and
  • make your application less UIThread hogging, which makes it more responsive.
    • accessing property from ViewModel instead of the UIElement (e.g. adding new Tab in TabControl, or lookup item in TreeView)
    • without ViewModel is quite hard to thread the items loading, which will freeze your UI,
  • allow you to UnitTest the ViewModel. Presenters are defined in a way which is tightly coupled with the View :
    public class FileListPresenter : PresenterBase<FileListView> { ... }
    

The photo above shows the other UserControls that I developed using the similar method described above, These are not included in this article. If you just go through my torture article you should be able to create the controls yourself.

Lets say I want to construct the Toolbar using MVVM, I would
  • Construct and Style a ToolbarBase that have nothing to do with MVVM, that included ToolbarBase and ToolbarItem
    • Make sure it's working without MVVM, using a simple WPF application
  • Construct the UserControl (e.g. Toolbar), which contains or inherited from ToolbarBase
  • Construct Models and ViewModels
    • Construct an abstract class named ToolbarItemModel
      • Which represent a ToolbarItem, you may want to have a ToolbarSubItem as well.
    • Construct a ToolbarViewModel (RootModel or DataContext), which will host a number of ToolbarItemModel (in ObservableCollection), and method to poll them in background.
    • Construct custom ToolbarViewModels (e.g. ViewModeViewModel, to change ViewMode, see the right side)
  • Construct View
    • Construct HierarchyDataTemplate for the ToolbarViewModel
    • Configure Style to bind the ToolbarItem dependency properties to ToolbarItemModel
      <Style x:Key="{x:Type uc:ToolbarItem}" TargetType="{x:Type uc:ToolbarItem}" > 
       <Setter Property="DependencyProperty" 
       Value="{Binding Property, Mode=OneWay}" />
      </Style> 
  • Hook them together, done.
If you found this article useful, please vote for the article. If you found anything thats not correct, please tell me your thoughts. Thank you

References


History

  • 2 May 2010 - Inital version 0.1
  • 4 May 2010 - 0.2 - screenshot
    • Added FileList.SortBy and SortDirection property.
    • Changed GridView selection behavior / Template
    • Fixed FileList not showing GridView Header.
  • 6 May 2010 - 0.3 - screenshot
    • Added TileView
    • Added GridView.Type header
    • Added DriveModel, all DirectoryModel is now constructed using ExModel.FromExEntry() method.
    • Added GridViewHeader (for sorting) for every ViewModel.
  • 13 May 2010 - 0.4
    • Fixed Small Image Icon not shared by all instance.
    • Enabled BugTrap support in app.xaml.cs. Fixed all warning messages.
    • Added DirectoryTree.AutoCollapse, collapse unrelated directory when changed externally.
    • Fixed FileList scrolling :
      • scroll based on a property (SmallChanges), instead of 10pt.
      • scroll horizontally if Orientation equals Vertical (e.g. ListView)
  • 28 May 2010 - 0.5 - screenshot
    • Fixed a bug related to expand wrong directory when expand via double click on file list.
    • Fixed a crash in DriveModel (Drive not found)
    • Added FileList Select(), SelectAll(), UnselectAll() and Focus() method.
  • 18 Jun 2010 - 0.6 - screenshot
    • Updated DirectoryTree Style so it match the style of FileList.
    • Added a wide range of Commands (in SimpleRoutedCommand format, 6 for FileList, 2 for DirectoryTree and 6 for both), can be accessed by
      - calling FileList/DirectoryTree.Commands (In separate class to reduce the complexity of main control).
      - most of those commands are bound with a RoutedUICommands, like ApplicationCommands.SelectAll.
      - shortcut keys (e.g. F2 for rename)
      See FileList/DirectoryTree/SharedCommands.cs for details.
    • Updated DirectoryInfoEx to 0.18.
  • 18 Jul 2010 - 0.7 - screenshot
    • Fixed a bug that caused wired thumbnail render.
    • Fixed W7TreeViewItem Style, which enable hot track only over the text instead of the whole line.
    • Fixed Virtual FileListItem retain selection state (IsSelected = true) after SelectAllCommand and User selecting another item (via Selection Helper).
    • Fixed click on GridView Header recognize as drag start.
    • For GridView, only support selection if drag occur inside the first column
    • Drop operations now use WorkEx, which support custom progress dialog implementation and run in separate thread.
    • OverwriteMode changes in DirectoryInfoEx 0.19. (see below)
  • 13 May 2012 - 0.8 -
    • Fixed Windows 7 related crashes:
      • ExIconConverter no longer link with SysImageList, as it's shared among different user controls. (but that means no more Extra large and Jumbo icons)
      • A number of dlls thats required is reside in another directory.
    • QuickZip.IO.PIDL library is updated.

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