Beyond WinFX

Transactions, Aero Wizards, And Task Dialogs In Windows Vista

Kenny Kerr

This article is based on a prerelease version of Windows Vista. All information herein is subject to change.

This article discusses:

  • Kernel Transaction Manager
  • Using command links for customizable buttons
  • Building more intuitive interfaces with Aero wizards
  • Replacing message boxes with task dialogs
This article uses the following technologies:
Windows Vista, C++, C#, Visual Studio 2005

Code download available at:Vista2006_07.exe(178 KB)

Contents

Kernel Transaction Manager
Command Links: Getting to the Point
Streamlined UI: Aero Wizards
From Message Box to Task Dialog
Sophisticated Task Dialogs
Conclusion

What comes to mind when you think about programming for Windows Vista™? Odds are you’ll think of WinFX®. WinFX encompasses a number of technologies that have been getting a lot of attention, including the Microsoft® .NET Framework 2.0, Windows® Presentation Foundation, Windows Communication Foundation, and Windows Workflow Foundation. But WinFX is only one part of the new OS. Windows Vista is a dramatic upgrade to the Windows operating system, providing a wealth of new and improved libraries and services that reach beyond WinFX, allowing you to build more powerful applications with less code.

This article introduces a number of new features that you can expect from Windows Vista. These features don’t normally get the attention they deserve because they’re either aimed at native C++ developers or just don’t fall into the category of WinFX programming. Nonetheless, they’re worth getting to know.

Kernel Transaction Manager

One of the most exciting new services that developers have at their disposal is the Kernel Transaction Manager (KTM). This acts as both a transaction manager for local transactions and a resource manager for distributed local transactions. KTM offers a new concept of multiple transaction managers on a single machine, as it manages the multiple instances and their associated resource managers. Before digging into the KTM’s functionality in more detail, let’s quickly recap the purpose of transaction and resource managers.

A resource manager is responsible for some local resource, such as a database engine or a message queue server. Resource managers work in conjunction with transaction managers to coordinate changes to the resources being managed within the context of a transaction. Any number of resource managers can participate in a transaction by coordinating with a transaction manager. In practice, most resource managers also act as transaction managers when the transaction is confined to a single resource, such as a database engine. This is referred to as a local transaction.

If a transaction enlists multiple resource managers it is said to be a distributed transaction and an independent transaction manager steps in to coordinate with the various resource managers. This coordination ensures that all the resource managers either commit or roll back any changes to their resources. Of course, this enlistment and coordination of distributed resource managers incurs a noticeable performance overhead and thus local transactions are preferred whenever possible.

Microsoft SQL Server™ is a good example of a resource manager. Explicit or implicit transactions involving a single SQL Server instance use SQL Server’s own transaction manager to transact any commands executed within the context of a transaction. The Microsoft Distributed Transaction Coordinator (DTC), a service provided by Windows, is used as a transaction manager when a transaction spans more than one resource manager. The DTC coordinates transactions with SQL Server and other user-mode resource managers, including KTM. Furthermore, you can start a transaction in managed code and use the transaction to transact file operations.

Just as SQL Server acts as a resource manager for local database transactions and as a transaction manager for distributed database transactions, the KTM acts as a transaction manager for local file system and registry operations and as a resource manager for distributed local transactions that involve file system and registry operations. The KTM allows you to call file system and registry functions in the context of a transaction, and will ensure that those operations are transacted as a whole.

The beauty of the KTM is that it doesn’t require you to change the way you interact with files and registry keys. Whatever API you use today for interacting with files and registry keys will continue to work as long as those abstractions end up interacting with kernel objects via file and registry handles.

Let’s take a look at a simple example using the file and registry management functions from the Windows SDK. Suppose you are writing a program that needs to keep track of a file system folder, perhaps for storing a history of instant messenger conversations. And say you want to allow the user to change the location of this history folder. You could achieve this with the simplistic function shown in Figure 1.

Figure 1 Basic Folder-Moving Function

