Cutting Edge

Customize Your Open File Dialog

Dino Esposito

Code download available at:CuttingEdge0303.exe(96 KB)

Contents

OpenFileDialog
System Settings for the Places Bar
RegOverridePredefKey
Overriding the Places Bar Key
A Custom Places Bar
Putting It All Together

Displaying an Open File dialog is certainly easy in the Microsoft® .NET Framework with Windows® Forms, but the resulting window is not as customizable as when you create it through the Win32® API. With Windows 2000, Microsoft added a nice feature—the places bar, which is the vertical toolbar that appears on the left side of the window to let you select a frequently visited folder. As you can see in Figure 1, the places bar contains buttons to take the user directly to five folders—History, Desktop, My Documents, My Computer, and My Network Places.

Figure 1 Selecting a Folder in the Places Bar

Figure 1** Selecting a Folder in the Places Bar **

When coding against the Open File common dialog in the Win32 API, you can set a style to hide the places bar. But like other features of the Win32 common dialogs, this setting seems to have gotten lost in the migration to the .NET Framework. Creating a common dialog has never been easier than it is in the Framework, but this simplicity comes at the cost of some flexibility. In addition, in managed code there is no way to extend the layout of the dialog with additional controls.

In this column, I'll focus on the places bar of the Open File common dialog. I'll discuss how to customize the list of folders displayed and how to make it application specific and even call specific. In the process of doing so, I'll review some of the registry hives and nodes and APIs in the .NET Framework for registry key manipulation. Finally, I'll show you a class that extends the OpenFileDialog system class with a collection representing the places to show in the toolbar.

OpenFileDialog

The OpenFileDialog class represents the system-provided common dialog box that allows the user to select and open a file. The class inherits from CommonDialog, but no additional classes can inherit from it. To show a dialog and pick up a file, you use this surprisingly simple C# code:

openFileDialog.InitialDirectory = @"c:\"; openFileDialog.Filter = "Bitmap|*.bmp|All|*.*"; openFileDialog.ShowDialog();

Notice the special formatting required by the Filter property string. The Filter property identifies the entries in the file's type box. The contents of the property must be a string consisting of pipe-separated pairs of strings. In each pipe-separated pair, the first token represents a descriptive name of the type to select, whereas the second token stands for the mask for the files. For example, if you want to filter the folder view to display all bitmaps or all files, use the following string:

Bitmap|*.bmp|All|*.*

The use of the pipe as the separator is mandatory in the .NET Framework. In Win32 you would use nulls instead (in ASCII, use 0). However, a common programming practice in Win32 is to use pipes in the string and then programmatically replace them with nulls. The good news is that this service is now automatically provided by the .NET Framework.

The OpenFileDialog class exposes a set of properties to configure the dialog. For example, you can choose the initial directory, the initial filter index, the title of the window, whether multiple files can be selected, and whether the application's current directory should be restored before closing. The class also fires an event (called FileOk) whenever the user clicks on the Open button. As I mentioned earlier, OpenFileDialog is a sealed class, so you can't derive from it. However, if you want to customize the behavior of a file dialog, to the extent that it is possible, you should create a brand new class deriving from the abstract class FileDialog. In this case, you have access to a couple of powerful but protected methods such as HookProc and RunDialog. HookProc defines the dialog box hook procedure that adds specific functionality to the common dialog. RunDialog has the following signature:

protected abstract bool RunDialog( IntPtr hwndOwner );

The method receives the HWND handle of the window that owns the dialog box. The typical implementation of this method simply consists of storing the hwndOwner argument in the CommonDialog's protected member hwndOwner. The RunDialog method is important because it allows you to set up a persistent dialog box and select multiple files from different folders. To transform an otherwise modal dialog box into a modeless one, you replace the owner window and make the dialog a child of the desktop window. The handle of the desktop window is returned by the Win32 API function GetDesktopWindow. Incidentally, while customizing the Open File dialog using a bunch of API functions is an absolute necessity.

System Settings for the Places Bar

