Про компанію Послуги Портфоліо Підтримка Відгуки клієнтів Контакнта інформація ТОВ Брутка: розробка програмного забеспечення та створення сайтів

Архів новин


Vista and Office: View Data Your Way With Our Managed Preview Handler Framework

Every version of Microsoft Windows and Office brings new methods and approaches for improving your ability to see, interact with, and understand data. Likewise, each version also presents new and improved extensibility points for plugging in custom functionality. In Windows Vista™ and in the 2007 Microsoft® Office system, these two areas of advancements have merged, giving you the ability to write custom preview handlers for Windows Vista and for Outlook® 2007.
Outlook 2003 provided a reading pane for e-mail that made it easy to view the contents of a message without having to open it. You simply selected the message in the mail folder's list view, and the message was rendered in a side or bottom window. Outlook 2007 extends this concept by allowing you to view message attachments in that same preview pane, without having to double-click an attachment to open it in the appropriate viewer.
Out of the box, Outlook includes preview handlers for Word documents, PowerPoint® presentations, Excel spreadsheets, font files, video and audio files, and a variety of other file types that are commonly sent as attachments. But that's only the beginning. Windows Vista supports a similar preview pane that is accessible from any folder in the shell. You can enable the preview pane by selecting Organize | Layout | Preview Pane from the folder's menu.
Both Outlook and Windows Vista subscribe to the same underlying preview mechanism, and they allow developers to implement custom preview handlers for any file type, register them, and instantly gain preview capabilities for those file types in Outlook 2007 and Windows Vista.
In this article, I'll explain what is required to implement a preview handler and discuss how to do so using managed code (the Windows® SDK for Vista includes a sample preview handler written in native C++). The code download for this article includes a framework that makes it a snap to implement your own preview handlers, and it provides several sample previewers (including previewers for PDF, XML, ZIP, MSI, BIN, CSV, XPS, and XAML files).
Hosting a Preview Handler
Those of you who have attempted to implement managed add-ins for the Windows shell in the past might be a bit uneasy about this concept. After all, Microsoft strongly advises that shell add-ins not be implemented in managed code, and such add-ins are not considered a supported scenario. This is because add-ins are loaded in-process into the shell (explorer.exe), only one version of the common language runtime (CLR) can be loaded into a given process, and managed code built against one version of the runtime may not run in a process that is running an earlier version of the runtime. What happens, then, if you have two shell add-ins that are both written in managed code-one targeting the .NET Framework 1.1 and one targeting the .NET Framework 2.0? If the 2.0-targeted add-in is loaded first, you probably won't notice any problems; the 1.1 add-in should load and run successfully against the CLR 2.0. But if the 1.1-targeted add-in loads first, the .NET Framework 1.1 CLR will load into explorer.exe. Assemblies targeting the .NET Framework 2.0 cannot be loaded by the .NET Framework 1.1, and therefore the 2.0-targeted add-in will fail when loaded after the 1.1-targeted add-in.
The same versioning problems exist with Windows Vista, so Microsoft still advises that shell add-ins shouldn't be implemented in managed code-even for new extensibility points within the shell, such as thumbnail providers and property handlers (which are used out-of-process by the Windows Vista search indexer but in-process by the Windows Vista shell).
There is good news, however, regarding preview handlers: they are always loaded out-of-process, at least as far as the shell is concerned. Preview handlers are implemented as COM components, but they're not hosted by the Windows Vista shell. Instead, a preview handler is either hosted by the preview handler surrogate host (prevhost.exe) or implemented as a local COM server. For managed code, implementing the latter is well beyond the scope of this article (for an overview of what's involved, see the "Preview Handlers and Windows XP" sidebar by Ryan Gregg). Besides, using prevhost.exe is the preferred, Microsoft-recommended service model.

