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:
- Generating zero-size-files/empty-directories first (for acquiring PIDL).
- Start the command. And when needed:
- Expand the file/directory (overwrite those zero-size-files/empty-directories).
- Invoke the selected command.
Index
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.:
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.
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).
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.
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.
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.
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.
To reduce complexity, FileSystemInfoExA
does not inherit from FileSystemInfoEx
. It implements the IFileSystemInfoExA
interface, providing the same properties / methods available.
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.
DirectoryInfoExA
and FileInfoExA
are inherited from WrapFileSystemInfoEx
, which is inherited from FileSystemInfoExA
.
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.
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.
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 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.
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();
.
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.
A number of interfaces have been developed to support FileSystemInfoExA
and its child classes:
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:
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.
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).
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.
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.
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.
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()
.
Changes in virtual directory are reported to their parents by implementing the INotifyFileSystemChanged
interface, as follows:
public enum ModiifiedAction { maRemoved, maAdded, maChanged }
interface INotifyFileSystemChanged
{
void AlertChildIsModified(VirtualFileSystemInfoExA sender,
VirtualFileSystemInfoExA originalSender, ModiifiedAction action);
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":
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
maintains a list of DirectoryLister
s, (e.g., BookmarkDirectoryLister
and ArchiveDirectoryLister
). All operations are called through the library.
private void updateEmbeddedFile()
{
if (accessMode == FileAccess.ReadWrite ||
accessMode == FileAccess.Write)
if (embeddedFile is VirtualFileSystemInfoExA)
{
embeddedFile.Refresh();
if (!embeddedFile.Exists)
throw new IOException("FileSystemInfoExA save failed...");
VirtualFileSystemInfoExA file = embeddedFile as VirtualFileSystemInfoExA;
file.Parent.put(file);
}
}
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
is similar to FileEx
and DirectoryEx
, except they have added support for virtual folders.
DataObjectExA
is a DataObject
that holds one or more IFileSystemInfoExA
s; 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
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).
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).
One of the goals of this component is to provide a similar interface as System.IO.DirectoryInfo
, so most operations are basically the same:
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.
BookmarkDirectoryLister.Register();
ArchiveDirectoryLister.Register();
If you are using an archive specific constructor (e.g., ArchiveInfoExA
), the directory lister is registered automatically.
Each entity class includes a constructor which accepts a string, e.g.:
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.
(IDirectoryInfoExA)FileSystemInfoExA.FromStringParse(@"C:\Dir");
If you use FromStringXXXX()
, the entity must exist, or a FileNotFoundException
will be thrown.
IFileInfoExA file = new ArchiveFileInfoExA(path);
using (TextWriter w = new StreamWriter(file.Create()))
{
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.
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
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.
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.
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.
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
- 01-01-10 - Initial version: 0.1.