Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / WPF

Extending DirectoryInfoEx to support Archives

0.00/5 (No votes)
31 Dec 2009LGPL313 min read 30.3K   334  
Allow users to make virtual directories and use them via a similar interface as DirectoryInfo.

Introduction

DirectoryInfoEx is a component that uses IShellFolder to list file system contents. It has a similar interface to System.IO.DirectoryInfo, but it can list Desktop items as well as items that do not exist in the file system.

It does great, but it cannot list files in archives. Windows XP+ does provide a zip folder, which is a namespace extension that allows to list and extract files in zip, but it's not sufficient for my purpose (by the way, it's possible to list zip in DirectoryInfoEx using IShellFolder, but I disabled it purposively, zip is treated as file instead of folder), so a new component was developed for this purpose.

The component included in this article further extends the previous component (DirectoryInfoEx) to support archives, and provides an interface for other users to add support to non-existing folders (e.g., bookmarks). The initial reason to develop this component is for supporting compressed archives, but the component is quite flexible, i.e., you can toggle the archive support using just one line of code.

The component (as well as DirectoryInfoEx) was written using .NET 2.0, although the demo requires the .NET 3.5 framework.

This article mostly describes how I created the component using my method, so this article won't be as technical as my previous article, but I hope the reader will find the component included in this article useful.

Issues encountered

The first issue is that, I want to provide a similar interface as DirectoryInfoEx (ex), but the ex version works nicely and I want to reuse most of the code without doing any modification, and the result is the DirectoryInfoExA (exa) version, and a few small modifications in ex to enable inheritance.

The exa version is located in separate assemblies. (PIDL for ex, COFE for exa.)

Another issue is that ArchiveDirectoryInfoExA is a lot different from DirectoryInfoExA. It's not practical for ArchiveDirectoryInfoExA to inherit from DirectoryInfoExA. To solve the problem, an interface is used for every class (e.g., DirectoryInfoExA.GetDirectories() returns the IDirectoryInfoExA interface instead of the DirectoryInfoExA class).

The third issue is that because some of the exa items are virtual, features that require the actual file/directory to exist, like drag and drop and context menu, cannot be used unless the items are expanded. This is solved by "expanding" the items twice, by:

  1. Generating zero-size-files/empty-directories first (for acquiring PIDL).
  2. Start the command. And when needed:
  3. Expand the file/directory (overwrite those zero-size-files/empty-directories).
  4. Invoke the selected command.

Index

FileSystemInfoExA implementation

FileSystemInfoExA is the base class for all ExA items; this is similar to FileSystemInfoEx, but unlike the ex version, the exa version is an abstract implementation of the IFileSystemInfoExA interface. The getFullName() method is implemented in the child class, e.g.:

C#
public new string FullName { get { return getFullName(); } }
protected abstract string getFullName(); 

Besides that, the exa version also adds a property named ParseName. Unlike the ex version, some of the paths are "fake" and not parsable by the shell; these paths are represented by ParseName, where FullName returns its temp path only.

There are two layers of hierarchy: Interface and Class.

Interface hierarchy

Image 1

IFileSystemInfoExA, IDirectoryInfoExA, and IFileInfoExA interfaces

C#
public interface IFileSystemInfoExA
{
 PIDL PIDLRel { get; }
 PIDL PIDL { get; }
 string Label { get; }
 string FullName { get; }
 string Name { get; }
 string ParseName { get; }
 string Extension { get; }
 bool Exists { get; }
 IDirectoryInfoExA Parent { get; }
 FileAttributes Attributes { get; }
 DateTime LastWriteTime { get; }
 DateTime LastAccessTime { get; }
 DateTime CreationTime { get; }
 bool IsFolder { get; }
 void Delete();
 void Refresh();
 bool Equals(object obj);
 int GetHashCode();
}
public interface IDirectoryInfoExA : IFileSystemInfoExA
{ 
 IDirectoryInfoExA Root { get; }
 bool IsBrowsable { get; }
 bool IsFileSystem { get; }
 bool HasSubFolder { get; }
 DirectoryInfoEx.DirectoryTypeEnum DirectoryType { get; }

 void Create();
 void MoveTo(string destDirName);
 
 IDirectoryInfoExA CreateDirectory(string dirName);
 IVirtualDirectoryInfoExA CreateVirtualDirectory(string dirName, string type);

 IFileInfoExA[] GetFiles();
 IDirectoryInfoExA[] GetDirectories();
 FileSystemInfoExA[] GetFileSystemInfos();
}
public interface IFileInfoExA : IFileSystemInfoExA
{
 long Length { get; }
 void MoveTo(string destFileName); 
 FileStreamEx OpenRead();
 StreamWriter AppendText();
 StreamReader OpenText();
 FileStreamEx Open(FileMode mode, FileAccess access);
 FileStreamEx Open(FileMode mode);
 FileStreamEx Open();
 FileStreamEx Create();
}