The contents of the places bar are not completely configurable by applications in either Win32 or in the .NET Framework. In the past couple of years, though, some Knowledge Base articles have reported that you can modify the list of places by simply writing some entries in a particular registry key. In other words, the list of places normally displayed by the dialog is only the default list. The internal Win32 code attempts to read the user's list of places from the registry. If it fails—that is, if no list has been specified—the default list is used. So which registry key can you use? It is located under HKEY_CURRENT_USER and, as such, is specific to the logged user. Here's the path:

Software \Microsoft \Windows \CurrentVersion \Policies \ComDlg32 \PlacesBar

The key doesn't exist by default and must be explicitly created. Try creating the key using either the Registry Editor or regedt32. Remember that you must be logged in with administrator rights in order to modify the registry. After creating the key, try launching any Win32 or managed application that makes use of the Windows Open File common dialog. For example, try it with Microsoft Paint; the result is shown in Figure 2. What's going on?

Figure 2 Empty Places Bar

Figure 2** Empty Places Bar **

When the Win32 internal code detected the registry subtree you just created, it discarded the default list. Since you haven't listed any places, the places bar is empty. (Note that this change affects all applications running on the system as long as the user who changed the settings is still logged in.)

Figure 3 Customizing Favorite Places

Figure 3** Customizing Favorite Places **

How can you specify your own favorite places? You should create entries in the aforementioned key. You can't create more than five entries because any more will be ignored. Each entry should be of type REG_SZ (a string value) and must be named "PlaceX", where X is a zero-based index. You can specify a place through a fully qualified path. Alternately, if you want to target special folders such as My Documents or My Computer, you use a folder ID. In this case, create a REG_DWORD entry and enter the folder number in hexadecimal. The two places entries in Figure 3 can be seen in action in Figure 4.

Figure 4 Two Places

Figure 4** Two Places **

If fewer than five entries are specified, the rest are left empty. No more than five places will ever fit in the bar. It is important that you name the entries Place0, Place1, and so on. In order to revert to the default, just delete the two keys that you added—that is ComDlg32\PlacesBar. Be extremely careful when you delete registry keys. You should make sure that only those two keys are removed, otherwise you could corrupt your system!

The following C# code shows how to enumerate the registered places for the current user:

