Click here to Skip to main content
15,867,704 members
Articles / Web Development / HTML

My Personal Commander Variant

Rate me:
Please Sign up or sign in to vote.
4.66/5 (13 votes)
5 Oct 2016CPOL14 min read 44.4K   1.2K   46   6
A C#/WPF application for displaying folders on a grid and performing combined functions on them.

Additional requirements:

  • On C# 4.0 installation of the Expression Blend SDK may be required for the code to run
  • System.Management.Automation - typically found in C:\Program Files\Reference Assemblies\Microsoft\WindowsPowerShell\v1.0\System.Management.Automation.dll after Installation after the PowerShell SDK. If you don't want to have that, remove the Lobster.PowerShell project from the solution. The application will run anyway, however PowerShell functionality will not be available

 

Table of Contents

Introduction 

The MPCV (My Personal Commander Variant) aims to become a highly customizable and extendible file system navigator alternative to the Windows Explorer. Here, customizable means that the user is able to change useful behavior and appearance attributes of the application directly in the User Interface. Extendible means that a developer can easily add support for new object types and attributes (e.g. the MPCV could be easily extended for displaying a process/thread list by subclassing a small set of base classes).

However I do not want to write a clone or replication of functionality of some other system. In fact, I have only some very old memories of Norton Commander and have hardly ever looked at other software such as Total Commander (see http://en.wikipedia.org/wiki/Norton_Commander). If I had, I probably would not write my own.

Then, why: For me; learning WPF and some other .NET technologies (which worked quite well so far), keeping up-to-date with design patterns, generating some reference code that is actually my own, and eventually creating something useful.

For you; Well, did you pay for it? If yes, I'd like to meet the person who sold it. Now, honestly there might be some ideas or code parts that could be useful. If you find something to use somewhere else, please leave a note.

Here is what it looks like:

Main Window

Please do not expect an article about WPF. There may be some ranting later on, but I have nothing new to add, so I will be sticking to explaining how the application works and what can be reused from it.

At this point, a lot of time has flown into the application and I would like to share my results.

The application is divided into the following assemblies:

  • Lobster.MPCV: The main application and the view in MVVM sense. It contains the solution file mpcv.sln.
  • Lobster.MPCV.VM: The Viewmodel.
  • Lobster.FolderBrowser: The model is designed as an easily extendable and reusable component.
  • Lobster.MVVM: My own experimental MVVM library. I don't regard it fit for reuse.
  • Lobster.Components: Some base classes that are not directly linked to MVVM or the application itself.

The application requires no installation. It keeps an app_state.xml file for serializing the configuration in your roaming app-data folder (e.g., C:\Users\YourUserName\AppData\Roaming\MPCV on Windows 7).

MPCV user guide  

The user documentation can be found in the Lobster.MPCV project, sub-folder "help". It is included into the build process, after building and starting the application, the help may be accessed via the menu bar "Help" > "User Help".  

In short, the application is a multiple files browser with a few extras. It supports multiple tabs containing up to any number of file browsers that your monitor is able to display. Highlights are:

  • Folders and settings are persistent between sessions.
  • Folder browsers can be given title names and background colors as well as a fixed parent folder so that only a certain directory tree can be navigated. 
  • Folder browsers can be linked together so that they can be navigated in parallel related to their given parent folders.
  • A configurable toolbar on each tab that can launch applications.
  • By dropping a file onto a folder browser, the folder is navigated to it. By this, the location of one folder browser may be copied to the one of another. It is not possible to copy or move a file to another application.  
  • The complete inability to change the browsed files and folders by accident - except if you configure the toolbar in such a way.  

Further information about the current state of the project:

  • Single-selection of files in each browser.
  • Double-click on file opens it in its standard application 
  • A configurable and independent context menu for launching shell commands
     

Besides of what it does or does not do, here is an idea about what this application can be used for:

Diff applications are one of the key tools of software version control systems (VCS). At least with the ones I used they were integrated well into the VCS and not much trouble to use. However, when it comes to comparing files which are on the file system, not much effort is made to aid the user in the file selection process. Often enough the user has to click his way through half the file system to get to the folders containing the needed files (okay, if he's halfway smart he would probably paste the path from Windows Explorer to some file dialog) and little help is provided for people desiring to do multiple subsequent comparisons. 

Points of interest

Customization via PowerShell

I have been adding support for Powershell so the MPCV can be customized by external scripts. For now, only the background color of folder browser items can be changed, but this already opens a range of options. The mechanism is pretty easy. Whenever the content of a folder browser is changed, a powershell script is called with the list of items contained. Within the script, each item can be analyzed and a new background-color may be set. The list given to the script is represented by Lobster.MPCV.VM.FolderBrowserInfoVM.FolderItemWrapper objects (simplified declaration):

public class FolderItemWrapper {
    public PathItemVM Item { get; }
	public PathItemVM GetSynchronizationSource();
}

This object is a mere level of indirection in order to allow access to the current item as well as its synchronized item in another folder browser (Item Synchronization can be configured in the menu "Tabs" > "Properties"). Item is a property as it is directly bound; the synchronized item has to be actively looked up, which will take about O(N) for each item and O(N²) in total. Each path item iteself can be accessed by the following simplified interface:

C#
public class PathItemVM {
    public string DisplayName {get;}
    public string PathName {get;}
    public System.Windows.Media.Color BackgroundColor {get; set;}
}

As a sample, I have used this new interface to create a folder comparison (found in $\lobster\MPCV\Scripts\Compare.ps1). Newer files are marked in light green while older ones are marked in light red. Files that are not found in the other folder are marked light gray. Identical files keep their white background:

Start-Sleep -m 500

foreach ($i in $input) { 
    $c = New-Object System.Windows.Media.Color
    
    $source = $i.GetSyncSource();
    # $bkgcol = $i.Item.BackgroundColor;
    
    $c.A = 255;
    $c.R = 255;
    $c.G = 255;
    $c.B = 255;

    if ($source) 
    {    
        # object does exist in synchronization source
        # Test whether it is a file
        if ((Test-Path $i.Item.Pathname -pathType leaf) -and 
                       (Test-Path $source.Pathname -pathType leaf))
        { 
            $md5 = new-object -TypeName System.Security.Cryptography.MD5CryptoServiceProvider;
            $hash1 = [System.BitConverter]::ToString(
                      $md5.ComputeHash([System.IO.File]::ReadAllBytes($i.Item.Pathname)));
            $hash2 = [System.BitConverter]::ToString(
                      $md5.ComputeHash([System.IO.File]::ReadAllBytes($source.Pathname)));
            if ($hash1 -eq $hash2) {
                # Files are identical - keep white
            } else {
                    $lw1 = (Get-Item $i.Item.Pathname).LastWriteTime;
                    $lw2 = (Get-Item $source.Pathname).LastWriteTime;
                if ($lw1 -gt $lw2) {
                    # Files are different - mark red
                    $c.A =255;
                    $c.R =128;
                    $c.G =255;
                    $c.B =128;
                } else {
                    # Files are different - mark red
                    $c.A =255;
                    $c.R =255;
                    $c.G =128;
                    $c.B =128;
                }
            }
        } else {
            # At least one item is not a file, therefore state is unknown
        }
    } else {
        # Object does not exist in synchronization source
        $c.A =255;
        $c.R =192;
        $c.G =192;
        $c.B =192;    
    }    
    $i.Item.BackgroundColor = $c;
}

Once the script is in place, it has to be linked from the folder browsers (Set the full pathname to the script in menu "Tab" > "Configuration" > Folder Browser Configuration > "Update Script" for each file browser that should use the script). The resulting customized MPCV will look as follows:

Powershell Result Sample

The script will run asynchronously in order not to interfere with the loading of the folder contents.

The FolderBrowser model

The Lobster.FolderBrowser namespace is designed to be reusable and extensible. Entry point is the class PathModelBridge, which is set to a path by the CurrentPath property. The Items collection will then contain the items located at the given path. All in all, the following members are of interest:

C#
public class PathModelBridge {
    /// The path (e.g. C:\Windows) which items are to be shown.
    /// When a new path is set, it may relative to the current path 
    /// as well as absolute. The path model creators are responsible 
    /// for finding out what is meant.
    public string CurrentPath { get; set; };
		
    /// The items that are located at the current path and are shown
    /// under the given configuration. This is an observable collection
    /// as the implementation may observe the path and reflect changes 
    /// directy. 
    public ReadOnlyObservableCollection<IPathItemModel> Items { get; };
		
    /// Performs a precheck if there is a parent path to the current path
    /// and whether this path is an extension of the base path
    public bool CanNavigateUp { get; };
				
    /// Performs a precheck if a given path extends the current base path 
    public bool ExtendsBasePath(string value);
		
    /// Gets or sets the base path which serves as root for any navigation. The current 
    /// path may not be further up the directory tree than the base path.
    public string BasePath { get; set; };
		
    /// Returns true, if the current path can be set to the given path. False
    /// may be returned if the base path is violated or there is no appropiate
    /// implementation for the given path.
    /// The given path may be absolute or relative to the current one. The
    /// path model creators are responsible for finding out what is meant.
    public bool CanNavigateTo(string path);
		
    /// Is true if the base path can be navigated to. This is typically 
    /// not the case if the base path is already the current path.
    public bool CanNavigateToBase { get; };
}

You probably noted all the writing about implementations, the idea to that is as follows: As soon as a current folder is set (which is just a string from the architecture point of view, currently there is just only an implementation for pathnames), the bridge chooses a proper implementation by using a factory pattern and then returns the items delivered by this implementation. I've created a class diagram to give an overview of the model:

Model Class Diagram

All folder implementations provide the IPathModel interface, there currently is an implementation for the workspace DriveModel and one for folders FileModel. Folder items are created by the implementation and must provide the IPathModelItem interface. 

Therefore own implementations can be easily written and hooked into the factory.

WPF and MVVM 

Honestly, there are a lot, especially with the WPF part. I had to figure out a lot there and as it had to be small problems taking the greatest amount of time. WPF and MVVM certainly is not a way to easily develop user interfaces in no time. Anyway, I really don't want to delve into this as I am a bit late for writing tutorials on MVVM. If you are considering to write a WPF project ask yourself this: Am I really willing to put up with the ignored complexity (attached dependency properties, converters, binding notations, validators, bloated cs code and XAML making XSLT look nice) for the benefits everybody everybody wants to see in it?

A while ago, CodeProject made a poll asking Will programming become more or less complex in the future?. In short, quite many people feel that the complexity stays the same. People also might want to maintain legacy software just to make sure complexity won`t rise.

The Collection Joiners

OK, enough with the ranting. As the name says, the collection joiner class set (located in Lobster.Components.Collections.Joiner) provides on-the-fly joining for ICollection based collections.

Just place a couple of generic collections into the input Collections of the CollectionJoiner class, and the output in JoinedCollection (type ReadOnlyObservableCollection - it seemed to much effort to me allowing adding and removing items here) is updated each time a collection is added or removed from the input. Also changes to any of the input collections are forwarded to the output (except for Clear(), at the moment NotifyCollectionChangedAction.Reset is not supported).

Both classes internally use the CollectionJoiner

Another feature is the automatic wrapping of items in the joined collection. The constructor allows to be given a class type TJoinedItem which is used in the output instead of the input item types.

C#
public class CollectionJoiner<TJoinedItem> 
    where TJoinedItem : class
{...}

Of course, the joiner usually does not know by itself how to create the output item from collection items, so this information has to be given by using the delegate CreateItemBox. Here's a usage sample from the application:

C#
...
{
    ...
    var SubItemWrapper = new CollectionJoiner<ITreeNodeVM>();
    SubItemWrapper.CreateItemBox = (o) =>
    {
        return createItemBox(this, o);
    };
    SubItems = SubItemWrapper.JoinedCollection;
    SubItemWrapper.Collections.Add(subItems);

    this.SubItems = SubItemWrapper.JoinedCollection;
    ...
}
...

There is a special optional interface IItemPositionInfo for TJoinedItems, which comes with the implementation and are specially supported, if used. The interface provides an property Y which identifies the index of the input collection and X giving the index of the item within the input collection.

There are two more typesafe wrappers for the CollectionJoiner:

C#
public class ObservableCollectionJoiner<TItem, TJoinedItem> 
    where TItem : class
    where TItemBox : class
{...}
				
public class ReadOnlyObservableCollectionJoiner<TItem, TJoinedItem> 
    where TItem : class
    where TItemBox : class
{...}

When using these classes instead of the base implementation, the input collection type is fixed to ObservableCollection / ReadOnlyObservableCollection. The input item type is fixed via the TItem type parameter. I prefer these classes as they are more typesafe and therefore will warn during compiling if I try to add incompatible collections.

I came to use it quite often in the implementation, e.g. where each row of browsers in the main window is a collection of its own, but the "Folder Browser Configuration" has to display information about all of them in one go. You can also see the generated index information in the first column.

Of course, there are standard components which can be used for similar tasks:

  • System.Windows.Data.CollectionViewSource can be used for filtering as well as sorting.
  • System.Windows.Data.CompositeCollection allows merging of different input collection types (they don't need to be observable) and does not specify an output type.

How to add your own detail to path items

Lets assume we want to extend the file model by adding information about the file version. Technically, the file version is easily acquired by:

<a name="OwnDetails">FileVersionInfo myFileVersionInfo = FileVersionInfo.GetVersionInfo(pathName);
string fileVersion = myFileVersionInfo.FileVersion;</a>

In order to add that information to the FileModel class, we open FileModel.cs. There we insert a property FileVersion at the end of the class (The order of properties in the item model is important as it is used as the initial order in the UI as well). By standard, the property name will by used as the column title. Another title can by assigned by using the DisplayNameAttribute. As the FolderModel that creates the FileModels may perform updates from a background thread, the property must be thread safe and marshal notifications to the UI thread:

C#
<a name="OwnDetails">private object fileVersionLocker = new object();
private string fileVersion;

[DisplayName("File Version")]
public string FileVersion {
    get {
        lock (fileVersionLocker) {
            return fileVersion;
        }
    }
    private set {
        lock (fileVersionLocker) {
            if (value == fileVersion) return;
            fileVersion = value;
        }
        syncContext.Post((o) => {
            this.OnPropertyChanged("FileVersion");
        }, null);
    }
}
</a>

Item models are usually initialized from their contructor. But as the FolderModel may send updates, the initialization has been moved to the FileModel.UpdateDetails() method. This method is also called when the underlying file is changed. In the part of this method dealing with files, we add the following code:

C#
<a name="OwnDetails">public void UpdateDetails()
{
    ...
    if (File.Exists(pathname))
    {
        ...
		
        FileVersionInfo myFileVersionInfo = FileVersionInfo.GetVersionInfo(pathname);
        FileVersion = myFileVersionInfo.FileVersion;
    }
    ...
}
</a>

And thats it. As new properties are automatically added to the UI, you may need to hide them. For this, there are two attributes: Properties tagged [PropertyHidden] are never forwarded to the UI while [PropertyHiddenByDefault] signals that the property is initially not shown in the UI.

References

On WPF and MVVM

On MVVM a lot has been written, so better read it from there than from me:

More references for specific problem solutions can be found in the code.

History

  • 2012/12/13 First release
  • 2013/01/06
    • New configuration dialog replacing the DataGrid implementation. The new configuration dialog required a new VM which shows that it is not really possible to separate View and VM. E.g. replacing the two DataGrids (requiring each a SelectedItem VM member) by a TreeView with only one selection and a completely different selection model (IsSelected member in each tree item VM) trigger great changes in the VM.
    • Model now accepts relative pathnames; The original model required the VM to calculate absolute paths from relative input which required the VM to make an assumption that paths always look like file system paths. This should be responsibility of the model. 
  • 2013/01/11 Download Correction: Well, I included the wrong project and nobody even told me ...  Anyway, now it's fixed ... I hope 
  • 2013/01/14 Fixes to the MiniColorPicker, the toolbar and the configuration dialog
  • 2013/02/09
    • Path items may have associated details that are optionally displayed similar to the report view of the windows explorer. Adding a new detail to an existing path model is as easy as writing a property.
    • New path model details for the FileModel class are: File size, attributes (text representation), creation and change date.
    • New path model details for the DriveModel class are: free size, total size, used percentage, volume label
    • Path models may be individually configured. While path models are newly created after each path change, pathmodel configurations are reused for each PathModelBridge.
    • Improvement of the path edit box command syntax. These now accept commands with parameters. E.g. by clearing the path, then typing "explorer ." plus enter a windows explorer will be opened at the currently selected path.  
  • 2013/03/24
    • Crash fix for UI update on folder content changes
    • Configurable context menus for folder browsers
    • User guide as HTML Help .chm file accessible from the MPCV
    • Switch to ListView for displaying path items
  • 2013/04/07
    • Textsearch enabled, items in the active browser may be found by typing their first characters
    • Bookmarks, important items may be highlighted on base of their path name
    • MPCV can be positioned on top of all windows  
  • 2013/04/20
    • Tab order improved
    • Keyboard focus on path textbox and folder browser clearly identified by red border
    • Folder browser accepts Enter key for switching to sub-folder or activating primary command (for path items)
    • Folder browser accepts Backspace key for switching to parent path
  • 2013/05/09
    • Execution of context menu on wrong folder browser fixed
    • Crash on observation of short-lived files fixed (When FolderFileModel.UpdateDetails(...) ran on a file that was currently in the process of deletion)
  • 2013/05/30
    • Basic PowerShell support for customization added, currently an update script may change the background colors of folder items
  • 2013/11/23
    • Fixed two crashes when adding a new tab and when adding an folder browser to a row in the tab configuration. Anyway, where's the QA department when you need it. Added minor improvements as access to the home folder and using the {home} placeholder in the PowerShell script path.
  • 2016/10/05
    • Fixed display of underscores in file names

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer
Germany Germany
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
QuestionMy Personal Commander Variant Pin
Hans Hinterseer13-Oct-16 0:20
Hans Hinterseer13-Oct-16 0:20 
GeneralMy vote of 5 Pin
ishihidul14-Jan-13 11:53
ishihidul14-Jan-13 11:53 
GeneralRe: My vote of 5 Pin
Doc Lobster14-Jan-13 20:04
Doc Lobster14-Jan-13 20:04 
GeneralAll image links in the article are broken. Pin
pt140112-Jan-13 4:45
pt140112-Jan-13 4:45 
AnswerRe: All image links in the article are broken. Pin
Doc Lobster12-Jan-13 6:03
Doc Lobster12-Jan-13 6:03 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.