Expose the properties and methods in the original FileSystemInfo, DirectoryInfo, and FileInfo classes. IDirectoryInfoExA contains the CreateVirtualDirectory() method, which is used to construct a special directory (e.g., Archive in this case).

IInternalDirectoryInfoExA interface

C#
public interface IInternalDirectoryInfoExA : IDirectoryInfoExA
{
 void put(IFileSystemInfoExA file);
 IFileInfoExA CreateFile(string fileName);
}

Inherited from IDirectoryInfoExA, it contains some methods that are not designed to be visible to the end-user.

IVirtualFileSystemInfoExA, IVirtualFileInfoExA, IVirtualDirectoryInfoExA interfaces

C#
public interface IVirtualFileSystemInfoExA : IFileSystemInfoExA
{
 string TempPath { get; }
 string RelativePath { get; }
 void expandSelf();
}

IVirtualFileSystemInfoExA exposes the TempPath (the path after expanding) and RelativePath (to its root) properties, as well as the expandSelf() method.

C#
public interface IVirtualDirectoryInfoExA : 
       IDirectoryInfoExA, IVirtualFileSystemInfoExA
{
 bool IsRoot { get; }
}
public interface IVirtualFileInfoExA : 
       IFileInfoExA, IVirtualFileSystemInfoExA
{
}

The IVirtualFileInfoExA and IVirtualDirectoryInfoExA are currently empty interfaces. The root virtual directory should have a DirectoryType property return root, and implement the IFileBasedFS interface, if applied.

IFileBasedFS interface

C#
public interface IFileBasedFS
{
 IFileInfoExA RootFile { get; }
}

This is implemented by the root of IVirtualDirectoryInfoExA, which specifies the RootFile property. For ArchiveInfoExA, RootFile points to the archive file.

Class hierarchy

Image 2

The base class, FileSystemInfoExA

To reduce complexity, FileSystemInfoExA does not inherit from FileSystemInfoEx. It implements the IFileSystemInfoExA interface, providing the same properties / methods available.

C#
public PIDL PIDL { get { return getPIDL(); } }
protected abstract PIDL getPIDL();

Most properties are implemented as abstract, and they must be implemented by the inherited classes.

PIDL directory and file (WrapFileSystemInfoEx)

DirectoryInfoExA and FileInfoExA are inherited from WrapFileSystemInfoEx, which is inherited from FileSystemInfoExA.

C#
public FileSystemInfoEx embeddedItem { get; private set; }
protected override PIDL getPIDL() { return embeddedItem.PIDL; }

WrapFileSystemInfoEx has a property named embeddedItem. All property requests and methods are redirected to embeddedItem. It contains a property named embeddedItem, which is a FileSystemInfoEx, and all work in the exa version is redirected to the ex version.

Virtual directory and file (VirtualFileSystemInfoExA)

C#
public new FileSystemInfoEx embeddedItem { get { initSelf(); return base.embeddedItem; } }
protected override PIDL getPIDL() { expandSelf(); return embeddedItem.PIDL; }

VirtualFileSystemInfoExA is the base class for all virtual folders and files; it is also inherited from WrapFileSystemInfoEx, but its embeddedItem is not initialized until needed. The embeddedItem is associated with the TEMP item in the file system, so most original properties, like FullName and PIDL, are associated with the Temp directory / file. A new property, ParseName, is used to represent the virtual path (parasable path).

The initSelf() and expandSelf() methods are lazy load methods which delay the loading till needed, because most of the time, you don't have to use them at all. E.g.:

ParseName C:\temp\cantonese_input.7z\cantonese_input2\ddd.zip
FullName %temp%\Cofe\%HashCode%\ddd.zip (folder)
TempPath %temp%\Cofe\%HashCode%\ddd.zip (folder)

The HashCode is calculated based on the previous root archive ParseName, in this case, "C:\temp\cantonese_input.7z".GetHashCode();. Also note that ddd.zip in FullName is a real directory instead of an archive.

The difference bewteen FullName and TempPath is that, when you call FullName, the item is created if it does not exist (by calling expandSelf()). But if you call TempPath, it will return you the path without checking if the item was created.

C#
internal abstract IDirectoryLister directoryLister { get; }

VirtualDirectoryInfoExA is inherited from VirtualFileSystemInfoExA; it adds some directory related methods. It also includes a property named directoryLister, which is used to do most of the virtual file operations (more information below).

