This article discusses:
- The new ManagedSpy utility
- Understanding how ManagedSpy works and helps in debugging
- A look at the inner workings of ManagedSpyLib
- Using ManagedSpyLib for unit testing
|
This article uses the following technologies: .NET Framework 2.0 |
Code download available at:ManagedSpy.exe(284 KB)
Contents
Many developers use the Spy++ tool provided with Visual Studio®. With Spy++, you can understand the window layout of a running application or identify a certain window message that causes a bug. However, when you create a Microsoft® .NET Framework-based application, Spy++ becomes less useful because the window messages and classes intercepted by Spy++ don't correspond to anything a developer uses or even sees. What a developer really wants to see are managed events and property values.
This article describes how to use a new utility called ManagedSpy and its associated library ManagedSpyLib, both of which are available for download from the MSDN®Magazine Web site. Similar to how Spy++ displays Win32® information such as window classes, styles, and messages, ManagedSpy displays managed controls, properties, and events. ManagedSpyLib allows you to programmatically access Windows® Forms controls in another process. You can get and set properties and sync on events in your own code. ManagedSpyLib can also help you build test harnesses and can perform window, message, and event logging.
Spy on Your UI
When writing client applications, there are many cases where a traditional debugger isn't useful. For example, if your bug involves focus or other UI aspects, it's difficult to debug because the debugger modifies this state whenever you hit a breakpoint. Another problem that's difficult to debug is layout. If your form has a complex and dynamic layout, it's not always obvious whether your layout logic is invoked multiple times. To debug these problems, you usually must resort to event or message logging to get an idea of what input is being fed to your UI.
With complex UI, it is useful to have a view of windows and associated states. For example, it can be difficult to locate the relevant control objects in the debugger. Most of the time you must guess that a debugger variable is the one you are looking at in the UI.
Figure 1 shows a dialog box with a few nested controls. This app has a bug in the top-right textbox, though for the purposes of this example it doesn't really matter what the bug is. It would be useful to identify not only which member is the red textbox, but also the parent hierarchy and layout of related controls.
Figure 1 Problem Dialog Box
ManagedSpy can help with this scenario and others. It displays a treeview of controls in your .NET-based client application. You can select any control and get or set any property on it. You can also log a filtered set of events that the control raises. While great for debugging, this can also help in compatibility testing of your control. You can use real applications and log events to ensure that event ordering is preserved for the next version of your control.
When you first run ManagedSpy, it displays a list of processes in a treeview in the left side of the window and a PropertyGrid on the right side. You can expand the process to see top-level windows in that process.
When you select a control, the PropertyGrid shows properties on that control. You can examine or change property values here. You should note that custom types are supported as long as they are binary serializable (see
Basic Serialization).
The toolbar contains commands to select which events get logged to the event pane, to refresh the TreeView when new windows have been created, to start or stop logging of events to the event pane, and to clear the event pane.
For the dialog box shown in Figure 1, ManagedSpy displays the information shown in Figure 2. According to ManagedSpy, textBox1 is parented to a SplitContainer (SplitContainer2), which in turn is parented to a TableLayoutPanel (tableLayoutPanel1). The parent of the TableLayoutPanel is a TabControl, which is in yet another SplitContainer. Note also that ManagedSpy tells me that the BackColor is Red.
Figure 2 Debugging a Control in ManagedSpy
Clicking on the Events tab will display events such as MouseMove on the currently selected control in the treeview. To begin logging events, click the Start Logging button. The output will appear as shown inFigure 3.
Figure 3 Logging Events
There are usually many mouse events. You can filter these or other events from being logged by clicking the Filter Events button, which displays a dialog box that lets you specify which events are to be logged. The event filter dialog lists all events from the Type control. Any event that is declared in a derived class is controlled through the Custom Events selection.
Inside ManagedSpy
The main method in ManagedSpy is called RefreshWindows. Its job is to fill the TreeView with all of the processes and windows running on the desktop. The first thing it does is clear the TreeView and requery all of the top-level windows in the system:
private void RefreshWindows() { this.treeWindow.BeginUpdate(); this.treeWindow.Nodes.Clear(); ControlProxy[] topWindows = Microsoft.ManagedSpy. ControlProxy.TopLevelWindows; ...
Once it has a collection of the top-level windows, ManagedSpy enumerates each window and, if it is a managed window, adds it to the treeview:
if (topWindows != null && topWindows.Length > 0) { foreach (ControlProxy cproxy in topWindows) { TreeNode procnode; //only showing managed windows if (cproxy.IsManaged) {
Here, ManagedSpy is using the ControlProxy class defined in ManagedSpyLib. A ControlProxy represents a window running in another process. If the window is actually a System.Windows.Forms.Control, then IsManaged will be true. Since ManagedSpy can only show information on .NET Framework-based controls, it doesn't display other window types.
Now, for each top-level ControlProxy that is managed, ManagedSpy can find its owning process. Once the process has a node in the TreeView, ManagedSpy uses that as the parent TreeNode of the new ControlProxy entry:
Process proc = cproxy.OwningProcess; if (proc.Id != Process.GetCurrentProcess().Id) { procnode = treeWindow.Nodes[proc.Id.ToString()]; if (procnode == null) { procnode = treeWindow.Nodes.Add(proc.Id.ToString(), proc.ProcessName + " " + proc.MainWindowTitle + " [" + proc.Id.ToString() + "]"); procnode.Tag = proc; } ...
At this point, procnode is a TreeNode of the owning process. Its title is generated using information from System.Diagnostics.Process. The only other interesting point here is that ManagedSpy avoids showing windows from itself.
Finally, ManagedSpy adds another TreeNode under procnode to represent the Window (see Figure 4). ManagedSpy uses ControlProxy.GetComponentName and ControlProxy.GetClassName as the title of the TreeNode. GetClassName refers to the System.Type of the remote control—not the window class that Spy++ displays.
Figure 4 Adding a Window Node
string name = String.IsNullOrEmpty(cproxy.GetComponentName()) ? "<noname>" : cproxy.GetComponentName(); TreeNode node = procnode.Nodes.Add(cproxy.Handle.ToString(), name + " [" + cproxy.GetClassName() + "]"); node.Tag = cproxy; ... if (treeWindow.Nodes.Count == 0) { treeWindow.Nodes.Add("No managed processes running."); treeWindow.Nodes.Add("Select View->Refresh."); } this.treeWindow.EndUpdate();
Whenever you select a TreeNode, ManagedSpy places that TreeNode's Tag in the PropertyGrid displayed on the right side. This is how properties are displayed for the remote control. The following code shows how ManagedSpy displays its TreeView and all of its properties:
private void treeWindow_AfterSelect(object sender, TreeViewEventArgs e) { this.propertyGrid.SelectedObject = this.treeWindow.SelectedNode.Tag; this.toolStripStatusLabel1.Text = treeWindow.SelectedNode.Text; StopLogging(); this.eventGrid.Rows.Clear(); StartLogging(); }
I won't step through how events are logged, but it is no more complicated than displaying properties. ManagedSpy subscribes on the EventFired event of the selected ControlProxy. When this event fires, a new row is added to a DataGridView control to display the data (the DataGridView control is new to the .NET Framework 2.0).
Using ManagedSpyLib
ManagedSpy is written on top of a managed C++ library called ManagedSpyLib. The purpose of ManagedSpyLib is to allow programmatic access to .NET Framework-based windows in another process. ManagedSpyLib exposes a class called ControlProxy that represents a control in another process. Although it's not an actual control, you can access all of the properties and events of the control it represents.
ManagedSpyLib works by transferring data between the spying and spied processes using a memory-mapped file. For this to work, all data transferred between the processes must be binary serializable. The main mechanism used to communicate between processes is custom window messages and SetWindowsHookEx. This ensures that the destination code runs on the thread that owns the window you need to query. This is important because there are many operations that only work when called from the owning thread of a window.
There are two ways to create a ControlProxy. The first is by using ControlProxy.FromHandle, passing into the method the IntPtr representing the HWND of the target control. This returns to you a ControlProxy for the target. The HWND of a window can usually be found using Win32 methods such as EnumWindows or by using applications like Spy++. You can also obtain the HWND by accessing a Control's Handle property.
The second way is by using ControlProxy.TopLevelWindows. You call this static method to get an array of ControlProxy classes. You will get a ControlProxy for every top-level window on your desktop. Not all of these windows are represented by managed controls, however. To determine this, examine the properties of the ControlProxy to see if it is indeed a managed window. See the Properties section that follows for more information on what you can retrieve. Figure 5 provides an example that lists the number of top-level windows per process.
Figure 5 Listing Windows by Process
using System; using System.Text; using System.Diagnostics; using System.Windows.Forms; using System.Collections.Generic; using Microsoft.ManagedSpy; class Program { static void Main(string[] args) { Dictionary<int, int> topWindowCounts = new Dictionary<int, int>(); foreach (ControlProxy proxy in ControlProxy.TopLevelWindows) { if (!topWindowCounts.ContainsKey(proxy.OwningProcess.Id)) { topWindowCounts.Add(proxy.OwningProcess.Id, 0); } topWindowCounts[proxy.OwningProcess.Id]++; } foreach (int pid in topWindowCounts.Keys) { Process p = Process.GetProcessById(pid); Console.WriteLine("Process: " + p.ProcessName + " has " + topWindowCount[pid].ToString() + " top level windows"); } } }
Accessing Underlying Control Properties
One of the main reasons to use ControlProxy is to access properties from a control in another process. (The properties are described in
Figure 6.) To access these properties, you simply create a ControlProxy using ControlProxy.FromHandle or ControlProxy.TopLevelWindows, then call two methods to access the values. Call GetValue to obtain a property value from the underlying control in the spied process. For example, you can call GetValue with this code to obtain the Size property:
controlproxy.GetValue("Size")
Call SetValue to change a property value in the underlying control in the process you're watching. For example, the following sets the background color to blue:
controlproxy.SetValue("BackColor", "Color.Blue")
Figure 6 ControlProxy Properties
Property | Description |
IntPtr Handle |
Returns the underlying HWND of the Control. This would be equivalent to accessing the Handle property on the Control. |
array<ControlProxy^>^ Children |
Returns an array of ControlProxy classes that represent the child windows of the Control. |
Process^ OwningProcess |
Returns a Process object that the Control is running in. |
bool IsManaged |
If the window that the ControlProxy is examining represents a managed System.Windows.Forms.Control, then the value returned is true. Otherwise it is false. Most ControlProxy methods only work if the window is a System.Windows.Forms.Control. |
Type^ ComponentType |
Returns the Type of the Control. Note that this Type is originally from an Assembly loaded in the process being spied. The Assembly and Type are reloaded in the spying process when the ControlProxy is first created. |
ControlProxy^ Parent |
Returns the parent window as a ControlProxy. Use Parent and Children to navigate the window chain of the process. |
To demonstrate the usefulness of ManagedSpyLib for editing properties across processes, I'll create a simple C# application. I add a textbox called textBox1 and a button called button1. I then double-click the button to create the button1_Click handler and add some code that includes the excerpt shown in Figure 7.
Figure 7 Modifying Other Instances of an Application
private void button1_Click(object sender, EventArgs e) { foreach (Process p in Process.GetProcessesByName("WindowsApplication1")) { if (p.Id != Process.GetCurrentProcess().Id) { ControlProxy proxy = ControlProxy.FromHandle(p.MainWindowHandle); string val = (string)proxy.GetValue("MyStringValue"); MessageBox.Show("Changing " + val + " to " + MyStringValue); proxy.SetValue("MyStringValue", (object)MyStringValue); } } } public string MyStringValue { get { return this.textBox1.Text; } set { this.textBox1.Text = value; } }
If I run two instances of the application, type some text into textBox1 of one instance, and then click button1, it will find all other running instances of this app and change their textbox strings to match, as shown in Figure 8.
Figure 8 Instances
You can subscribe to events such as Click or MouseMove on a control in another process. Subscribing to events is a two-step process. You must first call SubscribeEvent with the event name to have the ControlProxy listen on that event. You then subscribe on the ControlProxy event called EventFired:
private void SubscribeMainWindowClick(ControlProxy proxy) { proxy.SubscribeEvent("Click"); proxy.EventFired += new ControlProxyEventHandler( Program.ProxyEventFired); } void ProxyEventFired(object sender, ProxyEventArgs args) { System.Windows.Forms.MessageBox.Show(args.eventDescriptor.Name + " event fired!"); }
Note that when you are finished with a ControlProxy, you should unsubscribe from all previously subscribed events.
ManagedSpy itself uses the ControlProxy class to retrieve property values. For example, FlashCurrentWindow highlights the selected window for a few seconds. It also subscribes to events for its logging functionality.
Other ControlProxy Methods
There are a few additional methods worth taking a look at in ControlProxy. Call the SendMessage method to send a window message to the control. This is useful if you want to create a test harness. For example, you can send WM_CLICK or WM_KEYDOWN messages to simulate input. If you want to use ManagedSpyLib in this way, you can modify it so that the window hook procedure is always on and have it filter every window message except those you have programmed. This creates an automation driver that disables other input.
PointToClient and PointToScreen transform screen coordinates into client coordinates. The SetEventWindow and RaiseEvent methods are not intended to be used from user code. They are used internally to manage events cross process. ICustomTypeDescriptor allows an object to dynamically specify properties and events. ControlProxy implements this interface for PropertyGrid support. You can call these methods directly from user code, but it's usually not necessary. To access properties, use the GetValue and SetValue methods.
Using Window Hooks
As mentioned previously, ManagedSpyLib works by transferring data between processes. A window hook is a way to intercept window messages such as WM_SETTEXT. There are two methods of creating a window hook. SetWindowLong allows you to intercept window messages on a specific window in the same process. SetWindowsHookEx allows a wide range of message hooking, including the ability to hook messages for all windows for all processes in the current desktop.
Most developers who use native code will recognize SetWindowLong as the Win32 function that subclasses a window. After you have subclassed a window, Windows will send your callback method all of the Win32 messages destined for the window handle that you specified. This allows you to modify or just examine the message.
Note that SetWindowLong requires you to be in the same process as the window you are subclassing. If you want to do this type of subclassing, the .NET Framework makes it very simple by providing a class called System.Windows.Forms.NativeWindow. You may be asking two questions at this point.
- What if I want to see window messages and I am not in the same process as the target window?
- How does hooking window messages relate to ManagedSpyLib if it ends up showing managed information anyway?
If you want to see window messages and you are not running in the same process as the target window, you cannot use SetWindowLong. You can use SetWindowsHookEx with one caveat: for most types of hooks, your callback method must be exposed as a dllexport. This means you must write the callback in a native DLL or a mixed mode C++ DLL. ManagedSpyLib was written using managed C++ for this very reason. It uses the C++/CLI support in Visual Studio 2005.
There are two reasons why ManagedSpyLib uses window message hooking. To receive requests in a target process, it must be able to execute code in that process. SetWindowsHookEx allows you to do this. ManagedSpyLib also uses custom window messages to send and receive data between processes. This means its window hook must be activated when it sends a request (such as retrieving the BackColor of a Control in another process).
Using Memory-Mapped Files
But how exactly does ManagedSpyLib transfer data across processes? Sure, it can send a custom window message such as WM_SETMGDPROPERTY to set a value of a property. But if the property is BackColor, for example, how does it send BackColor.Red? Window messages only have two DWORDs as parameters.
The answer is that it uses a memory-mapped file. This is not actually a file on disk. It is an area of memory that is shareable among multiple processes. You map that memory into your own process address space. The consequence of this, however, is that the shared area has a different starting address. Therefore, you have to be careful storing data in it—no pointers! Also, you cannot have any managed objects in the memory-mapped file because the common language runtime (CLR) cannot manage that memory. This means you can only store raw byte data.
For this reason, ManagedSpyLib stores only binary serialized data. This is why properties (and EventArgs) must be serializable to be supported by ManagedSpyLib. ManagedSpyLib uses CAtlFileMapping to create a memory-mapped file for every transaction.
ManagedSpyLib calculates the size of the binary stream, creates a memory-mapped file of the right size, and then copies the data to it. Now that you have a general idea of how ManagedSpyLib uses window hooks to install itself and memory-mapped files to send data, let's take a closer look at how the ControlProxy class is created and maintained.
Creating a ControlProxy and Handle Recreation
Figure 9 illustrates how a ControlProxy is created (red arrows) and how it is maintained when its handle changes (blue arrows). The user initially calls either ControlProxy.FromHandle or ControlProxy.TopLevelWindows. TopLevelWindows will call EnumWindows and then FromHandle on each window enumerated. So you can think of TopLevelWindows as just a more complex FromHandle call.
Figure 9 Creating a ControlProxy
ManagedSpyLib turns on a windows hook for the thread that owns the destination window. Then ManagedSpyLib sends a WM_GETPROXY message to the destination window (the windows hook will be turned off once this message is handled). On the receiving side, the message is received and the command library calls Control.FromHandle to get the managed control running in the spied process. Using the control, ManagedSpyLib creates a new ControlProxy. This ControlProxy stores the Type.FullName of the control as well as the Assembly.Location of all loaded assemblies in the current AppDomain.
The ControlProxy subscribes to the Control's HandleCreated and HandleDestroyed events. It uses this later to maintain the proper window handle state. The ControlProxy is stored in the ProxyCache of the spied process and sent back to the spying process using binary serialization. The spying process deserializes the ControlProxy and adds it to its local ProxyCache. It then returns the ControlProxy to the user.
When the spied process recreates the handle for a control, ManagedSpyLib maintains the proper state. HandleDestroyed is received from the ControlProxy in the spied process. The ControlProxy checks Control.RecreatingHandle to see if the control is just performing a handle recreate. If the handle is being recreated, the ControlProxy waits for the corresponding HandleCreated. It updates the local ProxyCache, then sends a WM_HANDLECHANGED to the EventWindow of the spying process. The spying process locates the correct ControlProxy from the ProxyCache by looking up with the old window handle. It then updates the ControlProxy and the spying process's ProxyCache.
Figure 10 shows how ControlProxy gets properties (red arrows) and receives events (blue arrows). ManagedSpyLib performs the following sequence when you get a property value via ControlProxy.GetValue(propertyName). First, the spying process calls into ControlProxy.GetValue with the name of the property. ManagedSpyLib turns on a window hook for the thread that owns the destination window. This will be turned off once the message is handled. ManagedSpyLib stores the name of the property to get in a memory-mapped file (the Parameters section of the calling process's memory store). It uses binary serialization to do this.
Figure 10 Getting Proxies and Receiving Events
ManagedSpyLib sends a WM_SETMGDPROPERTY message to the destination window. The windows hook procedure (MessageHookProc) will be called inside the spied process to handle the window message. MessageHookProc will then process the command and uses reflection to get the return value. It stores the return value in the calling process's memory store. When SendMessage completes, the spying process deserializes the return value from its memory store. It sends a WM_RELEASEMEM to the same destination window to notify it that it can release its reference on the mapped file. Finally, it returns the value.
To subscribe and get events is similar. The spying process calls into SubscribeEvent, which stores the following in the Parameters section of the spying process' memory store: the EventWindow handle, the name of the event to subscribe to, and an event code unique to this event within this window (usually an index to the event in the event list).
SubscribeEvent sends WM_SUBSCRIBEEVENT to the destination control. Upon receiving WM_SUBSCRIBEEVENT in the spied process, ManagedSpyLib creates an EventRegister object that subscribes on the event and keeps track of what event it has subscribed on. When an event is fired, the EventRegister sends a WM_EVENTFIRED message to the Event window with the source window, event code and the EventArgs stored in the spied process' memory store.
The spying process handles WM_EVENTFIRED, parses the source window, event code and EventArgs, and calls RaiseEvent on the correct ControlProxy with the correct event and EventArg information. RaiseEvent raises the EventFired event on ControlProxy.
ManagedSpyLib for Unit Testing
With ManagedSpyLib, you can do testing without having to expose hooks from your application. To illustrate this, I created a new C# Windows Forms-based application called Multiply. I added three textboxes and one button, then double-clicked the button and added the following code for its Click event:
private void button1_Click(object sender, EventArgs e) { int n1 = Convert.ToInt32(this.textBox1.Text); int n2 = Convert.ToInt32(this.textBox2.Text); this.textBox3.Text = (n1 * n2).ToString(); }
All this application does is calculate two textboxes and show the result in the third textbox. The real point is creating a unit test app that works with this simple sample.
For the next step, I added a new C# Windows-based application to the solution and name it UnitTest. There's just a single button on the form along with the code shown in Figure 11.
Figure 11 Testing Code
private void button1_Click(object sender, EventArgs e) { Process[] procs = Process.GetProcessesByName("Multiply"); if (procs.Length != 1) return; ControlProxy proxy = ControlProxy.FromHandle(procs[0].MainWindowHandle); if (proxy == null) return; //find the controls we are interested in... if (cbutton1 == null) { foreach (ControlProxy child in proxy.Children) { if (child.GetComponentName() == "textBox1") { textBox1 = child; } else if (child.GetComponentName() == "textBox2") { textBox2 = child; } else if (child.GetComponentName() == "textBox3") { textBox3 = child; } else if (child.GetComponentName() == "button1") { cbutton1 = child; } } //sync testchanged on textbox3 so we can tell if it has changed. textBox1.SetValue("Text", "5"); textBox2.SetValue("Text", "7"); textBox3.SetValue("Text", ""); textBox3.EventFired += new ControlProxyEventHandler(textBox3_EventFired); textBox3.SubscribeEvent("TextChanged"); } else textBox3.SetValue("Text", ""); //now click on the button to start the test... if (cbutton1 != null) { cbutton1.SendMessage(WM_LBUTTONDOWN, IntPtr.Zero, IntPtr.Zero); cbutton1.SendMessage(WM_LBUTTONUP, IntPtr.Zero, IntPtr.Zero); Application.DoEvents(); } if (result == 35) MessageBox.Show("Passed!"); else MessageBox.Show("Fail!"); } void textBox3_EventFired(object sender, ProxyEventArgs ed) { int val; if (int.TryParse((string)textBox3.GetValue("Text"), out val) { result = val; } }
When you run the unit test application, it will change the first textbox to 5 and the second to 7. It then sends a click (via a mousedown and mouseup) to the button and looks at the final result (which is set in the event callback).
Conclusion
ManagedSpy is a diagnostic tool, similar to Spy++. It shows managed properties, allows you to log events, and is a good example of using ManagedSpyLib. ManagedSpyLib introduces a class called ControlProxy. A ControlProxy is a representation of a System.Windows.Forms.Control in another process. ControlProxy allows you to get or set properties and subscribe to events as if you were running inside the destination process. Use ManagedSpyLib for automation testing, event logging for compatibility, cross process communication, or whitebox testing.