Click here to Skip to main content
Click here to Skip to main content

Using Vista Preview Handlers in a WPF Application

, 22 Apr 2008 CPOL
Rate this:
Please Sign up or sign in to vote.
This article is about how to use Windows Vista Preview handlers within a WPF application

Introduction

First of all, what is a preview handler? Preview handler is a COM object, that is called when you want to display the preview of your item. In 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 our WPF application? Probably we can. Let’s see how we can do it.

Let's Do It

Let's create a simple WPF window that displays a file list from the left and preview of items on the right side. We'll use a simple file list string collection as our datasource, bind it to Listbox Items and then bind the 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 with changes of the file system. So, this is a very good chance to use the 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 a simple DataTemplate, we can see the file list in the left pane of our application. It will be updated automatically upon file change in a certain directory.

The next step is to understand how to use preview handlers within a custom application. After all, a preview handler is a regular COM object that implements the 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 the preview handler to a specific file type, all we have to do is to look into HKEY_CLASSES_ROOT and find COM Guid of the 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 file. 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 the appropriate COM instance for the 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 its 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 a handle to the window we want the handler to sit in. We should also provide bounds of region of the window to the handler to 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, the 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 listen to its ActualHeight and ActualWidth properties. First get the window handler (it won't be changed during the application life cycle), then update the layout of our WPF preview handler for the 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 attach the preview handlers for the displayed file to the control:

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

We're done. The last thing to do is to implement the IStream interface in our COMStream C# class in order to be able to load streaming content (for example, for the 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're finished. We can use unmanaged preview handlers to display content of our files, held by the 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.

History

  • 22nd April, 2008: Initial post

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Tamir Khason
Architect Better Place
Israel Israel
Hello! My name is Tamir Khason, and I am software architect, project manager, system analyst and [of course] programmer. In addition to writing big amount of documentation, I also write code, a lot of code. I used to work as a freelance architect, project manager, trainer, and consultant here, in Israel, but recently join the company with extremely persuasive idea - to make a world better place. I have very pretty wife and 3 charming kids, but unfortunately almost no time for them.
 
To be updated within articles, I publishing, visit my blog or subscribe RSS feed. Also you can follow me on Twitter to be up to date about my everyday life.

Comments and Discussions

 
QuestionPDF Preview PinmemberMember 410297025-Aug-11 22:53 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Terms of Use | Mobile
Web03 | 2.8.1411022.1 | Last Updated 22 Apr 2008
Article Copyright 2008 by Tamir Khason
Everything else Copyright © CodeProject, 1999-2014
Layout: fixed | fluid