Archive, ArchiveDirectory, and ArchiveFile (ArchiveInfoExA)

Image 3

Archive support is based on my CAKE3, which is a wrapper (middleware) for a couple .NET and W32 libraries. You can find more information here.

ArchiveInfoExA, ArchiveDirectoryInfoExA, and ArchiveFileInfoExA are inherited from VirtualDirectoryInfoExA / VirtualFileSystemInfoExA. These three classes and ArchiveDirectoryLister are used to provide archive support.

C#
internal IFileInfoExA embeddedArchive;
internal override IDirectoryLister directoryLister { 
         get { return ArchiveDirectoryLister.Instance; } }

ArchiveInfoExA is the root of all archive, thus it has a property named embeddedArchive. As you can see, it's a IFileInfoExA type. That means, embeddedArchive can also be ArchiveFileInfoExA, and thus Archive in Archive is supported.

ParseName C:\temp\cantonese_input.7z\cantonese_input2\ddd.zip
FullName %temp%\Cofe\%HashCode%\ddd.zip (folder)
TempPath %temp%\Cofe\%HashCode%\ddd.zip (folder)
embeddedArchive.FullName%temp%\Cofe\%HashCode2%\cantonese_input.7z\cantonese_input2\ddd.zip (file)

HashCode2 is calculated based on the current root archive ParseName, in this case, "C:\temp\cantonese_input.7z\cantonese_input2\ddd.zip".GetHashCode();.

Bookmark, BookmarkDirectory, and BookmarkEntry (BookmarkInfoExA)

Image 4

Another implementation of the virtual file system is bookmarks.

Bookmark (root) and BookmarkDirectory are inherited from VirtualDirectoryInfoExA, which lists its contents using directoryLister, which has similar methods as ArchiveDirectoryLister. The internal structure of BookmarkDirectoryLister is based on XSD (XML Schema Definition). I create the XSD file, then run xsd.exe to generate the class I needed. It can then be loaded and saved using XmlSerializer. This technique was learnt from Mike Elliott's article; additional information can be found from this MSDN article as well.

Bookmark entry (RedirectDirectoryInfo) is inherited from WrapFileSystemInfoExA, which is the same as WrapFileSystemInfoEx except the embeddedItem is IFileSystemInfoExA, because bookmark may link to a virtual directory (FileSystemInfoExA). Both classes share similar code; I am unable to refactor them, so there is some duplicate code.

Support Interfaces/Classes

A number of interfaces have been developed to support FileSystemInfoExA and its child classes:

IDirectoryLister interface

Every Virtual Directory has a property named directoryLister, which is a class that implements IDirectoryLister; it contains functions to list, add, remove, and extract items, similar to ShellFolder in shell.

DirectoryLister does a similar job as ShellFolder and Storage in Shell.

The earliest version of DirectoryLister (which was designed before DirectoryInfoEx), the directoryLister, and the entity (FileSystemInfoEx) reside in the same class. It was later separated to reduce complexity and some other issues, including:

  • thread problems (call from different thread = crash).
  • control problem (there may be multiple instances of the entity doing actions that conflict).
  • performance problem (list() may be called many times, and there may be many copies of the listed contents).

Because of these problems, using a separate static class is a better solution.

Some of the functions of the IDirectoryLister interface:

Listing and manipulating commands
C#
bool Exists(IVirtualFileSystemInfoExA item);
void Rename(IVirtualFileSystemInfoExA item, string newName);
IEnumerable<FileSystemInfoExA> List(IVirtualDirectoryInfoExA parent, bool refresh);
bool Expand(IVirtualFileSystemInfoExA item);
IVirtualFileSystemInfoExA CreateFile(IVirtualFileSystemInfoExA parent, string name);
IVirtualDirectoryInfoExA CreateDirectory(IVirtualDirectoryInfoExA parent, string dir);
void Put(IVirtualDirectoryInfoExA dir, IFileSystemInfoExA item, bool raiseAlert);
bool HasFolder(IVirtualDirectoryInfoExA parent);

The command names explains themselves.

Entrance point of Virtual Directory
C#
IVirtualDirectoryInfoExA CreateVirtualDirectory(IDirectoryInfoExA parent, 
                         string dir, string type);

Normal and Virtual Directories can create a virtual directory using the IDirectoryLister.CreateVirtualDirectory() method implemented by any directoryLister, the "type" specified by the type of the virtual directory (e.g., zip, lha).

C#
FileSystemInfoExA ConvertItem(FileSystemInfoExA item);

