QuickZipDev Workshop

Home‎ > ‎Articles‎ > ‎

Rewrite DirectoryInfo using IShellFolder

DirectoryInfo is a class to represent a folder in disk, it's suitable to list file system entries, but it cannot be used to represent a special folder (e.g. virtual folder that doesnt exist in the disk). you may have to use IShellFolder to enumerate these directories. DirectoryInfoEx is written to support these folders.

The project is a rewrite based on Steven Roebert's C# File Browser's code and article.  His code does even more than my project, his article emphasis on how to take advantage of the shell once you have a complete implementation (e.g. context menu, shell drag and drop, preview handler etc), but lack of documentation about how to create one.  I rewrite part of his code (the core part) to learn how it works, and this article explain how to do the basic file operations using IShellFolder interface, in C#.

Because my lack of knowledge, and the nature of DirectoryInfo / FileInfo, the new class is a lot simplier than CShellItem.

Index
  • Obtaining PIDL
  • IShellFolder interface
  • IStorage interface
  • IStream interface
  • My DirectoryInfoEx implementation
  • Demo
  • Performance improvement in version 3

Obtaining PIDL

Folders and Files in shell namespace can be located by PIDL (ITEMIDLIST), just like folder path (or DisplayName), there are Relative PIDL and Full PIDL, just like relative path (abc.txt) and full path (c:\abc.txt).

To obtain PIDL from a string path, you can use Desktop's IShellFolder.ParseDisplayName() (see below)

ShellAPI.SFGAO pdwAttributes = 0;
DesktopShellFolder.ParseDisplayName(IntPtr.Zero, IntPtr.Zero, path, ref pchEaten, out pidlPtr, ref pdwAttributes);
PIDL pidl = new PIDL(pidlPtr, false);

For special directories(e.g. virtual ones like DRIVES, or special file system directories  like PROFILE), CSIDL enum has a list of them, you can use SHGetSpecialFolderLocation() to obtain it's PIDL

int RetVal = ShellAPI.SHGetSpecialFolderLocation(IntPtr.Zero, csidl, out ptrAddr);
if (ptrAddr != IntPtr.Zero)
{
  pidl = new PIDL(ptrAddr, false);
  return pidl;
}


To obtain parent directory's PIDL, one can use PIDL.ILRemoveLastID2() :

IntPtr pParent = PIDL.ILClone(pidl.Ptr); //Clone a pidl
PIDL.ILRemoveLastID2(ref pParent); //Remove last item (like Path.GetDirectoryName())
PIDL pidlParent = new PIDL(pParent, false); //construct the pidl.

* the original PIDL.ILRemoveLastID() is not working correctly.

Obtaining IShellFolder interface

Desktop is the root of all shell namespace folder, you can use SHGetDesktopFolder() to obtain the IShellFolder inferface for Desktop.

IntPtr ptrShellFolder = IntPtr.Zero;
if (ShellAPI.SHGetDesktopFolder(out ptrShellFolder) == ShellAPI.S_OK)
  iShellFolder = (IShellFolder)Marshal.GetTypedObjectForIUnknown(ptrShellFolder, typeof(IShellFolder));


as for other directories (including non-file directory, e.g. MyComputer), you can use BindToObject().

if (Parent.ShellFolder.BindToObject(pidl.Ptr, IntPtr.Zero, ref ShellAPI.IID_IShellFolder, out ptrShellFolder) == ShellAPI.S_OK)
  iShellFolder = (IShellFolder)Marshal.GetTypedObjectForIUnknown(ptrShellFolder, typeof(IShellFolder));

if you have the full pidl, you can use desktop's IShellFolder's BindToObject() directly : 

_desktopShellFolder.BindToObject(_pidlFull, IntPtr.Zero, ref guid, out ptrShellFolder); //then you can construct the shell folder directly.


In Version 3, a new class named ShellFolder is added, which will dispose the pointers automatically when no longer referenced : 

IShellFolder _shellFolder = new ShellFolder(ptrShellFolder); //new class is added to reduce the complexity

