Friday, April 18, 2008

Using Vista Preview Handlers in WPF application

First of all what is Preview Handler? Preview Handler is COM object, that called when you want to display the preview of your item. Other words, Preview Handlers are lightweight, rich and read-only previews of file’s content in a reading pane. You can find preview handlers in Microsoft Outlook 2007, Windows Vista and, even sometimes in XP. Can we use preview handlers within your WPF application? Probably we can. Let’s see how we can do it.

 image

Let’s create simple WPF window, that displays file list from left and preview of items in right side. We’ll use simple file list string collection as our datasource, bind it to Listbox Items and then bind selected item to some contentpresenter. I blogged about this approach earlier.

<Grid DataContext={StaticResource files}>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width=".2*"/>
            <ColumnDefinition Width=".8*"/>
        </Grid.ColumnDefinitions>
        <ListBox ItemsSource={Binding} IsSynchronizedWithCurrentItem="True" />
        <ContentPresenter Grid.Column=”1” Content={Binding Path=/}/>
        <GridSplitter Width="5"/>
    </Grid>

Our data source should be updated automatically within changes of file system. So, this is very good chance to use FileSystemWatcher object.

class ListManager:ThreadSafeObservableCollection<string>
    {
        string dir = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
        public ListManager()
        {
            FileSystemWatcher fsw = new FileSystemWatcher(dir);
            fsw.NotifyFilter = NotifyFilters.FileName | NotifyFilters.Size | NotifyFilters.LastWrite;
            fsw.Created += new FileSystemEventHandler(fsw_Created);
            fsw.Deleted += new FileSystemEventHandler(fsw_Deleted);

            fsw.EnableRaisingEvents = true;

            string[] files = Directory.GetFiles(dir);
            for (int i = 0; i < files.Length; i++)
            {
                base.Add(files[i]);
            }

        }

        void fsw_Deleted(object sender, FileSystemEventArgs e)
        {
            base.Remove(e.FullPath);
        }

        void fsw_Created(object sender, FileSystemEventArgs e)
        {
            base.Add(e.FullPath);
        }
    }

Now, after applying simple DataTemplate, we can see file list in the left pane of our application. It will be updated automatically upon files change in certain directory.

Next step is to understand how to use Preview Handlers within custom application. After all, preview handler is regular COM object, that implements following interfaces

[ComImport]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
[Guid("8895b1c6-b41f-4c1c-a562-0d564250836f")]
interface IPreviewHandler
{
    void SetWindow(IntPtr hwnd, ref RECT rect);
    void SetRect(ref RECT rect);
    void DoPreview();
    void Unload();
    void SetFocus();
    void QueryFocus(out IntPtr phwnd);
    [PreserveSig]
    uint TranslateAccelerator(ref MSG pmsg);
}

[ComImport]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
[Guid("b7d14566-0509-4cce-a71f-0a554233bd9b")]
interface IInitializeWithFile
{
    void Initialize([MarshalAs(UnmanagedType.LPWStr)] string pszFilePath, uint grfMode);
}

[ComImport]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
[Guid("b824b49d-22ac-4161-ac8a-9916e8fa3f7f")]
interface IInitializeWithStream
{
    void Initialize(IStream pstream, uint grfMode);
}

In order to find and attach preview handler to specific file type, all we have to do is to look into HKEY_CLASSES_ROOT and find COM Guid of preview handler (8895b1c6-b41f-4c1c-a562-0d564250836f). The default value of this key will be the Guid of COM object, that actually can preview this type of files. Let’s do it