The PIDL Directory's GetFileSystemInfos() method (as well as GetDirectories() and GetFiles()) returns only the file that actually exists, so for every item returning, it calls ConvertItem for each directoryLister; if the directoryLister finds it's supported (e.g., an archive), it can convert it (new ArchiveExA(...)) and return a converted one.

If you need to reference the item, construct a new one instead of using the item directly; the item may be destroyed by GC, and AccessViolationException will trigger when you try to access them; most implemented classes support ICloneable, so you can call the Clone() method to construct.

C#
FileSystemInfoExA[] AppendItems(DirectoryInfoExA parent); 

Another method is to add extra items to the list when GetFileSystemInfos() is called, e.g., adding a bookmark directory to the Desktop.

Filepath parsing

Unlike the PIDL File and Directory, you can't use DesktopShellFolder.ParseDisplayName() to parse a virtual file path, so the default way is to parse from the Desktop and lookup level by level (FileSystemInfoExA.FromStringIterate()), which is slow.

Another method is to parse and construct the directory directly (FileSystemInfoExA.FromStringParse()). For PIDL Files and Directories, ParseDisplayName() is used. For Virtual Files and Directories, because different Virtual Directories may have different ways to parse, IDirectoryLister has a method for each individual Directory Lister to return the represented item.

C#
FileSystemInfoExA ParseDisplayName(string displayName);

This method parses a display name and returns its represented FileSystemInfoExA. For the Archive Directory Lister, it will try to form the last archive node to the first and checks if the node is creatable; if yes, it will iterate from there.

e.g. c:\abc.zip\cde.7z\efg.lha
-> efg.lha exists? no
--> efg.lha 's temp file exists? no
---> cde.7z exists? no
----> cde.7z 's temp file exists? yes
-----> Iterate from cde.7z

If no Directory Lister returns an item, the normal iterate approach is used, which looks up from the Desktop. Most properties constructed using Parse, including Parent, FullName, and other properties in embeddedItem are not initiated until required. Keep in mind that if the item does not exist, a FileNotFoundException will be generated whether you are using FromStringIterate() or FromStringParse().

INotifyFileSystemChanged interface

Changes in virtual directory are reported to their parents by implementing the INotifyFileSystemChanged interface, as follows:

C#
public enum ModiifiedAction { maRemoved, maAdded, maChanged }
interface INotifyFileSystemChanged
{
 /// <summary>
 /// Update parent if file or archive in archive is updated.
 /// </summary>
 /// <param name="childItem"></param>
 void AlertChildIsModified(VirtualFileSystemInfoExA sender, 
      VirtualFileSystemInfoExA originalSender, ModiifiedAction action);

 /// <summary>
 /// Alert parent that current directory is modified.
 /// </summary>
 void AlertModified(ModiifiedAction action);
}

Archive support is inherited from Virtual Directory, so when an item is added / removed / changed, let's say "c:\abc.zip\cde.7z\newFile":

C#
newFile.AlertModified(ModifiedAction.maAdded);
-> newFile.AlertModified(newFile, newFile, ModifiedAction.maAdded);
--> cde.AlertModified(cde, newFile, ModifiedAction.maChanged);
---> abc.AlertModified(abc, newFile, ModifiedAction.maChanged);
-----> because newFile.Root != abc.Root (cde.7z != abc.zip), cde.7z in abc.zip is updated.

This mechanism enables support for multi-level archives, plus allows support for passive virtual file system monitoring.

DirectoryListerLibrary static class

DirectoryListerLibrary maintains a list of DirectoryListers, (e.g., BookmarkDirectoryLister and ArchiveDirectoryLister). All operations are called through the library.

FileStreamExA class

C#
private void updateEmbeddedFile()
{
 if (accessMode == FileAccess.ReadWrite || 
 accessMode == FileAccess.Write) //Require update
 if (embeddedFile is VirtualFileSystemInfoExA) // Virtual file
 {
 embeddedFile.Refresh(); //Update exists state.
 if (!embeddedFile.Exists)
 throw new IOException("FileSystemInfoExA save failed...");
 VirtualFileSystemInfoExA file = embeddedFile as VirtualFileSystemInfoExA;
 file.Parent.put(file); //Update back to parent.
 }
}

public override void Flush()
{
 base.Flush();
 closeStream();
 updateEmbeddedFile();
}

FileStreamExA is inherited from FileStreamEx. To support a virtual directory system, the opened temp file is updated back to its parent when the Flush() method is called.

FileExA and DirectoryExA static classes

FileExA and DirectoryExA is similar to FileEx and DirectoryEx, except they have added support for virtual folders.

DataObjectExA class