IShellFolder interface contains methods to manage the folder, e.g. : 
  • Obtain a list of subitems (sub-folders / sub-files), e.g.
    static ShellAPI.SHCONTF folderflag = ShellAPI.SHCONTF.FOLDERS | ShellAPI.SHCONTF.INCLUDEHIDDEN; /* Specify to include folders only */
       
    if (iShellFolder.EnumObjects(IntPtr.Zero, folderflag, out ptrEnum) == ShellAPI.S_OK) //return the pointer of IEnumIDList
    {
      
    IEnumIDList IEnum = (IEnumIDList)Marshal.GetTypedObjectForIUnknown(ptrEnum, typeof(IEnumIDList));
       IntPtr pidlSubItem;
       int celtFetched;

       while (IEnum.Next(1, out pidlSubItem, out celtFetched) == ShellAPI.S_OK && celtFetched == 1) 
           dirList.Add(new PIDL(pidlSubItem, false));       
          /*  Add PIDL of each subdirectory to dirList, noted that in normal case you should release pidlSubItem
           *  but Steven Roebert's PIDL class is a IDisposable class which will dispose it for you.                                                 
           *  Also noted that the pidl is relative instead of full. Use GetDisplayNameOf() (see below) to get it's name. */

       if (IEnum != null) //Release resource
       {                       
          Marshal.ReleaseComObject(IEnum);
          Marshal.Release(ptrEnum);
        }
      }

  • Convert PIDL back to readable path using GetDisplayNameOf() :
    IntPtr ptrStr = Marshal.AllocCoTaskMem(ShellAPI.MAX_PATH * 2 + 4);
    Marshal.WriteInt32(ptrStr, 0, 0);
    StringBuilder buf = new StringBuilder(ShellAPI.MAX_PATH);

    try
    {
      /*  uflags is a SHGNO enum that allow you to get different folder names,
       *  e.g. "My Documents"(SHGNO.NORMAL) folder is named as "Documents"(SHGNO.FORPARSING) in file system.
       * StrRetToBuf() convert STRRET structure to buffer usable by StringBuilder              */
      if (iShellFolder.GetDisplayNameOf(pidl, uFlags, ptrStr) == ShellAPI.S_OK)
        ShellAPI.StrRetToBuf(ptrStr, pidl, buf, ShellAPI.MAX_PATH);

    }
    finally
    {
      if (ptrStr != IntPtr.Zero)
        Marshal.FreeCoTaskMem(ptrStr);
      ptrStr = IntPtr.Zero;
    }
    Console.WriteLine(buf.ToString());


  • Retrieve IShellFolder / IStorage interface for a subfolder :

    Using SHBindToParent()
    :
    IntPtr pidlLast = IntPtr.Zero;
    retVal = ShellAPI.SHBindToParent(dir.PIDLRel.Ptr, ShellAPI.IID_IStorage, out storagePtr, ref pidlLast);
    Or BindToStorage() :
    retVal = dir.Parent.ShellFolder.BindToStorage(
                        dir.PIDLRel.Ptr, IntPtr.Zero, ref ShellAPI.IID_IStorage,
                        out storagePtr);
    /* Beside IID_IStorage interface, there is IID_IStream and IID_IPropertySetStorage as well. */

    if ((retVal == ShellAPI.S_OK))
    {
      IStorage storage = (IStorage)Marshal.GetTypedObjectForIUnknown(storagePtr, typeof(IStorage));
      /* Your work here, free the pointer and interface when done. */
    }

    In Version 3, a new class named Storage is added, which will dispose the pointers automatically when no longer referenced : 

    IStorage _storage = new Storage(storagePtr); //new class is added to reduce the complexity


IStorage interface contains methods for creation or manage items / subitems in the folder, e.g..

  • Rename, move or copy files, using pidl as parameter. e.g.
    SrcStorage.MoveElementTo(SourceFileName, DestStorage,
       DestFilename, ShellAPI.STGMOVE.MOVE) != ShellAPI.S_OK)

  • Delete files
    ParentStorage.DestroyElement(name);

  • Read / Write files contents
    /* FileStreamEx class is a customized IDisposable Stream class, which uses IStream interface of a file. */
    FileStreamEx stream =
    new FileStreamEx(path, mode, access);
    StreamReader sr = new StreamReader(stream);
    Console.WriteLine(sr.ReadToEnd());    