The PreviewHandler Framework
To be a valid preview handler, several interfaces must be implemented (all of which are documented at windowssdk.msdn.microsoft.com/aa361576.aspx). This includes IPreviewHandler (shobjidl.h); IInitializeWithFile, IInitializeWithStream, or IInitializeWithItem (propsys.h); IObjectWithSite (ocidl.h); and IOleWindow (oleidl.h). There are also optional interfaces, such as IPreviewHandlerVisuals (shobjidl.h), that a preview handler can implement to provide extended support.
If you were to write a preview handler in native code, you could dive right in, given that all of these interfaces are already defined and ready for inclusion from the header files I just noted parenthetically. However, to write a preview handler in managed code, you must first write or obtain COM interop definitions for each of these interfaces.
In my managed framework for implementing preview handlers, most of the functionality for these interfaces is wrapped up into an abstract base class, called PreviewHandler:
public abstract class PreviewHandler :
IPreviewHandler, IPreviewHandlerVisuals, IOleWindow, IObjectWithSite
{ ... }
This hides all of the gory implementation details (which, as you'll see, aren't really gory at all). Note, however, that neither IInitializeWithFile nor IInitializeWithStream are implemented by PreviewHandler. Those honors are bestowed upon two abstract classes that derive from PreviewHandler:
public abstract class StreamBasedPreviewHandler :
PreviewHandler, IInitializeWithStream { ... }
public abstract class FileBasedPreviewHandler :
PreviewHandler, IInitializeWithFile { ... }
To implement a custom preview handler, you derive a new class from StreamBasedPreviewHandler or FileBasedPreviewHandler and override one method. Here's the implementation from my XmlPreviewHandler class:
public sealed class XmlPreviewHandler : FileBasedPreviewHandler
{
protected override PreviewHandlerControl
CreatePreviewHandlerControl()
{
return new XmlPreviewHandlerControl();
}
}
The CreatePreviewHandlerControl method returns an instance of a custom type that you write derived from either StreamBasedPreviewHandlerControl or FileBasedPreviewHandlerControl. Both derive from my PreviewHandlerControl abstract base class:
public abstract class FileBasedPreviewHandlerControl :
PreviewHandlerControl { ... }
public abstract class StreamBasedPreviewHandlerControl :
PreviewHandlerControl { ... }
public abstract class PreviewHandlerControl : Control
{
public abstract void Load(FileInfo file);
public abstract void Load(Stream stream);
public virtual void Unload() { ... }
}
As its name implies, the Load method is called when a file or stream should be loaded and previewed. Likewise, the Unload method is called when the current preview should be torn down. A custom PreviewHandlerControl is then responsible for deriving from the appropriate type (either FileBasedPreviewHandlerControl or StreamBasedPreviewHandlerControl), overriding the Load method, and creating whatever Windows Forms controls are needed to render the file or stream. For my XML preview handler, I simply create a WebBrowser control and load the XML document into it, giving Windows Vista shell users the same pretty-printing of XML that is provided by Internet Explorer®:
public class XmlPreviewHandlerControl : FileBasedPreviewHandlerControl
{
public override void Load(FileInfo file)
{
WebBrowser browser = new WebBrowser();
browser.Dock = DockStyle.Fill;
browser.Navigate(file.FullName);
Controls.Add(browser);
}
}
The base PreviewHandlerControl's Unload implementation disposes and clears all controls from the Controls collection. If that functionality is appropriate for your control, there's no need to override it.
Aside from applying a few attributes to the derived PreviewHandler class, that's all there is to writing a custom preview handler. Figure 3 shows the complete implementation of a ZIP preview handler, the results of which are shown in Figure 4, running in Outlook 2007 (the download includes a richer ZIP preview handler that sports a tree view of the files and directories within the .zip, file icons, and double-click support for viewing the files contained in the archive). This class uses the Visual J#® 2005 ZIP library (included in the .NET Framework 2.0) to provide a list of all the files contained in the ZIP file. To demonstrate that these preview handlers do work in both Windows Vista and Outlook, Figure 5 shows the XmlPreviewHandler running in the Windows Vista shell.
Implementing PreviewHandler
Now that you know how to implement a managed preview handler using my framework, let's take a look at the guts of the PreviewHandler base class to better understand how preview handlers work.
At its core, PreviewHandler is simply a container for a Windows Forms control, which is stored in a private member variable, _previewControl. It is initialized to the control returned from the GetPreviewHandlerControl method implemented by the custom preview handler implementation (for example, XmlPreviewHandlerControl or ZipPreviewHandlerControl):
protected PreviewHandler()
{
_previewControl = CreatePreviewHandlerControl();
IntPtr forceCreation = _previewControl.Handle;
}
While this is a very small constructor, there are several subtle issues worth pointing out. The first is that I'm violating an important .NET Framework design guideline, which at a high level says that constructors should not call virtual methods (this will be caught by the FxCop rule ConstructorsShouldNotCallBaseClassVirtualMethods). In .NET, unlike in ISO C++, the target of a virtual call will be on the most derived type (the override) rather than on the base type currently being constructed (the virtual). The problem here is that at the time when the base type's constructor is executing, the derived type's constructor has not yet executed. This means the method override will be called on the derived instance before that instance's construction has been completed, hence the rule that constructors should not call virtual methods that haven't been sealed in the same class. Keep this in mind, as it means you shouldn't do anything in your override of CreatePreviewHandlerControl that relies on anything set up in the constructor (that construction code will not have run yet). Based on the framework I've created, however, where CreatePreviewHandlerControl should simply be instantiating and returning the correct control type, this rule shouldn't be difficult to follow.
Of course, this begs the question, if I knew about this guideline in the first place, why did I break it by calling CreatePreviewHandlerControl from the constructor? Because I need to create the Control on the VI thread. This leads to the second issue I want to address. At the end of the constructor shown previously, you'll notice that I've retrieved the value of _previewControl.Handle, but I'm not doing anything with the results. I've done this in order to invoke the get accessor of the Handle property on the control. In other words, I didn't want the result; I simply wanted the accessor to execute. Invoking the Control.Handle get accessor forces the creation of the underlying window for the control. This is important, because the thread that instantiates the preview handler component and calls its constructor is a single-threaded apartment (STA) thread, but the thread that calls into the interface members later on is a multithreaded apartment (MTA) thread. As you may know, Windows Forms controls are meant to run on STA threads and sometimes die a horrible death if an attempt is made to use them from MTA threads. So, the best time to create the Windows Forms preview window is in the preview handler's constructor. Later calls to the other interface methods that require interaction with the preview control will need to be marshaled back to that STA main thread. This is easily accomplished using the control's ISynchronizeInvoke interface. Note that this problem of two threads does not exist when implementing a native preview handler and is most likely an artifact of how the CLR handles COM interop.
(It's worth pointing out that although I am discussing implementations that rely on Windows Forms, there may be times when the easiest way to render a preview is to use an ActiveX control. For a discussion on this, see the "Using ActiveX Controls" sidebar.)

Using the COM Interfaces
Believe it or not, the constructor is the most difficult part of the whole PreviewHandler implementation. The rest of the implementation simply serves to route calls made on the COM interfaces to the Windows Form controls and to handle some basic bookkeeping. I'll start by discussing the easiest interfaces first.
IOleWindow IOleWindow allows a hosting application to get a handle to the window that participates in in-place activation-in other words, a handle to our control (and for preview handlers, ContextSensitiveHelp is never called). This makes the interface extremely easy to implement, as shown here:
void IOleWindow.GetWindow(out IntPtr phwnd)
{
phwnd = _previewControl.Handle;
}
void IOleWindow.ContextSensitiveHelp(bool fEnterMode)
{
throw new NotImplementedException();
}
IObjectWithSite IObjectWithSite is just as simple. The interface is used to provide an object with a pointer to the site associated with its container. The site provided to SetSite is actually the IPreviewHandlerFrame that contains the preview handler's window. As such, the SetSite method stores the provided IUnknown interface pointer into a private member (which GetSite can return on request) and casts this to an IPreviewHandlerFrame (which it also stores). Under the covers, this cast results in a QueryInterface call:
private object _unkSite;
private IPreviewHandlerFrame _frame;
void IObjectWithSite.SetSite(object pUnkSite)
{
_unkSite = pUnkSite;
_frame = _unkSite as IPreviewHandlerFrame;
}
void IObjectWithSite.GetSite(ref Guid riid, out object ppvSite)
{
ppvSite = _unkSite;
}
IInitializeWithFile and IInitializeWithStream Moving on, IInitializeWithFile and IInitializeWithStream are implemented in FileBasedPreviewHandler and StreamBasedPreviewHandler, respectively. They are called to provide the preview handler with the full path or IStream to the file to be previewed. Implementing these methods takes only a few lines of code:
// in FileBasedPreviewHandler
private string _filePath;
void IInitializeWithFile.Initialize(string pszFilePath, uint grfMode) {
_filePath = pszFilePath;
}
// in StreamBasedPreviewHandler
private IStream _stream;
void IInitializeWithStream.Initialize(IStream pstream, uint grfMode) {
_stream = pstream;
}
The sole method on these interfaces, Initialize, is provided with a path to the file to be previewed or with an IStream representing the file, along with a file mode indicating how the file can be opened. The file path or stream is stored in a member variable so it can be accessed later. Preview handlers should be read-only, so I've ignored the second parameter. (In fact, all preview handlers should ignore the second parameter and open files read-only. And they should allow for subsequent deleting, reading, and writing of files). IInitializeWithFile and IInitializeWithStream are easy to implement, but they do deserve some further discussion.
If you read the MSDN® documentation on implementing preview handlers, you'll find that the documentation strongly advocates implementing IInitializeWithStream instead of IInitializeWithFile. The main advantage IInitializeWithStream has over IInitializeWithFile is that IInitializeWithStream allows a preview handler to preview data that isn't stored in an independent file, such as a text file stored in a ZIP file. (If the shell tries to preview data available only as a stream using a previewer that only implements IInitializeWithFile, the shell will save a copy of the stream to a local file and then preview that file. Clearly that's not as efficient as previewing the stream directly.) Whenever possible, follow this advice. Admittedly, however, this isn't always possible or practical. This is because many previewers are for file types most easily loaded and rendered using APIs that only support file paths or URLs. For example, the MsiOpenDatabase function I use in my sample MsiPreviewHandler to open an MSI file expects a file path to the target MSI file. It doesn't accept an IStream, which is the only data I would have if I implemented IInitializeWithStream instead of IInitializeWithFile.
I've chosen to support this model in my framework using the two PreviewHandler subclasses mentioned earlier: FileBasedPreviewHandler and StreamBasedPreviewHandler. Each implements only one of the interfaces, not both. If PreviewHandler itself implemented both, the shell would always use the IInitializeWithStream implementation, which it prefers for reasons having to do with security and isolation. (Outlook would actually favor the IInitializeWithFile implementation, for legacy reasons). In your derived class, you can choose which interface to use by deriving from the appropriate PreviewHandler subclass.
Another important consideration is that the data from the file or stream should not be loaded in Initialize. Rather, as I've done here, the path to the file or the reference to the stream should be stored, and only when the preview handler is actually asked to render the preview should it load the contents of the file. Additionally, it should not in any way lock the file for exclusive access while the preview is being displayed. A user should be able to look at the preview and open the target file for editing at the same time. Most importantly, a user should still be able to delete the file while it is being previewed.
IPreviewHandler This brings me to the core interface a preview handler must support, which is aptly named IPreviewHandler. IPreviewHandler exposes seven methods that must be implemented: SetWindow, SetRect, DoPreview, Unload, SetFocus, QueryFocus, and TranslateAccelerator.
When a file is about to be previewed, the host passes information about the file or stream to the preview handler using one of the initialization interfaces discussed previously. The handle for the view window that will contain the preview window is then passed to the preview handler using the SetWindow method-this allows the preview handler to set that view window as the parent of the preview window. The host may then set the size of the preview window appropriately, using the SetRect method. At some point after that, the host calls the DoPreview method to cause the preview to be displayed. While the preview is displayed, the host may call SetRect again, whenever the view window is resized. Finally, when the preview is closed, the host tells the preview handler to tear down the preview by calling the Unload method. This refers to the unloading of any resources loaded from the item being previewed; it doesn't mean the preview handler itself is being unloaded. The same instance of the preview handler can be reused for multiple previews.
To support my implementation, I am using a couple of helper functions. First, my InvokeOnPreviewThread helper accepts a MethodInvoker delegate (a void and parameterless delegate defined in the System.Windows.Forms namespace). This is executed on the main UI thread with the help of the preview Control's ISynchronizeInvoke.Invoke method (remember that the control was created in the preview handler's constructor so as to be instantiated from an STA thread). Second, I use my UpdateWindowBounds helper to set the parent window for the preview control, move it to the correct position, and display it. This functionality is useful in a helper method, as several IPreviewHandler interface methods modify this information and require it to affect the preview.
SetWindow and SetRect are two such methods. The former provides the parent window handle and the new bounds for the preview window. The latter provides just the new bounds. SetFocus simply translates into a call to the preview control's Focus method, and QueryFocus returns the result of calling user32.dll's GetFocus function from the preview window's thread.
That leaves TranslateAccelerator, Unload, and DoPreview. My implementation of PreviewHandler currently doesn't provide much support for accelerator keys. As such, TranslateAccelerator simply delegates to the TranslateAccelerator method on the IPreviewFrameHandler stored in SetSite. If your preview handler displays multiple controls that can be tabbed through, you will want to augment TranslateAccelerator to provide more robust tab handling.
IPreviewHandlerFrame has another method on it, GetWindowContext, that allows the previewer to get a table that can be used to filter down the accelerators that should be forwarded to IPreviewHandlerFrame::TranslateAccelerator. This is meant to enable performance gains. However, even in native code, it doesn't provide huge performance improvements, and considering the complications involved in managing an accelerator table, doing so is probably not worthwhile. Preview handlers don't need to use the method-just forwarding everything to TranslateAccelerator is fine. In short, don't worry about GetWindowContext.
The Unload method hides the preview window and then delegates to the PreviewHandlerControl.Unload virtual method shown earlier in this article. And DoPreview, which is arguably the most important method on the interface, is no harder to implement. DoPreview is called to perform the actual rendering of the preview. My implementation calls to the abstract Load method, passing into it the PreviewHandlerControl. The derived implementation then calls either the FileBasedPreviewHandler's Load method with the path stored from IInitializeWithFile or the StreamBasedPreviewHandlerControl's Load method with the stream provided in IInitializeWithStream:
// in FileBasedPreviewHandler
protected override void Load(PreviewHandlerControl c)
{
c.Load(new FileInfo(_filePath));
}
// in StreamBasedPreviewHandler
protected override void Load(PreviewHandlerControl c)
{
c.Load(new ReadOnlyIStreamStream(_stream));
}
(Note that it encloses the COM IStream with a wrapper I derived from System.IO.Stream, allowing the derived class to consume the IStream in a read-only manner just as it would with any other .NET stream.) Once the data has been loaded, the DoPreview method calls UpdateWindowBounds to ensure that the preview is properly displayed.

Registering Handlers
Both the Windows Vista shell and Outlook 2007 look to the Windows registry to determine what preview handlers are available and to see what file types they are associated with.
As with any other COM class, a preview handler needs to be assigned a class ID. Referring back to Figure 2, this is the job of the GuidAttribute applied to the class:
[Guid("853f35e3-bd13-417b-b859-1df25be6c834")]
You'll notice that each of my preview handlers has a different GUID. Any custom preview handlers you create must also be assigned unique IDs. This ID is used as part of the preview handler's registration. The uuidgen.exe and guidgen.exe utilities included with Windows provide quick ways to generate GUIDs.
First, add a REG_SZ value to the PreviewHandlers key at HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\PreviewHandlers, where the name of the value is the GUID assigned to the preview handler. While it's not actually used, the value of this entry should be the display name of the preview handler. All of the built-in preview handlers do this, making it easy to see what preview handlers are registered by simply scanning this list in the registry . Later on, after everything has been registered correctly, you can also see a list of all of the registered preview handlers within the Outlook Trust Center .
Next, you need to create a special registry key under the shellex key for the target extension. This special key is named with a specific identifier, "{8895b1c6-b41f-4c1c-a562-0d564250836f}", which tells the system that the data it contains represents a preview handler. The default value for that key is set to the GUID for the preview handler used for that extension:
using (RegistryKey extensionKey =
Registry.ClassesRoot.CreateSubKey(extension))
using (RegistryKey shellexKey = extensionKey.CreateSubKey("shellex"))
using (RegistryKey previewKey = shellexKey.CreateSubKey(
"{8895b1c6-b41f-4c1c-a562-0d564250836f}"))
{
previewKey.SetValue(null, previewerGuid, RegistryValueKind.String);
}
If a specific preview handler can be used with multiple file extensions, you can simply repeat this step for as many extensions as appropriate. My framework looks for a PreviewHandlerAttribute on the preview handler. This attribute specifies the display name of the handler, the extensions the handler supports, and an AppID (which I'll discuss in a moment). I allow the supported extensions to be a simple string, as is the case with the ZipPreviewHandler:
[PreviewHandler("ZIP Preview Handler", ".zip",
"{c0a64ec6-729b-442d-88ce-d76a9fc69e44}")]
I also allow it to be a semicolon-delimited list of extensions, as is the case with my BinPreviewHandler:
[PreviewHandler("Binary Preview Handler", ".bin;.dat",
"{e92d3c10-89c8-4543-91b9-7a74305e9df4}")]
In such a scenario, my registration function (which you can see in the download) loops through each of the listed extensions and registers the handler for each of them. (Note that there are some preview handlers in Windows Vista that can handle more file types than they're registered to support. For info on previewing more file types, see the "If You've Got It, Flaunt It" sidebar.)
If You've Got It, Flaunt It
There are a handful of preview handlers built into Windows Vista. However, some of them can actually handle more file types than they're registered to support by default. Take, for instance, the Microsoft Windows TXT Preview Handler. As you can guess, this preview handler renders .txt files. But nothing about this preview handler restricts it to working with just .txt. As a developer, I frequently receive C#, Visual Basic®, and C++ code files as e-mail attachments. And I'd love to be able to preview these attachments within Outlook, rather than having to open Visual Studio® as the default viewer. Well, I can. All I have to do is register .cs, .vb, .cpp, and .h files as extensions to be previewed with the Microsoft Windows TXT Preview Handler. The following .reg file does just that:
Windows Registry Editor Version 5.00
[HKEY_CLASSES_ROOT\.cs\shellex\{8895b1c6-b41f-4c1c-a562-0d564250836f}]
@="{1531d583-8375-4d3f-b5fb-d23bbd169f22}"
[HKEY_CLASSES_ROOT\.vb\shellex\{8895b1c6-b41f-4c1c-a562-0d564250836f}]
@="{1531d583-8375-4d3f-b5fb-d23bbd169f22}"
[HKEY_CLASSES_ROOT\.cpp\shellex\{8895b1c6-b41f-4c1c-a562-0d564250836f}]
@="{1531d583-8375-4d3f-b5fb-d23bbd169f22}"
[HKEY_CLASSES_ROOT\.h\shellex\{8895b1c6-b41f-4c1c-a562-0d564250836f}]
@="{1531d583-8375-4d3f-b5fb-d23bbd169f22}"
If you want to be really slick, you can write a simple application or script that enumerates all of the file extensions under HKEY_CLASSES_ROOT. You can check to see if each has a Content Type value set, and if it does, whether that Content Type's value starts with "text/". If it does and if the extension doesn't already have a preview handler configured, you can set it to be previewed with the TXT handler—just as I did in the previous registry script. With only a few minutes work, you can dramatically expand the number of file types that can be previewed.
Of course, the TXT Preview Handler isn't the only handler capable of previewing a variety of file types. Some handlers you write might also exhibit this flexibility. Consider the XmlPreviewHandler described in this article. If you look in the code download, you'll see that the preview handler is actually named InternetExplorerPreviewHandler. I changed its name since it's not really specific to the XML file format—it's more specific to the control used to render the XML (the WebBrowser control). Since the preview handler uses the WebBrowser control to navigate to the file being previewed, this handler can be used with quite a variety of file types. Many of these file types are already covered by other preview handlers, but some aren't. For example, the new XML Paper Specification (XPS) file format delivered with Windows Vista doesn't have a built-in preview handler, but Internet Explorer is capable of rendering XPS documents. Therefore, I augmented the InternetExplorerPreviewHandler by adding .xps to the list of supported file extensions, and with that simple effort, I now have a preview handler for XPS.
Close [x]

In addition to registering on file types, you can register on Prog IDs. For example, the default value in HKEY_CLASSES_ROOT\.xml is "xmlfile", so my XML preview handler is actually registered under HKEY_CLASSES_ROOT\xmlfile\ShellEx\{8895b1c6-b41f-4c1c-a562-0d564250836f}:
[PreviewHandler("XML Preview Handler", "xmlfile",
"{88235ab2-bfce-4be8-9ed0-0408cd8da792}")]
That way, if you have multiple file extensions that are really the same type, you can use the same Prog ID for each and register the previewer only once. (For example, you could register on HKEY_CLASSES_ROOT\jpegfile instead of on both HKEY_CLASSES_ROOT\.jpeg and HKEY_CLASSES_ROOT\.jpg). In fact, the Windows shell team recommends that new file types should define new Prog IDs and register previewer handlers on those, rather than on actual extensions, even if there's currently a 1:1 mapping between the Prog ID and the extension.
The next registry change (for managed handlers) involves AppIDs. The reason relates back to my discussion about writing managed add-ins for the Windows Vista shell. As I mentioned, preview handlers are loaded out-of-process from the shell, by default in a surrogate host (prevhost.exe). For efficiency, the system wants as few instances of prevhost.exe running as possible, so multiple types of preview handlers are loaded into the same external process. This means that while the shell won't be attempting to load managed preview handlers targeting different versions of the CLR, prevhost.exe might. And this brings us back to the problem we initially had. The solution is to tell the system that instances of your preview handler must be loaded into a surrogate host dedicated to preview handlers of that same type. This guarantees you won't have multiple preview handlers targeting different versions of the CLR loaded into the same surrogate process. The COM class registration for the preview handler (which I'll get to momentarily) specifies an ID for configuration details about exactly what surrogate host to use, and that ID is referred to as an AppID. When a managed preview handler is registered, I create a new AppID registration under HKCR\AppID that will only be used by that managed preview handler:
using (RegistryKey appIdsKey = Registry.ClassesRoot.OpenSubKey(
"AppID", true))
using (RegistryKey appIdKey = appIds.CreateSubKey(appID))
{
appIdKey.SetValue("DllSurrogate",
@"%SystemRoot%\system32\prevhost.exe",
RegistryValueKind.ExpandString);
}
Please note that this AppID still points to prevhost.exe as the surrogate host.
The only thing left to do is to add the preview handler's COM component registration. Most of this is easily done by using the regasm.exe tool included with the .NET Framework SDK:
regasm /codebase ManagedPreviewHandler.dll
(You can do this from an installer as well, with a bit of code that uses the RegisterAssembly method of the RegistrationServices class in the System.Runtime.InteropServices namespace of mscorlib.)
As mentioned previously, however, I need to modify the component registration to point to the new AppID I've created. And I need to add to it a display name that will be used by hosts like Outlook 2007 to list registered handlers:
using (RegistryKey clsidKey = Registry.ClassesRoot.OpenSubKey("CLSID"))
using (RegistryKey idKey = clsidKey.OpenSubKey(previewerGuid, true))
{
idKey.SetValue("AppID", appID, RegistryValueKind.String);
idKey.SetValue("DisplayName", name, RegistryValueKind.String);
}
For a production preview handler, DisplayName should really be set to a REG_SZ value that points to a localized Win32 binary resource string ("@myhandler.dll,-101", for example). Unfortunately, the IDE for Visual C#® 2005 does not provide a built-in way to include a Win32 resource file in a managed assembly. To do that, you need to drop down to the command line, since csc.exe supports the /win32res switch that allows for the incorporation of Win32 resource files. For the purposes of this article and my registration code, I've resorted to a non-localized value.
All of this registration logic is wrapped up into a single method on PreviewHandler:
protected static void RegisterPreviewHandler(
string name, string extensions, string previewerGuid, string appID)
There is a corresponding unregistration method, as well.
The regasm.exe tool supports ComRegisterFunctionAttribute from the System.Runtime.InteropServices namespace in mscorlib. You can apply this attribute to a static method in an assembly to be registered with regasm, and when types in that assembly are found to be ComVisible, their type info is passed to the method marked with the ComRegisterFunction attribute (after the type has had its COM class registered in the registry). This makes it very easy to write a method that handles all of the previously mentioned registration goo when the assembly is registered for COM interop. I've included a counterpart unregistration function that is marked with the ComUnregisterFunctionAttribute.
With this in place, installing and registering my preview handlers requires just two lines at the command prompt:
gacutil -i MsdnMagPreviewHandlers.dll
regasm /codebase MsdnMagPreviewHandlers.dll
Similarly, unregistering the installed preview handlers requires just two lines at the command prompt:
regasm /unregister MsdnMagPreviewHandlers.dll
gacutil -u MsdnMagPreviewHandlers
If you write your own custom preview handlers that use this framework, and you write them in the same assembly as mine, you won't have to do anything else to get them to work. If you write them using my framework, but you write them in a separate assembly, you just need to add the following class to your assembly:
internal sealed class PreviewHandlerRegistration
{
[ComRegisterFunction]
internal static void Register(Type t) {
PreviewHandler.Register(t);
}
[ComUnregisterFunction]
internal static void Unregister(Type t) {
PreviewHandler.Unregister(t);
}
}
First install and register MsdnMagPreviewHandlers.dll, as I showed previously. Then install and register your assembly. When regasm sees the ComRegisterFunction in the PreviewHandlerRegistration class in your assembly, it will call the ComRegisterFunction for each of the preview handlers in your assembly. The function will delegate to all of the previously described functionality included in MsdnMagPreviewHandlers.dll.
To exemplify how to do this, I've included a preview handler for XAML files in the download for this article. It's implemented in its own assembly, which contains my XamlPreviewHandler class and the PreviewHandlerRegistration class. Figure 10 is a screenshot of the handler in action in Outlook 2007 (the XAML file being previewed, which is from the Windows Presentation Foundation SDK team blog, is available at blogs.msdn.com/wpfsdk/archive/2006/05/23/Animating_XAML_Clip_Art.aspx). Note that XamlPreviewHandler also derives from StreamBasedPreviewHandler rather than FileBasedPreviewHandler. The XamlReader class provided with the Windows Presentation Foundation supports loading from a Stream, and thus it makes sense to implement IInitializeWithStream rather than IInitializeWithFile:
public override void Load(Stream stream)
{
Frame f = new Frame();
XamlReader reader = new XamlReader();
f.Content = reader.LoadAsync(stream);
ElementHost xamlHost = new ElementHost();
xamlHost.Child = f;
xamlHost.Dock = DockStyle.Fill;
Controls.Add(xamlHost);
}

Debugging
Since in-process preview handlers run under a surrogate hosting process (prevhost.exe by default), to debug a preview handler, you need to attach the debugger to the host process. There are a few ways to do this. The first is to wait for the host process to start and then use the debugger's ability to attach to an existing process. However, there may be multiple instances of prevhost.exe, especially if you follow my suggestion of creating a new AppID for each of your managed preview handlers. When there are multiple instances of prevhost.exe running, you need to know which instance to connect to. You can use a tool like Process Explorer (www.sysinternals.com/Utilities/ProcessExplorer.html) from Sysinternals (recently acquired by Microsoft) to examine the command-line arguments used when the appropriate instance of prevhost.exe was launched. As a surrogate host executable, prevhost.exe needs to be told at startup what COM component to host, and thus it will be passed the GUID for your preview handler on the command line:
prevhost.exe {8FD75842-96AE-4AC9-A029-B57F7EF961A8} -Embedding
You can look at the command-line arguments for each instance of prevhost.exe to find the one with the GUID that matches the preview handler you want to debug. You can then use the process ID to attach the debugger to the correct process.
If the process is already running, the preview handler will have already been constructed and some of its code executed. To debug its startup, you need a different solution. One approach is to place a call to System.Diagnostics.Debug.Break at the beginning of the constructor. This causes the debugger to launch and attach to the process at that moment, allowing you to debug your entire component.
But attaching a debugger isn't always the best solution. Sometimes you just want classic "printf-style" debugging, where you output a bunch of information that you can watch as the preview handler runs. In this case, it's best to use the System.Diagnostics.Trace class. By default, the TraceListenerCollection that Trace outputs to contains a TraceListener that writes the traced data to OutputDebugString, enabling any listening debuggers to display the output text. The Windows SDK includes Debug Monitor (DbMon.exe) which makes it easy to view these traces.
For testing purposes, I recommend that you add a trace statement to the beginning of each of the interface methods I discussed earlier. Alternatively, you can use Debugger.Break in concert with the new tracepoint feature in Visual Studio 2005. By doing this, you'll gain a much clearer understanding of how the shell and Outlook 2007 interact with your preview handler.

Conclusion
Preview handlers are an awesome addition to both the Windows Vista shell and Outlook 2007. Throw in the productivity advantages of managed code (using my existing framework, it took me only seven minutes to create the XAML preview handler and get it up and running) and you've got yourself a very powerful platform for displaying existing file types as well as your own custom file types. I believe preview handlers have the potential to make us more productive in our daily routines-and the more supported file types there are, the more productive we'll all be. So get out there and start coding a preview handler for your favorite extension.

2007-03-25

 

Архів новин: новини IT, опис технологій, ціни