DataObjectExA is a DataObject that holds one or more IFileSystemInfoExAs; it's used with the DragDrop.DoDragDrop() (WPF) or WinForm.DoDragDrop() methods; it overrides the GetData() method, which can delay the item expand process till a drop takes place.

The QueryContinueDrag event for the sender must be handled, in WPF and WinForms; it notifies the component when/whether to expand the items. For details, please check the DragDropHelperExA class (or here).

ContextMenuWrapperExA class

ContextMenuWrapperExA is inherited from ContextMenuWrapper; it does similar except for virtual items. It actually displays the context menu for the temp file/directory; expand is delayed until a command is selected and will be invoked (by listening to the BeforeInvokeCommand event).

Image 5

It currently cannot update items back to the virtual file system, and a number of commands are not handled correctly (e.g., delete virtual items).

How to use the component

One of the goals of this component is to provide a similar interface as System.IO.DirectoryInfo, so most operations are basically the same:

Register the archive directory lister

Archive/Bookmarks related code is placed in separate assembly (COFE.Archive), so you have to register them before exa is aware of them.

There is no plug-in system in this component, you have to register the lister before using it. If you don't register the directory lister, it will act like in the previous version, which lists only shell folders and files.

C#
BookmarkDirectoryLister.Register();
ArchiveDirectoryLister.Register();

If you are using an archive specific constructor (e.g., ArchiveInfoExA), the directory lister is registered automatically.

Entity constructor

Each entity class includes a constructor which accepts a string, e.g.:

C#
new DirectoryExA(@"C:\dir");
new FileInfoExA(@"C:\dir\abc.zip");
new ArchiveInfoExA(@"C:\dir\abc,zip");
new ArchiveDirectoryInfoExA(@"C:\dir\abc.zip\cde");
new ArchiveFileInfoExA(@"C:\dir\abc.zip\cde\efg.txt");

The path need not necessarily exist, but its parent must exist, because the parent is construct by FromStringParse().

Besides that, you can use the FileSystemInfoExA.FromStringIterate() and FromStringParse() methods to construct an entity.

C#
(IDirectoryInfoExA)FileSystemInfoExA.FromStringParse(@"C:\Dir");

If you use FromStringXXXX(), the entity must exist, or a FileNotFoundException will be thrown.

Construct a non-existing entity

C#
IFileInfoExA file = new ArchiveFileInfoExA(path);
using (TextWriter w = new StreamWriter(file.Create())) //FileStreamExA.
{
 w.WriteLine("Test");
}

Once you have created a non-existing entity, you can use the Create() method to construct it. For ArchiveInfoExA, it will create a base on its extension; currently, only zip/lzh/lha/7z/sqx are supported. A NotSupportedException will be thrown if Create() is called on other extensions.

Demo program

Image 6

The demo program in version ex, which is a WPF "explorer-like" application, is modified slightly to work with version A; it's a demo only, not a fully finished control. To test the drag drop component, I have also included the WinForms version in the same application (although I haven't coded the drag and drop code for it yet).

DragDropHelperExA (WPF)

DragDropHelperExA is a WPF component that comes with two attached properties: EnableDrag and EnableDrop; you can use these properties on ListView and TreeView to enable support for Drag and Drop.

Although I haven't tested them, DragDropHelperExA should support the M-V-VM pattern; you have to implement the ISupportDrag/ISupportDrop interface in your ViewModels.

Incomplete items

This component is a lot more complicated than the previous one, so a number of features are not complete; they will be added in future releases.

  • Some methods in FileExA/DirectoryExA are missing.
  • Context menu, some commands should be handled by the component (e.g., delete).
  • Custom context menu specified by the entity (or directoryLister).
  • Drag and drop on WinForms.
  • FileSystemWatcherExA/DriveInfoExA is missing.

Issues

  • DragDropHelperExA.EnableDrop will collapse the whole treeview after drop.
  • Drop in FileList will update (refresh) FileList only, and vice versa.
  • WrapFileInfoEx and WrapFileInfoExA have duplicate code.
  • Unmanaged FileSystemInfoEx.PIDL stored in WrapFileSystemInfoEx class, which is used by ArchiveInfoExA.Parent, is shared between assemblies (COFE and COFE.Archives); this may cause an AccessViolationException, and may need to be changed to generate-on-demand, if needed.

Future version (DirectoryInfoExB)

I am hoping to extend version A to provide database support, which will greatly increase the listing performance, add tags/order-by-user support, and enable searching.

A new interface / class to copy (or compress) files asynchronously is also required, because for archives, it's too slow to compress/decompress one file at a time.

References

Version history

  • 01-01-10 - Initial version: 0.1.

License

This article, along with any associated source code and files, is licensed under The GNU Lesser General Public License (LGPLv3)