Search code examples
c#.netcomthumbnailsshell-extensions

managed shell Thumbnail provider doesn't get called


I am trying to write handler for Windows COM IThumbnailProvider interface, to generate thumb images for custom file type (.URLtest to test things). Using C# .Net.
No matter what I try, my ThumbnailProvider class / handler dll is never called (based on my simple text file logging). And my .URLtest files are displayed in Explorer like they do not have thumbnail provider assigned.

I am testing this under Windows 7 64
I compiled the Thumbnail provider lib as both x86 and x64
Registered each version with right RegAsm
"C:\Windows\Microsoft.NET\Framework\v4.0.30319\RegAsm.exe" ThumbUrlThumbnailProvider.dll /codebase
"C:\Windows\Microsoft.NET\Framework64\v4.0.30319\RegAsm.exe" ThumbUrlThumbnailProvider.dll /codebase
That created all these registry data

[HKEY_CLASSES_ROOT]

  [ThumbUrlHandler.ThumbnailProvider]
    @="ThumbUrlHandler.ThumbnailProvider"
    [CLSID]
      @="{E7CBDB01-06C9-4C8F-A061-2EDCE8598F99}"

  [CLSID]
    [{E7CBDB01-06C9-4C8F-A061-2EDCE8598F99}]
      @="ThumbUrlHandler.ThumbnailProvider"
      DisableProcessIsolation=1                          <<< added by me as one attempt to make things work

      [Implemented Categories]
        [{62C8FE65-4EBB-45E7-B440-6E39B2CDBF29}]

      [InprocServer32]
        @="mscoree.dll"
        "ThreadingModel"="Both"
        "Class"="ThumbUrlHandler.ThumbnailProvider"
        "Assembly"="ThumbUrlThumbnailProvider, Version=0.1.0.0, Culture=neutral, PublicKeyToken=bb72a5681544afed"
        "RuntimeVersion"="v4.0.30319"
        "CodeBase"="file:///W:/bin/x64/Debug/ThumbUrlThumbnailProvider.dll"

        [0.1.0.0]
          "Class"="ThumbUrlHandler.ThumbnailProvider"
          "Assembly"="ThumbUrlThumbnailProvider, Version=0.1.0.0, Culture=neutral, PublicKeyToken=bb72a5681544afed"
          "RuntimeVersion"="v4.0.30319"
          "CodeBase"="file:///W:/bin/x64/Debug/ThumbUrlThumbnailProvider.dll"

      [ProgId]
        @="ThumbUrlHandler.ThumbnailProvider"

  [Record]
    [{40FD8D55-6F91-3A5C-A049-6664E649643C}]             <<< no idea why this is registered
      [0.1.0.0]
        "Class"="ThumbUrlHandler.WTS_ALPHATYPE"
        "Assembly"="ThumbUrlThumbnailProvider, Version=0.1.0.0, Culture=neutral, PublicKeyToken=bb72a5681544afed"
        "RuntimeVersion"="v4.0.30319"
        "CodeBase"="file:///W:/bin/x64/Debug/ThumbUrlThumbnailProvider.dll"

I created file type registry entry

[HKEY_CLASSES_ROOT]
  [.URLtest]
    "PerceivedType"="Image"
    "Content Type"="image/jpeg"

    [ShellEx]
      [{e357fccd-a995-4576-b01f-234630154e96}]              <<<< IThumbnailProvider COM interface CLSID
        @="{E7CBDB01-06C9-4C8F-A061-2EDCE8598F99}"          <<<< my ThumbnailProvider class CLSID

My simple test thumb provider code

// COM interfaces
public enum WTS_ALPHATYPE
{ .. }

