Bugslayer
GUI Control to Major Tom
John Robbins
Code download available at: Bugslayer 2007_03.exe(199 KB)
Contents
Microsoft UI Automation
UISpy
Better UI Automation Testing
The UIWindow Class
The UIMenuBar Class
Wrap-Up
Tip
In my 14 years of Windows development, the absolute best demo I've ever seen was at the Longhorn Professional Developer's Conference (PDC) in 2003. While sessions on Windows® Presentation Foundation and the Visual Studio® 2005 betas excited many attendees, the program manager for Active Accessibility® got the only standing ovation that I've ever seen at a technical conference. He demonstrated a build of "Longhorn," which is now Windows Vista™, that allowed separate input queues for a GUI application. His demo was playing Solitaire in the foreground while a Microsoft® .NET-based application using the underlying automation APIs from Active Accessibility added and removed hardware devices from Device Manager in the background! He even clicked around in the Device Manager and all input was ignored because the controller program had complete control of that input queue.
The problem in Windows development is that when you attempt to automate a GUI application with a mouse and keyboard playback utility, such as the Tester application I developed in previous Bugslayer columns, simply bumping the mouse at the wrong time can break your automation. What was so exciting about the PDC demo was the realization of the dream of being able to automate the GUI portions of your application plus the guarantee that the playback would be exactly what you expected. If you're writing any sort of Windows-based application, you've got some part of it that has a GUI, be it the whole application, the install, or the latest in AJAX buzzword compliance. Being able to test the GUI portion of the application consistently and correctly is a key to great code.
As I was working on this column, Windows Vista was released to manufacturing. Sadly, the multiple input queues feature that garnered such great applause didn't make it to the final release. However, a good portion of the GUI automation did make it into Active Accessibility. Additionally, the new GUI automation portions are available through the .NET Framework 3.0, so it not only works on Windows Vista, but also Windows XP and Windows Server® 2003. Moreover, the Active Accessibility code works seamlessly with both Windows Presentation Foundation UIs and HWND-based applications. Perhaps the next operating system release will reach the ultimate goal of separate input queues, but with the new Microsoft UI Automation, getting the GUI portions of your applications automated is now quite a bit easier-and you can use it today.
My goal for this column is to show you how to get started with GUI automation using the UI Automation tools and APIs. As with most Bugslayer columns, there's a large set of code that I provide to make your GUI testing far easier than using the API directly. I'll assume that you've read over the documentation from msdn2.microsoft.com/ms747327.aspx; if you haven't, you should do it now or the rest of the column probably won't make much sense. As a reminder, you'll need the .NET Framework 3.0 installed as well as the Windows Vista and .NET Framework 3.0 Software Development Kit.
Microsoft UI Automation
As you read through the documentation for Microsoft UI Automation, you'll see the roots of life are truly in Active Accessibility. Much of the discussion centers around finding all the particular child controls in a window. As screen readers and other assistance tools need that information quickly, it's nice to have an API to easily get those trees for both HWND-based as well as Windows Presentation Foundation applications without having to write two separate sets of code on your own.
There's also an excellent discussion on how to write your own providers, which serve up your custom controls through the Microsoft UI Automation API in a uniform manner. If you're writing your own unique custom controls, you'll definitely want to read the UI Automation Providers Overview (msdn2.microsoft.com/ms750446.aspx) so anyone using assistive technology can use your application. The work necessary to create a provider is not too onerous, but it's outside the scope of what I want to cover for automated testing in this column.
One part of the Microsoft UI Automation API that's very interesting is its excellent event support. While you can install a journal hook to record keyboard and mouse usage on a machine, there's no clean API to get notifications about which control has focus, when windows open and close, and so on. If you look back at the code for my Tester application, you can see that I was using Computer Based Training (CBT) hooks and a good bit of guessing to get those notifications for HWND-based applications. What I find most impressive about the new eventing system is that you can isolate the event notifications so that you can get everything from a single control, to a window with child controls, to the whole desktop. For an example of using the eventing controls to record an application's usage, the Test Script Generator Sample is a big help (msdn2.microsoft.com/ms771275.aspx).
A key point of the whole Microsoft UI Automation API is that everything is based around a control type. Instances of the AutomationElement class wrap each control, which allows you to quickly identify what a particular control on the screen is and what it can do. In the past, it took a commercial application to provide that level of detail, and if you've ever had to deal with any of those expensive and custom automated testing tools in the past, you'll realize how nice it is to have everything in a free download.
There are 38 defined control types, covering everything from a button to a data grid (msdn2.microsoft.com/ms749005.aspx). What makes the provider model interesting from an automation testing standpoint is that these control types show exactly what properties and control patterns each supports. As you can guess, the properties are the subset of properties on the base AutomationElement class that are required or optional for that type of control. For example, a button control should support the AcceleratorKeyProperty to return the accelerator key used for that button.
You'll notice that the control patterns are a bit more interesting. The UI Automation Control Patterns Overview documentation (msdn2.microsoft.com/ms752362.aspx) sums it up best: "Control patterns provide a way to categorize and expose a control's functionality independent of the control type or the appearance of the control." As you'll see later in the column, we'll work a lot with control patterns in automating a GUI. What makes the control patterns so nice is that once you get the particular pattern for the control, the pattern will do the work of performing that action. For example, if you're dealing with a button, the IInvokePattern interface exposes an Invoke method that will do the button click. That way you don't have to wrestle with Windows messages or the SendInput API.
While there's not much in the documentation on using the API for automation testing, between the samples and a bit of reading, it's not too hard to see what's necessary. I do have to give the team who designed the UI Automation API some major credit for their work on internationalization. With previous tools and APIs for automating, you were forced to embed strings like "File" and "Open" to look for menu items. This meant that you had to provide all the internationalization inside your GUI automation scripts, which complicated your life and reduced the reusability of your testing scripts. Today, when you want to access a particular control in your application, you can use the AutomationId property. This name is unique and guaranteed to be the same on any machine no matter the locale. Now you can reuse those scripts and help your translators immensely because they can test your applications just as you did.
One UI testing feature missing from the UI Automation API is mouse support. There's none at all. For most applications, that's not much of a problem because the control patterns provide the equivalent functionality. However, if you're working on a painting application, you won't find any help to draw those smiley faces with a mouse. In that case, you can still use something like Tester, which does offer mouse support, in conjunction with a UI Automation API-using program to get you to the point where you need to rely on the mouse.
UISpy
Before I jump into the cool code I wrote to make your UI testing quite easy, I need to talk about a tool that you're going to use extensively to assist you in writing your tests: UISpy (shown in Figure 1). You can find UISpy in the Windows Vista and .NET Framework 3.0 Software Development Kit. Essentially, UISpy is a tool to walk and show in the Microsoft Automation UI format all the controls from the desktop on down so you can start seeing what the properties are for your particular controls. Additionally, as you play around with the Microsoft Automation UI API, you can execute patterns on controls as well as query for particular properties.
Figure 1** UISpy User Interface **(Click the image for a larger view)
Simply using the tool and clicking through the UISpy menus will teach you how everything works. However, I've spent a huge amount of quality time with UISpy getting the code that accompanies this month's column working correctly, and to save you a ton of hassles, I want to discuss a few of the snags I ran into.
The tree controls in UISpy show you the hierarchy for a particular view: Raw, Control, or Content. The Raw View shows the hierarchy most closely related to the particular native programmatic structure, and is one you'll rarely use. The Control View shows the hierarchy as it maps to what the user perceives when looking at the user interface. The Content View, which is a subset of the Control View, shows just the controls that have true information in a user interface. When using the Microsoft Automation UI API, you'll access the controls in your application using one of these hierarchies.
In UISpy, there's appears to be a bug in the Control View tree control: UISpy does not always update the selection when you right-click on a node. As the right-click menu is extremely important for refreshing the view, setting focus, or accessing control patterns, you'll find yourself executing the menu options on the previous selected node. Whenever you want to access the right-click menu, always select the item first with the mouse, and then right-click on it. I don't know how many times I was looking at the control patterns for the wrong control!
The documentation for the Microsoft Automation UI API says in several places that using the root element, the Desktop Window, as the start of your searches is extremely slow. Trust the documentation on this. Whenever you're using UISpy to look at your application, immediately select and right-click the child node for your application and select Scope to Element. That will isolate all focus tracking and highlighting to only the application you're interested in using.
One great feature of UISpy is the Hovering mode, which allows you to move the mouse over a control on the screen and, when you press the Ctrl key, jump to that node in all the open tree controls. The Hovering mode is especially important when you're looking at menu items because until a menu is expanded you won't see the child nodes. The problem is that UISpy doesn't always realize that the Ctrl key has been pressed, so get in the habit of pressing the Ctrl key multiple times.
The last, and worst, issue with UISpy isn't UISpy's fault, but it's something that will drive you bonkers when trying to figure out how to automate portions of the user interface you don't own. A perfect example is the new common Open dialog on Windows Vista. Looking at the dialog, most of the controls look like typical HWND-based edit boxes, buttons, list controls, and so on. But don't let your eyes deceive you. What looks like the Open button is sometimes reported in UISpy as a button control and sometimes as a pane control, which, according to the documentation, "represents a level of grouping lower than windows or documents, but above individual controls."
If you don't own the user interface you're trying to automate you'll need to inspect it with UISpy numerous times to see exactly what the control type really is. Between using the Hovering mode and the full Focus Tracking mode, which jumps to the control with focus in the UISpy tree views, you'll eventually find the exact type of those mystery controls.
Better UI Automation Testing
Once I got a handle on using UISpy and started getting a feel for the basic API, I began using the Microsoft UI Automation API to drive everyone's favorite application, Notepad. My plan was to learn about the API so I could show how to integrate automation testing into the wonderful unit-testing tool in Visual Studio Team System. You can use NUnit (nunit.org) to do essentially the same thing.
Using the straight Microsoft UI Automation API, I wrote an application that did the following:
- Ensured the process was DPI aware
- Started Notepad and executed the File | Open menu
- In the Open dialog, opened the source code for the program
- Executed the Help | About menu and clicked the OK button
- Executed the File | Exit menu
You can see part of this program in Figure 2, and I strongly suggest you read down all the code and comments as it will give you an excellent idea how to use the Microsoft UI Automation API. Please note that there is no error handling in the code to focus strictly on the core API usage.
Figure 2 Raw Microsoft UI Automation Usage
#region Using Statements
using System;
using System.Diagnostics;
using System.Collections.Generic;
using System.Text;
using System.Windows.Automation;
using System.Windows.Automation.Provider;
using System.Threading;
using System.ComponentModel;
using System.IO;
using System.Globalization;
using System.Windows.Forms;
using System.Runtime.InteropServices;
#endregion
namespace RawUsageExample
{
internal static class NativeMethods
{
// The Windows Vista DPI aware function.
[DllImport ( "user32.dll" , EntryPoint = "SetProcessDPIAware" )]
[return: MarshalAs ( UnmanagedType.Bool )]
private static extern bool RealSetProcessDPIAware ( );
internal static bool SetProcessDPIAware ( )
{
Boolean returnValue = false;
try
{
returnValue = RealSetProcessDPIAware ( );
}
catch ( EntryPointNotFoundException )
{
// Not running on Windows Vista.
}
return ( returnValue );
}
}
class Program
{
[STAThread]
static void Main ( )
{
// Set this process to DPI aware if running on Windows Vista.
NativeMethods.SetProcessDPIAware ( );
// Start Notepad and connect up to it.
// Wait for the process to get going enough to have a UI.
Process proc = Process.Start ( "notepad" );
Thread.Sleep ( 1000 );
// Attach to the main window.
AutomationElement rootElement =
AutomationElement.FromHandle ( proc.MainWindowHandle );
// Create the AndCondition to find the menuBar.
AndCondition menuBarFind = new AndCondition (
new PropertyCondition (
AutomationElement.ControlTypeProperty,
ControlType.MenuBar ) ,
new PropertyCondition (
AutomationElement.AutomationIdProperty, "MenuBar" ) ,
Automation.ControlViewCondition );
// Find the menuBar, which is a child of the main window.
AutomationElement menuBarElement =
rootElement.FindFirst ( TreeScope.Children, menuBarFind );
// Create the AndCondition to find the File menu.
AndCondition fileMenuFind = new AndCondition (
new PropertyCondition (
AutomationElement.ControlTypeProperty ,
ControlType.MenuItem ) ,
new PropertyCondition (
AutomationElement.AutomationIdProperty , "Item 1" ) ,
Automation.ControlViewCondition );
// Find the File menu.
AutomationElement fileMenuElement =
menuBarElement.FindFirst ( TreeScope.Children ,
fileMenuFind );
// Get the control pattern for ExpandCollapse and do the
// Expand to get the children.
ExpandCollapsePattern fileExpandPattern = fileMenuElement.
GetCurrentPattern ( ExpandCollapsePattern.Pattern )
as ExpandCollapsePattern;
fileExpandPattern.Expand ( );
Thread.Sleep ( 1000 );
// Create the AndCondition to find the Open menu.
AndCondition openMenuFind = new AndCondition (
new PropertyCondition (
AutomationElement.ControlTypeProperty ,
ControlType.MenuItem ) ,
new PropertyCondition (
AutomationElement.AutomationIdProperty , "Item 2" ) ,
Automation.ControlViewCondition );
// Find the Open menu.
AutomationElement openMenuElement =
fileMenuElement.FindFirst ( TreeScope.Descendants,
openMenuFind );
// Get the InvokePattern off the openMenu.
InvokePattern openInvokePattern =
openMenuElement.GetCurrentPattern (
InvokePattern.Pattern ) as InvokePattern;
// Invoke the menu to get the Open dialog.
openInvokePattern.Invoke ( );
Thread.Sleep ( 1000 );
...
As you read through the code in Figure 2, your eyes will probably glaze over because, while the Microsoft UI Automation API gives you a great deal of power, it has to be one of the most intensively fiddly APIs ever developed. Most of the code in Figure 2 simply opens the File | Open menu. Using the Microsoft UI Automation API directly means you have to do the slightly different 5 to 10 lines of code over and over (and over and over!). In fact, the full code takes 250 lines to perform the steps in the five bullet points I discussed earlier. The problem with APIs like this is that a developer will resort to "editor inheritance" (also known as cut and paste) to build up their unit tests, resulting in all sorts of bugs in the code. In turn, this will lead to the discontinued use of the API.
While I can see what the API designers were trying to accomplish, the big mistake, is that it's a flat API; all you're working with is a single class, AutomationElement, for every single control type. This leads to seven lines of code to find the File menu, find the ExpandCollapsePattern, perform the actual expand, and give the GUI time to show the menu. Sometimes when you attempt infinite flexibility, you produce APIs that are harder to use than they really need to be.
Right after my second attempt to learn the Microsoft UI Automation API, I simply had to work at encapsulating each of the 38 control types in their own classes so you would be able to figure out the exact type of control you are working with. Plus I wanted to hide as much of the continual grunt work in the raw API as possible, but still provide access to the underlying API for any advance work you need to perform. The code in Figure 3 shows the same program in Figure 2, but using the Bugslayer.TestTools.GuiAutomation namespace code. Again, I don't have any error checking to keep the focus on the code.
Figure 3 Bugslayer.TestTools.GuiAutomation Wrappers
#region Using Statements
using System;
using System.Diagnostics;
using System.Collections.Generic;
using System.Text;
using System.Windows.Automation;
using System.Windows.Automation.Provider;
using System.Threading;
using System.ComponentModel;
using System.IO;
using System.Globalization;
using System.Windows.Forms;
using Bugslayer.TestTools.GuiAutomation;
#endregion
namespace UIUsageExample
{
class Program
{
[STAThread]
static void Main ( )
{
// Start Notepad and connect up to it.
// Wait for the process to get going enough to have a UI.
Process proc = Process.Start ( "notepad" );
Thread.Sleep ( 1000 );
UIWindow notepadWindow = new UIWindow ( proc );
// Now we're on the main window so execute the
// File Open command.
UIMenuBar menuBar = notepadWindow.FindChild (
"MenuBar" , ControlType.MenuBar ) as UIMenuBar ;
// This wraps all the goo necessary to invoke the Open menu.
menuBar.InvokeMenus ( "Item 1" , "Item 2" );
...
The code in Figure 3, as you might notice, is much shorter than Figure 2. In fact, the full program is only 81 lines long, which is 32 percent of the original code! I'm also willing to bet that you'll find that the code using Bugslayer.TestTools.GuiAutomation is far easier to read and understand. And, of course, if the code is easy to use, you're more likely to use it.
The UIWindow Class
At this point, you may want to download the code for this month's Bugslayer to get a good look. You can follow along because I want to show you some of the implementation details. If you have Visual Studio Team Developer Edition or Visual Studio Team Suite, open the Bugslayer.TestTools.GuiAutomation\Bugslayer.TestTools.GuiAutomation.SLN file to see both the code and the unit tests. If you only have the Visual Studio Professional Edition, you'll want to open UIUsageExample\UIUsageExample.SLN so you won't get errors about missing projects and references. Both solution files include the Bugslayer.TestTools.GuiAutomation.CSPROJ file, which contains all the core code.
The class you'll want to examine is UIWindow, as that's the basis of all life for Bugslayer.TestTools.GuiAutomation. It wraps the AutomationElement class, which is exposed as the RootElement property. Nearly all the actual functionality for the derived classes is found in UIWindow. For example, all the most common properties you'll access on AutomationElements such as AutomationId, Name, IsEnabled, and so on, are there.
The child control finding methods are also on UIWindow. In thinking about how developers would use my API, I felt that the two main searches would be for a direct child control and occasionally for descendent controls. Consequently, I have numerous FindChild/FindDescendent methods that take the AutomationId to look up. Additional overloads let you also specify the exact ControlType so you can be sure you're finding the exact control you expect.
The AutomationId values are excellent for internationalization, but you do have to be very careful when searching down descendent trees because the values get reused. For example, the File menu on a menu bar has a value of "Item 1," but expanding the File menu, the Open menu item will also have a value of "Item 1."
In addition to searching by AutomationId values, I also support searching by the localized name of the control with the FindChildByName and FindDescendentByName methods. No matter what you search on, my Find* methods are wrappers around AutomationElement.FindFirst method so they only return the first item matching the specific condition. I chose to do that because when driving a GUI application, you know in advance which controls are in the user interface so you're generally not wondering what controls are on the screen. If you do need to call the AutomationElement.FindAll method to return all the matching controls for validation purposes, grab the UIWindow.RootElement property to search and sort all you want.
If you read about finding controls in the documentation, you saw there was a big discussion of searching in RawViews, ControlViews, and ContentViews. The UIWindow. Find* methods default to searching on the Automation.ControlViewCondition. If you want to change the searching for a particular UIWindow, set the TreeSearchCondition to Automation.ContentViewCondition, Automation.ControlViewCondition, or Automation.RawViewCondition.
The final point I want to mention about the Find* methods is that you may want to read through the UIWindow code, because it will show how I refactored the code down to some very small methods and how to think about finding controls in the Microsoft UI Automation API.
The big work in the UIWindow class is to provide protected wrappers around the control patterns. The 38 different control types use a number of the control patterns and I wanted to bury all the details of accessing and using the patterns in simple wrapper properties and methods. For example, a menu item control type may implement the ExpandCollapsePattern. My code has a UIMenuItem class that has a SupportsExpandCollapsePattern that calls the protected static UIWindow.ImplementsExpandCollapse method. The UIMenuItem class has public methods and properties for the rest of the ExpandCollapsePattern, ExpandCollapseState, Expand, and Collapse that you can call if SupportsExpandCollapsePattern returns true.
By implementing the control pattern code all in UIWindow, other control types, such as UIComboBoxControl (which also may support the ExpandCollapsePattern), just need to implement the public methods for the pattern. My idea is to expose only the methods and properties those classes support so it's much easier to use the API.
The big challenge that I faced when designing Bugslayer.TestTools.GuiAutomation was figuring out how to have the UIWindow.Find* methods return the exact type. If you look at UIWindow.FindDescendant for example, it returns a UIWindow. I wanted my actual automation test code to find a menu item and get back the UIMenuItem class. That meant I was going to have to give the UIWindow class a priori knowledge of the derived classes. While this idea was not exactly pure in the object-oriented sense of the word, it definitely works and makes Bugslayer.TestTools.GuiAutomation far easier to use. If I didn't do this, you'd have to write code like this:
UIWindow rawMenuBarWindow = notepadWindow.FindChild (
"MenuBar" , ConwwtrolType.MenuBar );
UIMenuBar menuBarWindow = new MenuBar ( rawMenuBarWindow.RootElement );
Whenever the UIWindow methods need to create a new UIWindow, it calls the UIWindow.AllocateExactType method, which takes an AutomationElement as the only parameter. Inside AutomationElement, AllocateExactType looks at the ControlType property and allocates the appropriate derived class. By doing the exact class type allocation, you can write code like the following using the as operator in all its glory:
UIMenuBar menuBar = notepadWindow.FindChild (
"MenuBar" , ControlType.MenuBar ) as UIMenuBar ;
If you add additional control types to Bugslayer.TestTools.GuiAutomation, you must make sure to add the appropriate allocation in AllocateExactType. I'll trade API ease of use over object-oriented purity any day.
One slightly interesting point concerns entering text. If you read through the control types documentation, you'll see that only a few control types support the TextPattern and ValuePattern necessary for getting and setting text. What I decided for those particular controls was to make the UITextControl class, which models a static text control, serve as the base class for all controls that support TextPattern and ValuePattern. That way the functionality is shared and I can build up a hierarchy where UIEditControl and UIToolTopControl are both derived from UITextControl. In addition, UIDocumentControl is derived from UIEditControl. This supports code reuse and also ensures that the correct ControlType is being used with the appropriate class.
In the Microsoft UI Automation API, any control that allows for multiple line input is considered a ControlType.Document. The documentation says that document controls are never supposed to support the ValuePattern, which means there's no way to set the value of those controls. (The TextProvider will give you the data from a document control.) This makes sense, as every program that supports data more complicated than plain text would be extremely hard to model. However, as sometimes just pumping text into a document is useful, I wrapped the SendKeys class from System.Windows.Forms into UIWindow. I also put the smarts in the class to ensure the focus is set to the modeled window before performing the actual keyboard input.
The UIMenuBar Class
The last interesting helper I want to discuss is the UIMenuBar class. When using the Microsoft UI Automation API, executing normal dropdown menus involves finding the menu, expanding it, finding the appropriate child, and invoking it. You can see how tedious the work back in Figure 2 was simply to execute the File | Open menu. Seeing that working with menus is so painful, the UIMenuBar.InvokeMenus command can take variable-length string parameters and perform the common work in a single call. The first string is assumed to be the initial menu that's a child of the menu bar and all subsequent parameters, except for the last, are menus to be expanded. The last menu is generally the one with the InvokePattern so UIMenuBar.InvokeMenus will invoke it. As you can see from Figure 3, it certainly cuts down on a huge amount of code you would normally have to write.
Wrap-Up
Now that you have what I think is an easy-to-use library in Bugslayer.TestTools.GuiAutomation, I hope you'll look hard at getting your user interface automation tied into your unit tests. Like anything else involving testing code, you'll need to treat it as a deliverable like the product, but the payoff is huge because code quality goes through the roof!
If you're looking for other ideas on how to use Bugslayer.TestTools.GuiAutomation, make sure to check out the unit tests in the Bugslayer.TestTools.GuiAutomation\Tests\Bugslayer.TestTools.GuiAutomation.Tests directory. Those tests use all aspects of the library in order to test it. As a huge believer in code coverage, I'm proud to report the existing unit tests hit over 90 percent of the code (on Windows XP)! If you have the appropriate version of Visual Studio, run the tests because it's wild to see five or six different instances of Notepad being automated at the same time (the tests run in random order and can thus be interleaved).
I want to leave you with a couple of ideas on how to extend Bugslayer.TestTools.GuiAutomation. As you look at the code, you'll see that I haven't modeled all the control types. Add the rest of the control types following the development patterns I've established. If you do add additional control types, make sure to add the appropriate unit tests.
The lack of mouse support in the Microsoft UI Automation API is a problem for many applications. Consider adding a PlayMouse method to UIWindow that takes a string in the SendKeys format and performs the operations using the SendInput API. You can look at how I implemented the PlayKeys command in Tester for help with building and parsing and the SendInput array.
If you were really motivated, you could build a recorder that would watch your interaction with an application using the excellent eventing support in the Microsoft UI Automation API. From the interaction, you can create a C# or Visual Basic® source file ready for compilation and testing.
If you do any of the above, please let me know, as I'll be happy to coordinate the work on Bugslayer.TestTools.GuiAutomation.
Tip
Tip 77 I love Team Foundation System, but was faced with a major problem the other day. I had to move work items from one project to another. Just as I was about to start crying, thinking I was going to have to do each item manually, Eric Lee came to the rescue with his Hemi tool, which moves work items between projects via a few mouse clicks. See how to use Hemi and download this wonderful free tool at blogs.msdn.com/ericlee/archive/2006/11/20/work-item-moving-tool-is-back.aspx.
Send your questions and comments for John to slayer@microsoft.com.
John Robbins is a cofounder of Wintellect, a software consulting, education, and development firm that specializes in both .NET and Windows. His latest book is Debugging Microsoft .NET Applications (Microsoft Press, 2006). You can contact John at www.wintellect.com.