string CLSID = "8895b1c6-b41f-4c1c-a562-0d564250836f";
            Guid g = new Guid(CLSID);
            string[] exts = fileName.Split('.');
            string ext = exts[exts.Length - 1];
            using (RegistryKey hk = Registry.ClassesRoot.OpenSubKey(string.Format(@".{0}\ShellEx\{1:B}", ext, g)))
            {
                if (hk != null)
                {
                    g = new Guid(hk.GetValue("").ToString());

Now, we know, that this file can be previewed, thus let’s initialize appropriate COM instance for preview handler

Type a = Type.GetTypeFromCLSID(g, true);
object o = Activator.CreateInstance(a);

There are two kinds of initializations for preview handlers – file and stream based. Each one has it’s own interface. So, we can only check if the object created implements this interface to be able to initialize the handler

IInitializeWithFile fileInit = o as IInitializeWithFile;
IInitializeWithStream streamInit = o as IInitializeWithStream;

bool isInitialized = false;
if (fileInit != null)
{
   fileInit.Initialize(fileName, 0);
   isInitialized = true;
  }
else if (streamInit != null)
  {
    COMStream stream = new COMStream(File.Open(fileName, FileMode.Open));
     streamInit.Initialize((IStream)streamInit, 0);
     isInitialized = true;
  }

After we initialized the handler we can set handle to the window we want the handler to sit in. Also we should provide bounds of region of the window to handler be placed in.

if (isInitialized)
                    {
                        pHandler = o as IPreviewHandler;
                        if (pHandler != null)
                        {
                            RECT r = new RECT(viewRect);
                            pHandler.SetWindow(handler, ref r);
                            pHandler.SetRect(ref r);

                            pHandler.DoPreview();
                        }
                    }

So far so good, but we’re in WPF. Thus ContentPresenter we’re using has no handle! That’s right, but the main WPF application window has. So, let’s first get the main application window handle, then create rectangle bounds of the region, occupied by ContentControl.

In order to do it, we’ll derive from ContentPresenter and will listen to ActualtHeight and ActualeWidth property of it. First get the window handler (it wont be changed during the application life cycle), then update layout of our WPF Preview Handler for region and bounds of the control.

class WPFPreviewHandler : ContentPresenter
    {
        IntPtr mainWindowHandle = IntPtr.Zero;
        Rect actualRect = new Rect();

        protected override void OnPropertyChanged(DependencyPropertyChangedEventArgs e)
        {
            if (e.Property == ContentControl.ActualHeightProperty | e.Property == ContentControl.ActualWidthProperty)
            {
                if (mainWindowHandle == IntPtr.Zero)
                {
                    HwndSource hwndSource = PresentationSource.FromVisual(App.Current.MainWindow) as HwndSource;
                    mainWindowHandle = hwndSource.Handle;
                }
                else
                {
                    Point p0 = this.TranslatePoint(new Point(),App.Current.MainWindow);
                    Point p1 = this.TranslatePoint(new Point(this.ActualWidth,this.ActualHeight),App.Current.MainWindow);
                    actualRect = new Rect(p0, p1);
                    mainWindowHandle.InvalidateAttachedPreview(actualRect);
                }
            }

public static void InvalidateAttachedPreview(this IntPtr handler, Rect viewRect)
        {
            if (pHandler != null)
            {
                RECT r = new RECT(viewRect);
                pHandler.SetRect(ref r);
            }
        }

Now, the only thing we have to do is to listen for ContentProperty change and attache the preview handlers for displayed file to the control

if (e.Property == ContentControl.ContentProperty)
            {
                mainWindowHandle.AttachPreview(e.NewValue.ToString(),actualRect);
            }

We done. Last thing to do is to implement IStream interface in our COMStream C# class in order to be able to load streaming content (for example for PDF previewer)

public sealed class COMStream : IStream, IDisposable
{
     Stream _stream;

     ~COMStream()
     {
         if (_stream != null)
         {
             _stream.Close();
             _stream.Dispose();
             _stream = null;
         }
     }

     private COMStream() { }

     public COMStream(Stream sourceStream)
     {
         _stream = sourceStream;
     }

     #region IStream Members

     public void Clone(out IStream ppstm)
     {
         throw new NotSupportedException();
     }

     public void Commit(int grfCommitFlags)
     {
         throw new NotSupportedException();
     }

     public void CopyTo(IStream pstm, long cb, IntPtr pcbRead, IntPtr pcbWritten)
     {
         throw new NotSupportedException();
     }

     public void LockRegion(long libOffset, long cb, int dwLockType)
     {
         throw new NotSupportedException();
     }

     [SecurityCritical]
     public void Read(byte[] pv, int cb, IntPtr pcbRead)
     {
         int count = this._stream.Read(pv, 0, cb);
         if (pcbRead != IntPtr.Zero)
         {
             Marshal.WriteInt32(pcbRead, count);
         }
     }

     public void Revert()
     {
         throw new NotSupportedException();
     }

     [SecurityCritical]
     public void Seek(long dlibMove, int dwOrigin, IntPtr plibNewPosition)
     {
         SeekOrigin origin = (SeekOrigin)dwOrigin;
         long pos = this._stream.Seek(dlibMove, origin);
         if (plibNewPosition != IntPtr.Zero)
         {
             Marshal.WriteInt64(plibNewPosition, pos);
         }
     }

     public void SetSize(long libNewSize)
     {
         this._stream.SetLength(libNewSize);
     }

     public void Stat(out System.Runtime.InteropServices.ComTypes.STATSTG pstatstg, int grfStatFlag)
     {
         pstatstg = new System.Runtime.InteropServices.ComTypes.STATSTG();
         pstatstg.type = 2;
         pstatstg.cbSize = this._stream.Length;
         pstatstg.grfMode = 0;
         if (this._stream.CanRead && this._stream.CanWrite)
         {
             pstatstg.grfMode |= 2;
         }
         else if (this._stream.CanWrite && !_stream.CanRead)
         {
             pstatstg.grfMode |= 1;
         }
         else
         {
             throw new IOException();
         }

     }

     public void UnlockRegion(long libOffset, long cb, int dwLockType)
     {
         throw new NotSupportedException();
     }

     [SecurityCritical]
     public void Write(byte[] pv, int cb, IntPtr pcbWritten)
     {
         this._stream.Write(pv, 0, cb);
         if (pcbWritten != IntPtr.Zero)
         {
             Marshal.WriteInt32(pcbWritten, cb);
         }
     }

     #endregion

     #region IDisposable Members

     public void Dispose()
     {
         if (this._stream != null)
         {
             this._stream.Close();
             this._stream.Dispose();
             this._stream = null;
         }
     }

     #endregion
}

And now we finished. We can use unmanaged preview handlers to display content of our files, hold by WPF application. Also, if you want, you can create your own preview handlers and they’ll appear in your WPF application as well as they’ll magically appear in Outlook. Following full source code for this article

Source code for this article >>

Good day, Happy Passover and, as always, be good people.

2 comments:

Rainer said...

Should this work on a Windows XP professional machine, or only Vista? I can build the project under XP but not test it. It gives me a few 'No such COM Interface' errors.

Tamir Khason said...

It's possible to make it working on XP, however you should implement preview handler host first. See http://thecodetrip.com/1/codegallery-comserver for more information