[ComVisible(true)]
[Guid("e357fccd-a995-4576-b01f-234630154e96")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface IThumbnailProvider
{
    void GetThumbnail(int cx, out IntPtr hBitmap, out WTS_ALPHATYPE bitmapType);
}

[ComVisible(true)]
[Guid("b824b49d-22ac-4161-ac8a-9916e8fa3f7f")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface IInitializeWithStream
{
    void Initialize(IStream stream, int grfMode);
}

// my thumbnail provider class
[ComVisible(true), ClassInterface(ClassInterfaceType.None)]
[ProgId("ThumbUrlHandler.ThumbnailProvider")]
[Guid("E7CBDB01-06C9-4C8F-A061-2EDCE8598F99")] 
public class ThumbnailProvider : IThumbnailProvider, IInitializeWithStream
{
    public void Initialize(IStream stream, int grfMode)
    {
        // instantly dispose the COM stream, as I don't need it for the test
        Marshal.ReleaseComObject(stream);
    }

    public void GetThumbnail(int cx, out IntPtr hBitmap, out WTS_ALPHATYPE bitmapType)
    {
        bitmapType = WTS_ALPHATYPE.WTSAT_RGB;
        var outBitmap = new Bitmap(cx, cx, System.Drawing.Imaging.PixelFormat.Format24bppRgb);
        hBitmap = outBitmap.GetHbitmap();
    }
}

I tried restarting Explorer process.
I tried creating new .URLtest files to get around thumbnails caching.
My handler is still ignored.
After several Explorer restart I noticed that now my x64 ThumbUrlThumbnailProvider.dll is locked by Explorer. But still no thumbnails show for the files and ThumbnailProvider methods are not called.

Does anybody know if this interface and approach is still supported in Windows ?
Does anybody see any error in my code or registry data ?
I am out of ideas.


Solution

  • I managed to make my shell extension to work. This is not really an answer, but at least a collection of things I have learnet, that may help others.

    There were 2 main problems:

    1. for part of development there was probably an error in registration of my file type or dll, so it was really not being recognized and called by shell.
    2. when this was fixed, the code was called, but was crashing silently.
      Shell runs your code in isolated process, and it does not have access to file system (and probably much anything else either). If you'll try to access it, for example to write a debug log file, your code will crash silently.
      The same will happen if you'll try to use Trace listener that writes to log file (like System.Diagnostics.TextWriterTraceListener)!

    So there isn't really an easy way to detect if your code was even run by shell, and verify if your registry registrations part is right.
    I tried to attach Explorer process to VisualStudio, but my breakpoint was never hit, even though my extension code was run.
    What you can do is opt out of "Process isolation" in your assembly registration. See my question for DisableProcessIsolation=1 param and context. Than your assembly will be loaded by Explorer process directly and you can see that in sysinternals Process Explorer utility. If your registrations are right.
    This may require Explorer process restart to work.
    If you do not opt out, your assembly will be loaded for only a brief time and than uloaded again, so you will not be able to spot it, or as I read elsewhere, it may be loaded by other process than Explorer.exe entirely.

    Few other things I learned:

    • try catch blocks in my code for some reason did not catch exceptions. The code still crashed and thumbnail provider did not work.
    • no errors were reported in windows event logs
    • you can compile your assembly as AnyCPU, and register it with both 32 and 64bit RegAsm. It does work at least for 64bit Explorer (Windows). I did not yet test if 32bit processes can run it too though.
    • Some code that worked when my dll was called by test app did not work when same dll was called by shell. For example IStream wrapper.
    • There is a problem with shell not releasing file locks when they are passed to managed shell extensions. You can read more about it in this stack question and this blog post. The blog mentions there is no official solution and managed shell extensions are officialy not supported by Microsoft.
      My dirty solution is loading whole file content to sparate MemoryStream (that keep no reference to the otiginal file or IStream) and calling Marshal.ReleaseComObject() in Initialize() method and not keeping any reference to the original IStream pointer.
      Simon Mourier mentions in his comment above that using Marshal.ReleaseComObject() is not a good idea, but that was the only solution I was able to find.
    • Which ever way you solve this, do not get rid of your file data / stream access in first GetThumbnail() method call. Shell may keep your thumbnail provider instance and call GetThumbnail() more times, for different thumbnail sizes atc.