HRESULT MoveHistoryFolder(PCWSTR oldPath, PCWSTR newPath) { HRESULT result = E_POINTER; if (0 != oldPath && 0 != newPath) { if (!::MoveFile(oldPath, newPath)) { result = HRESULT_FROM_WIN32(::GetLastError()); } else { CRegKey key; result = HRESULT_FROM_WIN32(key.Create( HKEY_CURRENT_USER, L"Software\\Kenny Kerr\\Sample")); if (SUCCEEDED(result)) { result = HRESULT_FROM_WIN32( key.SetStringValue(L"HistoryFolder", newPath)); } // Move the folder back if we failed to update the registry. if (FAILED(result)) VERIFY(::MoveFile(newPath, oldPath)); } } return result;

The MoveHistoryFolder function is intended to move the history folder and update a registry setting that stores its location for the application’s use. But this implementation is actually far too simple. There are a number of scenarios it doesn’t take into account.

MoveHistoryFolder starts off by moving the history folder. If the move is performed successfully the function will then attempt to update the registry. If the registry operations fail, MoveHistoryFolder carefully attempts to move the folder back to its original location so that the function can fail without side effects, providing a strong guarantee.

So what’s wrong with this function? Even in this simple example, it is possible for the application’s state to become corrupt. A power or hardware failure, for example, might result in the history folder being moved without the corresponding registry update occurring. The problem is that the operating system sees the various file and registry management function calls as independent operations. (Your application, on the other hand, considers these functions to be a single logical operation that should have no potential side effects.)

Thanks to the KTM, solving this problem is quite simple. Since all the resources involved are local resources controlled with kernel objects (assuming that you don’t allow users to move the history folder to a remote computer), you can use the KTM both as the transaction manager for a local transaction and also as the resource manager.

The CreateTransaction function creates a new transaction object controlled by the KTM. Transaction objects are regular kernel objects in the same way that other OS constructs like events and files are represented by kernel objects and accessed through secured handles. CreateTransaction returns a handle to the transaction object. A GUID uniquely identifies the transaction on the computer (if this isn’t specified, the KTM will create a GUID by default). As with other kernel objects, you can specify a SECURITY_ATTRIBUTES structure to control the security attributes of the handle. Additionally, you can pass a user-readable description string as the last parameter to CreateTransaction—this is recommended for management purposes.

When you no longer need the transaction, you simply close the transaction handle with the CloseHandle function. Closing the last handle to the transaction rolls it back if the transaction hasn’t been committed. You can request that a transaction be committed using the CommitTransaction function. A transaction can be rolled back by using the RollbackTransaction function. The use of these functions is greatly simplified with a little help from C++, as shown in Figure 2.

Figure 2 KtmTransaction Class

class KtmTransaction { public: HRESULT Create(PSECURITY_ATTRIBUTES securityAttributes = 0) { HRESULT result = S_OK; Handle.Attach(::CreateTransaction( securityAttributes, NULL, 0, 0, 0, 0, 0)); if (INVALID_HANDLE_VALUE == Handle) { result = HRESULT_FROM_WIN32(::GetLastError()); } return result; } HRESULT Commit() { HRESULT result = S_OK; if (!::CommitTransaction(Handle)) { result = HRESULT_FROM_WIN32(::GetLastError()); } return result; } HRESULT Rollback() { HRESULT result = S_OK; if (!::RollbackTransaction(Handle)) { result = HRESULT_FROM_WIN32(::GetLastError()); } return result; } CHandle Handle; };

The code illustrates the use of the KtmTransaction class. However, before you can actually use this class, you need a way to bind the transaction to the thread that is making the calls to the file and registry management functions so those functions, in turn, will be aware that they’re being called from within the context of a transaction. This is the role of the SetCurrentTransaction function: it binds the calling thread to the given transaction. The thread is bound until SetCurrentTransaction is called again with an invalid handle. Figure 3 shows the KtmTransactionBinding class, which you can use to simplify the process of binding a transaction to the calling thread.

Figure 3 Ktm TransactionBindingClass

class KtmTransactionBinding { public: KtmTransactionBinding(KtmTransaction& transaction) : m_transaction(transaction) { /* Do nothing */ } ~KtmTransactionBinding() { COM_VERIFY(Unbind()); } HRESULT Bind() { return Bind(m_transaction.Handle); } HRESULT Unbind() { return Bind(INVALID_HANDLE_VALUE); } private: static HRESULT Bind(HANDLE transaction) { HRESULT result = S_OK; if (!::SetCurrentTransaction(transaction)) { result = HRESULT_FROM_WIN32(::GetLastError()); } return result; } KtmTransaction& m_transaction; };

With the KtmTransaction and KtmTransactionBinding classes defined, you can call the sample application’s MoveHistoryFolder function within the context of a transaction, as shown in Figure 4. Unless MoveHistoryFolder returns a success HRESULT, the transaction object’s Commit function will not be called and any file and registry operations will be rolled back when the transaction object is destroyed.

Figure 4 Moving a Folder Within a Transaction

KtmTransaction transaction; HRESULT result = transaction.Create(); if (SUCCEEDED(result)) { KtmTransactionBinding binding(transaction); result = binding.Bind(); if (SUCCEEDED(result)) { result = MoveHistoryFolder(L"C:\\Users\\Kenny\\My History", L"C:\\Users\\Kenny\\History"); if (SUCCEEDED(result)) result = transaction.Commit(); } }

I can’t stress enough how beneficial transactions can be for file and registry operations. Since all rollback logic is performed automatically, you can focus on the code needed to perform the necessary operations, accounting for any and all scenarios, without worrying about writing all the complex rollback logic. As an example, the MoveHistoryFolder function shown in Figure 1 does not take into account the fact that the new folder location may be nested while the MoveFile function requires the immediate parent directory to exist in order for the operation to succeed. Adding the logic to ensure that the parent directory exists is not hard, but all the additional rollback logic can be extremely tricky.

Command Links: Getting to the Point

Command links are a new style of button that simplifies the user experience by focusing the user’s attention on one or more "commands." Although designed primarily for use in wizards, command links are little more than old-fashioned button controls with special style attributes.

The simplest way to create a command link is by using the Visual C++® 2005 Resource Editor. Open the dialog editor’s Toolbox view and drop a custom control onto the dialog. (Although command links are just button controls, the dialog editor has no knowledge of command links and so does not provide any attributes for styling button controls as command links.) Be sure to make the custom control reasonably large as command links are intended to be displayed quite a bit larger than the average button. Now set the control’s Class property to BUTTON and its Style property to 0x5001000e. You may also want to update the Caption property with the text you want to display on the command link. You can see these settings in the right-hand panel of Figure 5. At this point you can compile the project and display the dialog box, which is also shown in Figure 5.

Figure 5a Creating Command Links

Figure 5a** Creating Command Links **

Figure 5b

Figure 5b  

Command links also let you add some additional text to the button to provide further explanations. You can do this by sending the button a BCM_SETNOTE message or by using the simpler Button_SetNote macro. You can also amplify a given command link by replacing the arrow with a shield. To do this, you either send the BCM_SETSHIELD message or use the Button_SetElevationRequiredState macro. The following shows an example of sending these messages in the context of an Active Template Library (ATL) dialog box class:

CWindow button1 = GetDlgItem(IDC_BUTTON1); Button_SetNote(button1, L"You know you want to click it."); Button_SetElevationRequiredState(button1, TRUE);

Streamlined UI: Aero Wizards

Although wizards have been around for a while, the Wizard 97 specification is full of useless and redundant displays of information, such as exterior pages and headers. Aero™ wizards, which replace the aging Wizard 97 specification, are designed to be both clear and concise, offering a fresh new experience for guiding users through some potentially complex operations.

As with command links, Aero wizards are simply exposed as additional styles on existing controls, in this case the property sheet. Figure 6 shows a minimal Wizard 97-style wizard. Let’s take a look at what it takes to transform this into a shiny new Aero wizard. To keep this as clean and short as possible, I’ll use ATL and Windows Template Library (WTL) to illustrate. You can, of course, use any framework you like.

Figure 6 Aging Wizard 97 Conversion

Figure 6** Aging Wizard 97 Conversion **

The window in Figure 6 is created by the Wizard class. The CPropertySheetImpl base class wraps the functions, messages, and structures necessary to implement a property sheet. Wizards, after all, are just property sheets with some additional flags to change their appearance and behavior. The PROPSHEETHEADER structure is used to control virtually all aspects of a property sheet. The CPropertySheetImpl class automatically populates this structure on your behalf. If you want to change the behavior in some way, you can freely modify the PROPSHEETHEADER structure that is exposed through the inherited m_psh member variable. This is exactly what I’ve done in Figure 6 to change the property sheet into a wizard.

Changing the wizard into an Aero style wizard is as simple as replacing the PSH_WIZARD97 flag with PSH_AEROWIZARD. But as you’ll see when you look at the result (see Figure 7), there is a bit more work to be done.

Figure 7 Aero Wizard Conversion

Figure 7** Aero Wizard Conversion **

The most striking difference is the size of the window. The Wizard 97 specification dictates a minimum size for wizard pages. The dialog template used for these samples is smaller than this minimum. With the PSH_WIZARD97 flag, the window is padded to ensure the minimum size is honored. The Aero wizard has no such restriction and it wraps the dialog template more cleanly.

The other thing you should notice is that the Back button at the bottom of the wizard has been replaced with a Web browser-style back button at top the top of the client area. Other than the appearance, the button is controlled programmatically in the same way as before.

Aero wizards are designed with command links in mind so that you can reduce the clutter of the traditional set of buttons that adorn the bottom of most wizards today. The first button to go was the Back button. The Next button can also be removed in many cases. In the past, the PSM_SETWIZBUTTONS message could be used to disable the Next button. But prior to Windows Vista you could not hide both the Next and Finish buttons. Windows Vista makes this possible, allowing the Next button to be replaced by any number of various command links.

Aero wizards introduce a new message that lets you control the visibility of the wizard buttons. This is where you start to see the usefulness of command links. In the Wizard 97 style you can create a wizard page that includes a number of radio buttons. In this wizard, the user would make a selection and then click the Next button. Using Aero wizards and command links you can replace the radio buttons with command links and hide the Next button altogether. Now the user makes a selection and he’s finished, all with a single click.

You can hide the Next button by sending the property sheet PSM_SHOWWIZBUTTONS message or by using the PropSheet_ShowWizButtons macro:

SetWizardButtons(0); PropSheet_ShowWizButtons(m_hWnd, PSWIZB_CANCEL, PSWIZB_CANCEL | PSWIZB_NEXT);

The SetWizardButtons function is provided by the CPropertySheetWindow class and posts the PSM_SETWIZBUTTONS message to the property sheet. Passing 0 indicates that both the Back and Next buttons should be disabled. If you need to keep the Back button enabled, simply specify the PSWIZB_BACK flag. The PropSheet_ShowWizButtons macro is then used to set the visibility of the wizard buttons. Flags that appear in both the second and third parameters indicate buttons that will be displayed. Flags that appear only in the third parameter indicate buttons that will be hidden. Finally, you can include command links that provide the user with options for how to proceed, as shown in Figure 8.

Figure 8 Command Links

Figure 8** Command Links **

From Message Box to Task Dialog

The task dialog is a powerful new tool for displaying user prompts with a small amount of code. The MessageBox function has been widely used for many years to prompt users for an acknowledgment, to confirm an operation, or to make a yes-or-no decision. Message boxes are popular because the MessageBox function is convenient, but the user experience often suffers. The MessageBox function is rather limited so developers don’t have much opportunity to improve the user experience short of rolling their own message box implementation.

Message boxes have finally been replaced with task dialogs, which retain the simplicity of message boxes but optionally offer considerable customization options for improving and controlling the user experience. Task dialogs are exposed through one of two functions. This section deals with the simpler TaskDialog function, while the next section will dig into the more powerful TaskDialogIndirect function.

The TaskDialog function is very similar to the MessageBox function and in most cases you can simply replace one with the other and change the parameters to get the desired behavior. Consider the following MessageBox call:

int result = ::MessageBox(0, // no owner window L"Do you want to save changes?", L"Application", MB_YESNOCANCEL | MB_ICONINFORMATION);

This code can be replaced using the TaskDialog function. The code and resulting dialog are shown in Figure 9.

Figure 9 Using TaskDialog to Control Message Display

int result = 0; COM_VERIFY(::TaskDialog(0 /* no owner window */, 0 /* no module resources */, L"Application", L"Do you want to save changes?", L"", TDCBF_YES_BUTTON | TDCBF_NO_BUTTON | TDCBF_CANCEL_BUTTON, TD_INFORMATION_ICON, &result));

Although not dramatically different, the TaskDialog function has some handy features that make it worthwhile. Perhaps the best new capability is the ability to specify a module handle as the second parameter, which allows you to specify resource identifiers instead of literal strings. This will save you a lot of boilerplate code calling the LoadString function. Specifying a module handle also allows you to display the icon of your choice on the dialog box. In addition, the task dialog is capable of displaying separate content with a smaller font to provide additional information if needed. Finally, there is a separate flag for each button so you can create arbitrary combinations of buttons, as shown in Figure 10.

Figure 10 Traditional Nondescript Buttons

int button = 0; COM_VERIFY(::TaskDialog(0, instance, MAKEINTRESOURCE(IDS_APPLICATION_TITLE), MAKEINTRESOURCE(IDS_SAVE_CHANGES), MAKEINTRESOURCE(IDS_SAVE_CHANGES_INFO), TDCBF_YES_BUTTON | TDCBF_NO_BUTTON | TDCBF_CANCEL_ BUTTON, MAKEINTRESOURCE(IDI_APPLICATION), &button));

Sophisticated Task Dialogs

One of the glaring problems with the simple TaskDialog function is that you cannot change the text that appears on the buttons. In Figure 10, it’s obvious that the wordy descriptions of the buttons would not be needed if the buttons had more specific wording, such as "Save", "Don’t Save", and "Cancel". This is the kind of problem that TaskDialogIndirect is meant to solve. But TaskDialogIndirect does more than that: it can turn the simple dialog box into a rich, interactive interface for communicating more information without the need to write lots of UI code.

The TaskDialogIndirect function is prototyped as follows:

HRESULT WINAPI TaskDialogIndirect( const TASKDIALOGCONFIG* config, int* button, int* radioButton, BOOL* verificationChecked);

As you can imagine, most of the work to use this function goes into preparing the config parameter. The caller-provided TASKDIALOGCONFIG structure controls all of the behavioral and user interface aspects of the task dialog.

To start, let’s recreate the "Do you want to save changes?" dialog with more descriptive buttons. Figure 11 shows the result.

Figure 11 Adding More Instructive Button Text

TASKDIALOG_BUTTON buttons[] = { IDYES, L"&Save", IDNO, L"&Don’t Save" }; TASKDIALOGCONFIG config = { sizeof (TASKDIALOGCONFIG) }; config.pszWindowTitle = L"Application"; config.pszMainInstruction = L"Do you want to save changes?"; config.pszContent = L"Click Cancel to return to the application."; config.dwCommonButtons = TDCBF_CANCEL_BUTTON; config.pszMainIcon = MAKEINTRESOURCE(TD_INFORMATION_ICON); config.cButtons = _countof(buttons); config.pButtons = buttons; int button = 0; COM_VERIFY(::TaskDialogIndirect(&config, &button, 0, 0));

An array of TASKDIALOG_BUTTON structures is provided. Each structure specifies a button ID as well as the text caption for the button. The various text fields of the TASKDIALOGCONFIG structure are then populated. Many more fields are available for your customization. The TaskDialogIndirect function provides a tremendous amount of control over the appearance of the dialog—so much so that it would not be practical to examine each of its options in detail in this article. To allow you to easily explore the TaskDialogIndirect function’s options, I wrote the TaskDialog Designer shown in Figure 12.

Figure 6 Aging Wizard 97 Conversion

Figure 6** Aging Wizard 97 Conversion **

The TaskDialog Designer is a small Windows Forms application written in C# that exposes most of the TaskDialog options in a set of property pages so you can experiment with various options. The TaskDialog Designer makes use of a .NET assembly written in C++/CLI that provides a managed interface to the TaskDialogIndirect function. You can rewrite the previous native C++ code with this C# code to create the same task dialog:

using (TaskDialog dialog = new TaskDialog()) { dialog.Title = "Application"; dialog.MainInstruction = "Do you want to save changes?"; dialog.Content = "Click Cancel to return to the application."; dialog.Buttons = TaskDialogButtons.Cancel; dialog.CustomButtons.Add(new TaskDialogButton("&Save", 6)); dialog.CustomButtons.Add(new TaskDialogButton("&Don’t Save", 7)); dialog.MainIcon = TaskDialogIcon.Information; int button = dialog.ShowDialog(); }

One of the many uses of task dialogs is to create an error reporting dialog box. Using the task dialog’s expandable surface area, you can create an unobtrusive dialog box for reporting an error to the user, allowing him to see more information if he so desires.

In Figure 13, the TaskDialog is prepared with information from the Exception object. Initially, the error message is displayed while the complete contents of the exception are only displayed if the user chooses to view them.

Figure 13 Reporting Errors Through TaskDialog

try { // Attempt to open file... } catch (Exception e) { using (TaskDialog dialog = new TaskDialog()) { dialog.Title = "Application"; dialog.MainInstruction = "The file could not be opened."; dialog.Content = e.Message; dialog.ExpandedInformation = e.ToString(); dialog.MainIcon = TaskDialogIcon.Error; dialog.PositionRelativeToWindow = true; dialog.AllowDialogCancellation = true; dialog.Buttons = TaskDialogButtons.Close; dialog.ExpandFooterArea = true; TaskDialogButton button = new TaskDialogButton( "&Send Report", 0); dialog.CustomButtons.Add(button); if (button.Id == dialog.ShowDialog(this)) { // Send error report... } } }

Conclusion

Windows Vista is a major upgrade to the Windows OS and includes a considerable upgrade to the Windows SDK. Of the many new features, a few like the Kernel Transaction Manager, Aero wizards, and task dialogs are worthy of a closer look. The KTM allows you to focus on application logic while the operating system takes care of all the complex rollback logic required to ensure that your applications have no unwanted side effects. Aero wizards combined with command links provide a fresh and simple new interface for interacting with users. And the task dialog provides a powerful new API for simple communication and represents a very real replacement for the aging message box.

Kenny Kerr designs and builds distributed apps for Windows. He also has a particular passion for C++ and security programming. Reach Kenny at weblogs.asp.net/kennykerr or visit his Web site.