IStream interface allow you to read / write data to stream objects.
  • To obtain the IStream interface, use OpenStream() (Read/Write) or CreateStream() (Create New)
    if (parentStorage.OpenStream(filename, IntPtr.Zero, grfmode, 0, out streamPtr) == ShellAPI.S_OK)
      stream = (IStream)Marshal.GetTypedObjectForIUnknown(streamPtr, typeof(IStream));


  • IStream contains methods like seek(), read(), write().
  • If the stream object is released, it's consider closed.

All interface object should be release when done.

My DirectoryInfoEx implementation


The implementation is simplier than CShellItem, however, as FileSystemInfoEx's PIDL is exposed (via PIDLRel and PIDL property), you can implement custom operation (e.g. Extract Icon, Context Menu) externally. IShellFolder and IStorage (generate on demand) is also exposed in DirectoryInfoEx, they are automatically destroyed when disposed, do not free them yourself.

Both DirectoryInfoEx and FileInfoEx is inherited from FileSystemInfoEx, they are used as enumeration (listing) subitems, DirectoryInfoEx contains GetFiles() and GetDirectories() method for this purpose. To modify a file, use FileEx class for managing files (DirectoryEx is not implemented yet, use System.IO.Directory at this time), and FileStreamEx class for read/write files.

DirectoryInfoEx (and FileInfoEx)'s constructor accept a Path or PIDL. A number of special directories is defined in DirectoryInfoEx, including DesktopDirectory, MyComputerDirectory, CurrentUserDirectory, SharedDirectory and NetworkDirectory. For other special directories, you can obtain it's PIDL by calling DirectoryInfoEx.CSIDLtoPIDL().

And a demo


This demo is a simple WPF application that list the subdirectories below Desktop. The Icons are obtained using SHGetFileInfo(), which takes a full PIDL parameter.

Performance improvement in version 3

I have rewrite the component which allow to construct DirectoryInfoEx without iterate from Desktop, lets say c:\temp\1, last version will iterate from Desktop -> Drives -> c:\\ -> temp -> 1, this version will construct it directly, this performance gain is not significant when browsing from desktop (should be as fast thou, because old constructor is used when browsing sub items), but it should be much faster if loading a random directory, plus less memory usage.
.
Beside that, I have added a cache, which is useful when refreshing directory contents.  Instead of construct the subdirectory directly, it will try to reuse if created ones in cache. This feature is similar to the one used in Jim Parsells's work. 

PIDL subPidl = new PIDL(pidlSubItem, false);
if (!listContains(_cachedDirList, subPidl))
    _newDirList.Add(new DirectoryInfoEx(this, subPidl));
listRemove(_remDirList, subPidl);
...
//dirList contains cache
foreach (DirectoryInfoEx dir in _remDirList)
    listRemove(dirList, dir.PIDLRel);
foreach (DirectoryInfoEx dir in _newDirList)
    dirList.Add(dir);


References

History

08-22-09 version 0.1 
- Initial version 

08-23-09 version 0.2
- Demo updated.

11-01-09 version 0.3
- Demo no longer load Network contents, edit the converter to disable this change.
- DirectoryEx (static class) added.
- PIDL class is now IDisposable and free automatically now. Also added new - internal classes ShellFolder and Storage which do the same.
- Performance improved, no longer construct from desktop directory. (see above)
- DirectoryInfoEx and FileInfoEx is now serializable.

11-01-09 Version 0.4
- Fixed Cache not working.

11-04-09 Version 0.5
- DirectoryInfoEx/FileInfoEx works even if the path specified is not exists (Exists == false, you have to call Create() or Refresh() before using it).
- Refresh(), Create(), MoveTo(), Delete(), CreateSubdirectory() Open() and related instance method added.
- Constructor support Environment path (e.g. %temp%)
- Test project.

11-05-09 Version 0.6
- Context menu support (ContextMenuWrapper)
- Demo updated (Context menu)
- FileSystemWatcherEx class added.
- Fixed FileInfoEx created by EnumFiles()(which used by GetFiles() and GetFileSystemInfos()) return incorrect Parent directory.

11-08-09 Version 0.7
- Fixed Root of all FileInfoEx equals to c:\Users\{User}\Desktop instead of a GUID.
- Demo updated (Context menu multiselected)

11-08-09 Version 0.8
- Fixed unable Rename item in same directory.
- Fixed ContextMenuWrapper dont return OnHover message on popup.
- Added QueryMenuItemsEventArgs.Command, return properly for user query items.
- Demo updated (added statusbar)


Attachments (1)