WPF x FileExplorer x MVVM






4.99/5 (49 votes)
This article describe how to construct FileExplorer controls included DirectoryTree and FileList, using Model-View-ViewModel (MVVM) pattern.

- Download FileExplorer2_(VS2010).zip on CodePlex - 4MB (Screenshot: FileExplorer2.png
)
- Download FileExplorer1_(VS2010).zip - 4MB
- Download FileExplorer1_(VS2008).zip - 1.3MB
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.
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
- List directories and files (start from Desktop)
- Performance
- Sub-items are loaded in background
- Lookup directory in DirectoryTree
in background
- Construction of ListViewItems and TreeViewItems are virtualized
- DirectoryTree
- Setable root
directory
- Setable and getable selected
directory
- Setable root
directory
- FileList
- Multi-Select support using dragging
- Getable selected entries
- Multiple view mode (e.g. LargeIcon)
- Search within fileList using filter
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
DirectoryInfoEx
on this page may not be the
most update, please check it's
page for update.DirectoryTree (TreeView)
- DirectoryTree
- RootDirectory
- SelectedDirectory
- RootDirectory

You can set the
RootDirectory
and
SelectedDirectory
using the ExExtension
.
Ex
is
a
MarkupExtension
, which return theappropriate
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)
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.<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. 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
- Represent a
- ViewModel - 2 kinds
- Model of a
DirectoryTree
/FileList
- Model of a
DirectoryTreeItem
/FileListItem
/FileListCurrentDirectory
(which embedded a Model)
- Model of a
- View
- The
DirectoryTree
/FileList
itself, no code-behind exceptDependencyProperties
and someEventHandlers
- The
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)
- CurrentDirectoryViewModel (FileList)
- HierarchyViewModel
- DirectoryTreeViewItemViewModel (TreeViewItem)
- DirectoryTreeViewItemViewModel (TreeViewItem)
- FileListItemViewModel (ListViewItem)
- DirectoryViewModel
- RootModelBase
- DirectoryTreeViewModel (DirectoryTree which is a TreeView)
- FileListViewModel (FileList which is a ListView)
- ExViewModel
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)
- RefreshCommand (which calls
CurrentDirectoryModel.Refresh())
- FileListViewModel
FileListViewModel
is 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
- CurrentDirectoryModel
- DirectoryViewModel
CurrentDirectoryViewModel
responsible
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
- FileListItemViewModel
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
- HierarchyViewModel
- ExViewModel
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
- DirectoryTreeViewModel
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
- HierarchyViewModel
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)
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

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
<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.
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
- AutoCollapse (0.4)

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,
- accessing property from ViewModel
instead of the UIElement (e.g.
adding
new
Tab in TabControl, or lookup item in TreeView)
- 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
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 an abstract class named ToolbarItemModel
- 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>
- Construct HierarchyDataTemplate
for the ToolbarViewModel
- Hook them together, done.
References
- WPF: If
Carlsberg did MVVM Frameworks (Sacha Barber)
- Simplifying the WPF TreeView by Using the ViewModel Pattern (Josh Smith)
- WPF
TreeView
Selection (DaWanderer)
- Editing In ListView (ATC Avalon Team)
- VB IShellLink Interface (Vb accelerator)
- DataModel-View-ViewModel
pattern (Dan Crevier's Blog)
- Silverlight MVVM: An (Overly) Simplified Explanation (Michael Washington)
- Arranging Shapes in a Circle with Expression Blend (Walt Ritscher)
- ViewBase
Class (MSDN)
- Rewrite
DirectoryInfo
using
IShellFolder
- Enable
MultiSelect
in
WPF
ListView
(2) (Yes there is version
1 but you would'nt want to use it)
- WPF Filename To Icon Converter
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)
- scroll based on a property (SmallChanges), instead of
10pt.
- 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.