RegistryKey placesBarRoot; placesBarRoot = Registry.CurrentUser.OpenSubKey(key); string[] valuesOfKey = placesBarRoot.GetValueNames(); for(int i=0; i<valuesOfKey.Length; i++) { Console.WriteLine(placesBarRoot.GetValue(valuesOfKey[i]); } placesBarRoot.Close();

You begin by opening the places bar key on the current user hive. The GetValueNames method of the RegistryKey class fills an array of strings with the names of all the entries found. In the example that I give, the valuesOfKey array would contain strings like Place0, Place1, and so forth. The value of each key is read through the GetValue method.

In this way you can certainly customize the places that appear in the Open File common dialog, but don't forget that the changes are extended to all running applications. The registry settings work on a per-user basis and Microsoft doesn't provide an API to programmatically configure the places bar on a per-application or per-call basis. Is there a way to trick the system and customize the places bar each time you display the file dialog? The answer is yes, but the implementation is not trivial and it entails the use of a little-known Win32 API function—RegOverridePredefKey.

RegOverridePredefKey

The RegOverridePredefKey function requires Windows 2000 or later; it is not supported on systems running Windows 9x. Mostly intended for software installation programs, the function allows you to map one predefined registry key onto another. The ultimate goal of this function is to enable a setup program to examine the changes to the registry that an installable component is attempting to make. In practice, before invoking certain components the setup program will map a critical registry subtree elsewhere and lets the component proceed.

When the component is finished, the installation program looks over the changes and decides whether they are safe. If they are, the setup program mirrors the changes to the original locations intended by the DLL. Otherwise, the changes can either be rejected or adapted before writing to the target locations. The prototype of the function is shown here:

LONG RegOverridePredefKey( HKEY hKey, HKEY hNewHKey );

The first argument is the registry key to remap, while the second argument is the location of the redirected key. The redirect key should either be a predefined key or should be currently open. Once RegOverridePredefKey returns, any call to hKey is resolved in the subtree rooted in the hNewHKey key. For example, consider the following call:

HKEY hkMyCU; RegCreateKey(HKEY_CURRENT_USER, "Dino", &hkMyCU); RegOverridePredefKey(HKEY_CURRENT_USER, hkMyCU);

After the call takes place, a call to read from Software\Microsoft will actually read from Dino\Software\Microsoft. The hive must be the same for both keys. The automatic redirection applies only to the process that calls RegOverridePredefKey. If the hNewHKey argument is null, the function restores the default mapping of the key. Just the fact that the redirection is process specific gives you the tools to implement an application-specific places bar.

Overriding the Places Bar Key

Creating or updating registry entries is the only way to customize the list of places in the Open File dialog. However, to make the changes application specific you need to adapt the contents of the unique system key for each call. If you really read and write to the registry whenever an application needs to display the dialog, too much traffic is generated and, more importantly, concurrently running applications would override each other's settings. By using the RegOverridePredefKey key, you actually create a process-specific copy of the specified registry tree and can update it without corrupting the registry. Since the mapping lives only in the context of the process, no other running applications would be affected. The RegOverridePredefKey function is not directly supported by the .NET Framework, meaning that you need to explicitly import the function through the P/Invoke interoperability platform.

Not all Win32 registry functions have a matching managed class or static method in the .NET Framework. The data structure that is the core of the registry in Win32—the HKEY handle—is completely hidden in the .NET Framework and has been replaced by a class called RegistryKey. The RegistryKey class does make use of the HKEY handle internally, but there is no way for you to read that information from within an application built on the .NET Framework. The hkey data member, in fact, is marked as private. How does this affect you? (Who knows whether the Redmontonians will publicly expose the HKEY member in the next version of the .NET Framework?)

The RegOverridePredefKey function cannot be imported and adapted to use instances of the RegistryKey class to manage registry keys. You should import the function as shown here:

[DllImport("advapi32.dll")] private static extern long RegOverridePredefKey( IntPtr hkey, IntPtr hnewKey );

The issue is that you can't call the wrapper method I've just shown and pass it a registry key that has been opened using the .NET Framework classes. At this point, I see two possible approaches: either you open the registry key to map and its redirector using a Win32 function like RegOpenKeyEx and RegOpenKey, or you put all the initialization code in a new Win32 DLL and call its functions from within the application. In this column, I'll choose the second option. Figure 5 shows the Win32 code of a simple DLL that does the job. (I must confess that getting back to Win32 DLLs has been a little hard for me after more than two years spent coding only for the .NET Framework.)

Figure 5 Simple DLL

#include "stdafx.h" BOOL APIENTRY DllMain(HANDLE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) { return TRUE; } HKEY APIENTRY InitializeRegistry() { HKEY hkMyCU; RegCreateKey(HKEY_CURRENT_USER, "Dino", &hkMyCU); RegOverridePredefKey(HKEY_CURRENT_USER, hkMyCU); return hkMyCU; } void ResetRegistry(HKEY hkMyCU) { RegOverridePredefKey(HKEY_CURRENT_USER, NULL); RegCloseKey(hkMyCU); return; }

The library defines and exports two functions called InitializeRegistry and ResetRegistry. InitializeRegistry is parameterless but returns a Win32 HKEY object. ResetRegistry has no return value but takes the same HKEY opened with the previous method. The InitializeRegistry function opens the HKEY_CURRENT_USER hive and maps it to a newly created node called Dino. (Of course, you should feel free to rename it to something more meaningful to you.) The function creates the key if it doesn't already exist and then opens it. After you call InitializeRegistry, all the attempts to read and write under the HKCU result in readings and writings done under the remapped key. This occurs only for the current process. ResetRegistry restores the natural order of things, removes the mapping, and closes the redirector registry key. Of course, to avoid serious problems with other parts of your code, you should call InitializeRegistry/ResetRegistry within a very short time interval. You should never call InitializeRegistry upon the form's load or call ResetRegistry while unloading.

After compiling the Win32 DLL, place the file in the same folder as managed application executables. While compiling the DLL I repeatedly forgot to add an export file to the Visual Studio® 6.0 project with the immediateness of the .NET Framework programming in mind. I had assumed that declaring APIENTRY the functions would have been enough. As a result, the application built on the .NET Framework complained a few times that no such functions were found in the Win32 DLL. Using the public keyword in the .NET Framework is so easy and handy! This code snippet demonstrates how to import the two functions in a managed app.

[DllImport("myregutil.dll")] private static extern IntPtr InitializeRegistry(); [DllImport("myregutil.dll")] private static extern void ResetRegistry(IntPtr hKey);

A Custom Places Bar

Now let's see how to exploit this small Win32 library to create a custom places bar for each call made to the Open File common dialog. My goal here is to use an ad hoc class to set up the places bar and display the dialog. As mentioned earlier, though, you cannot create a new dialog class by inheriting from the OpenFileDialog class. When inheritance is not possible, aggregation is always an alternative to consider. Let's create an OpenDialogPlaces class that embeds an instance of the OpenFileDialog class. The following code snippet shows how this specific class would work:

OpenDialogPlaces o = new OpenDialogPlaces(); o.Places.Add(@"c:\\"); o.Places.Add(@17); o.Init(); o.OpenDialog.ShowDialog(); o.Reset();

The class exposes an array called Places that you populate as needed. When finished, you invoke a class-specific method called Init and update the registry. Next, you display the dialog and call Reset to restore the original status of the registry. Note that this is not the only possible way to implement the feature; it just gives you an idea of how to achieve it.

The OpenDialogPlaces class descends from Object and creates an OpenFileDialog object upon instantiation:

public OpenDialogPlaces() { m_places = new ArrayList(); m_openFileDialog = new OpenFileDialog(); }

Other private members include the handle to the overridden registry key, saved as an IntPtr object. Public members are the OpenDialog object representing the embedded common dialog instance and the Places collection.

The Init method sets up the registry for making the call. The mapping lasts until Reset is called, like this:

m_overriddenKey = InitializeRegistry(); RegistryKey reg; reg = Registry.CurrentUser.CreateSubKey(Key_PlacesBar); for(int i=0; i<Places.Count; i++) { if(Places[i] != null) reg.SetValue("Place" + i.ToString(), Places[i]); }

The call to InitializeRegistry lays the groundwork for creating a fake registry tree for the process to use. The code creates the tree under the redirector key and defines the place entries that the user added to the Places collection.

You use the CreateSubKey method on the Registry object to create a subtree. Note that the Win32 library remaps the CurrentUser key, so you have to create the subtree just below it. The SetValue method lets you create registry entries. This method is a managed wrapper for the RegSetValueEx Win32 API function. By default, it creates REG_SZ entries. The signature of the method is shown here:

public void SetValue(string name, object value)

The actual type of the value argument determines the type of registry key being created. In particular, if the argument is of type Int32, a REG_DWORD entry is created. Internally, the code performs the type check shown in the following code snippet:

if (value as Int32 != null) {...}

You'll need a REG_SZ entry if the name of the folder is an absolute or relative path. You need to use the folder-specific number if you want to target a special folder (see Figure 6 for a list). In this case, a REG_DWORD entry is needed.

Figure 6 Folder IDs

ID Folder
0 Desktop
2 Programs folder on Start menu
3 Control Panel
4 Printers
5 My Documents
6 Favorites
7 Startup folder on Start menu
8 Recent Files
9 Send To
10 Recycle Bin
12 Start menu
17 My Computer
18 My Network Places
20 Fonts

Putting It All Together

If you fill the Places array with the values shown in the following code and then display the file dialog, you get a window like the one that is shown in Figure 7:

Figure 7** A Custom Places Bar **

o.Places.Add(@"C:\"); o.Places.Add(17); o.Places.Add(5); o.Places.Add(@"C:\My Articles"); o.Places.Add(6);

If you indicate folders with a custom icon or a comment, that will be reflected by the item in the places bar. As Figure 8 shows, two applications running at the same time can have different places bars.

Figure 8** Different Places Bars **

The trick discussed here is not specific to the .NET Framework. It has to do with some characteristics of the Windows 2000 operating system and, as such, works well in Win32 also.

Send your questions and comments for Dino to cutting@microsoft.com.

Dino Espositois an instructor and consultant based in Rome, Italy. Author of Building Web Solutions with ASP.NET and ADO.NET and Applied XML Programming for .NET, both from Microsoft Press, he spends most of his time teaching classes on ASP.NET and speaking at conferences. Dino is currently writing Programming ASP.NET for Microsoft Press. Get in touch with Dino at dinoe@wintellect.com.