WPF in Visual Studio 2010 – Part 5 : Window Management
This is the fifth article in the WPF in Visual Studio 2010 series. This week, guest author Matthew Johnson presents a fascinating behind-the-scenes look at Visual Studio’s innovative window management system. – Paul Harrington
Several people have expressed interest in how much effort was required to write a WPF window management system that supports docking and undocking windows, reordering tabs, and persisting layout. The answer: we were able to use lots of WPF fundamentals, but wrote many custom controls. I’ll walk through some of the major features of the WPF window management system and explain how we implemented what we did.
One thing to keep in mind while reading this article is that a version of the Visual Studio window layout system is also used by the Expression 3 products (Expression Blend, Expression Web, Expression Design). Of course, Expression heavily customized the look-and-feel, but the underlying implementation and features are shared.
One of the first areas we tackled when designing the windowing system was how layout would work. Layout has two main underpinnings:
- Measurement and arrangement of windows
- Persistence of layout across multiple sessions
Data model and persistence
Practicing data and UI separation was the easiest way for us to decouple the visual appearance of the windowing system from the persistent layout data. Non-leaf elements contain information about the relative location and size of splitters, tab groups, document groups, and floating windows. The leaf elements are placeholders for tool windows and documents. In the layout tree, the only information stored has to do with sizes, positions, and monikers which uniquely identify a tool window or document.
To persist this data, we initially planned to use the XamlServices class to read and write the data model as XAML. However, general-purpose XAML reading and writing inherently relies on reflection to gather information about types. Since reading window layouts is on Visual Studio’s startup path, our performance testing found that this was adding dozens of milliseconds per window layout (and for Visual Studio, dozens of milliseconds adds up quickly). To squeeze out more performance, we wrote a serializer generator that understands our specific object schema, which is faster since the “type understanding” happened at code generation time, not at runtime. This gives additional security benefits for us as well, since our parser will only instantiate known objects (using the general-purpose XamlServices would have allowed any object to be instantiated at startup without our direct knowledge, if the persisted window layout were modified).
The layout data tree is converted into actual controls using WPF’s DataTemplates. Visual Studio keeps a separate DataTemplate for each data model type in its Application’s ResourceDictionary. There is one caveat here: top-level windows (like Visual Studio’s floating documents and tool windows) can’t easily be built from DataTemplates directly. Instead, these root layout elements are constructed directly by a special control factory. After those are constructed, each other data model element is built into a control simply by being placed in a ContentControl or ItemsControl.
For styling our controls, we use WPF’s generic theme dictionaries extensively. Every control in the logical tree (the controls built from DataTemplates) are subclasses of a WPF base class, and override the DefaultStyleKeyProperty. We then provide our base styles in the generic theme dictionary. Custom style overrides can be placed in the Application’s ResourceDictionary, which has a higher priority than the generic theme dictionary. In fact, we use style overrides for Visual Studio itself—anything that is Visual Studio-specific (such as referencing colors from Visual Studio’s color table) is kept out of the sharable code. The “Styles, DataTemplates, and Implicit Keys” section of the Resources Overview and Guidelines for Designing Stylable Controls article explain this approach in more detail.
This diagram summarizes the different parts involved in turning layout data into a complete visual tree for Visual Studio.
We were able to use many standard WPF controls as part of the layout of the windowing system. For example, we a derivation of the TabControl for tab groups. However, we did invest a lot into custom controls to add functionality beyond what exists in WPF itself. Some of these are described in the deep dives below.
Docking and undocking windows
The docking system is what allows users to tear off tool windows or documents and re-arrange their positions. When I say “dock adornment”, I’m specifically referring to the directional semi-transparent tiles that appear when you start to drag a floating window. Preview shadows (the blue semi-transparent rectangle in the screenshot) give an indication of the position a floating window will have once you release it over a specific dock adornment.
To understand how docking works, it’s helpful to understand specifically the data model for the main window looks. For the above screenshot, the data model tree for the main window looks something like this:
When each of these data model elements are translated into the WPF visual tree (via DataTemplates and ControlTemplates), a custom control we created called DockTarget is inserted into places above which a dock adornment should show. For example, each TabGroup becomes a TabGroupControl in the visual tree, and TabGroupControl’s ControlTemplate inserts a DockTarget around the content space. There is also a DockTarget for the title, and a DockTarget around each tab.
Some DockTargets indicate that an adornment should be shown (like the DockTarget around the content space). Other DockTargets (like the one around each tab and the one around the title bar) don’t request an adornment, but still provide a preview shadow.
DockTargets serve both as a marker for where to show dock adornments and previews, but also as the controller for what kind of docking action occurs when you drop a window over that target or the adornment it produces. While dragging a floating window, each mouse-move causes our dock manager to perform a hit test using WPF’s hit testing system (actually, a hit test is performed on the window on the element in the set of all (main window + floating windows) which is found to be under the mouse and on top of the z-order). The DockManager then walks up the visual tree and enumerates all of the DockTargets in the ancestry of the hit element. The DockManager uses these DockTargets to reposition dock adornments and the dock preview window accordingly. In this way, we can make changes to when and how dock adornments and preview appear simply by changing the ControlTemplates for controls in the main window.
For the dock adornment and dock preview windows themselves, we use a top-level HwndSource to present the visual appearance. Although the term “adornment” is shared with a WPF concept, we could not use WPF’s adorner layer primarily because we want the adornment and preview to be on top of two different top-level windows: the floating window being dragged and the target window. The only way to accomplish this is with a third top-level window (you would be surprised how much more confusing dock adornments and previews would be if their content was covered up by the window being docked).
In Visual Studio, auto-hidden windows can be shown in a flyout form, which overlaps other docked windows. These windows are part of the main window frame, and the auto-hide flyout is a sibling of each docked window. In previous versions of Visual Studio, every tool window or document was represented by an HWND. The auto-hide flyout HWND was simply kept always above the other HWNDs in z-order, and that kept its child subtree above other windows.
WPF airspace limitations effectively boil down to this rule: If you are a WPF surface presented in an HWND (an HwndSource or System.Windows.Window), your HWND has a single presentation surface for WPF content, and that single presentation surface is always below every child HWND. In the screenshot below, the Windows Forms designer and the Windows Forms Properties tool window are both HWND-based documents (highlighted in green), and consequently always render above the WPF main window. The auto-hidden Output window, however, is entirely WPF (highlighted in red).
To achieve overlap here, we had to wrap every auto-hidden flyout in its own HWND. This is implemented with an HwndHost (which puts the HWND into the main window’s visual tree) that contains an HwndSource (which allows WPF content to be presented in a new child HWND).
This sounds simple enough, but there were some additional issues we had to overcome:
- Each HWND which is a direct child of the main window has to have both WS_CLIPSIBLINGS and WS_CLIPCHILDREN applied as Windows styles. Without these styles, Windows will allow either window to overdraw its own siblings if their surfaces interesect.
- Many actions (such as acquiring focus) can change the z-order of an HWND. For example, if the Solution Explorer is auto-hidden and overlaps the docked Properties window, clicking on the “Properties” toolbar button can focus and activate the Properties window while leaving the Solution Explorer shown on top of it. The Properties window would normally come to the front of the z-order and overlap the Solution Explorer. To handle this case, we have WM_WINDOWPOSCHANGED handlers on each of the root sibling windows. When anything that’s not the auto-hide HWND has its z-order change, we make sure the auto-hide HWND is brought to the front again to compensate.
- WPF passes inherited properties through the logical tree. By using a child HwndSource, we effectively created a new logical tree that’s separate from the main window. For example, this prevents the TextOptions.TextFormattingMode property we set on the main window to improve text clarity from inheriting to the auto-hide flyout. However, this can be overcome! Our HwndHost (which is an element in the main window’s logical tree) uses AddLogicalChild to add the root visual of the HwndSource to the main window’s logical tree. This allows inherited properties and resource lookup to flow naturally from the main window to the auto-hide flyout as if there were no HWND.
Custom window chrome
If you’ve floated any tool windows or documents, you’ve probably noticed that these windows have non-standard title bars and resize areas. However, the window frame and titlebar is entirely WPF—how were we able to achieve that look while still supporting new Windows 7 features like Aero Snap?
Our implementation was mostly handled using techniques from Joe Castro’s WPF Chrome project. In short, WPF Chrome works by handling several Windows messages (WM_NCCALCSIZE, WM_NCHITTEST, and others) to inform Windows that Visual Studio is drawing the entire non-client area, but that Windows should treat certain areas as non-client for hit-testing purposes. In this way, we did not have to implement any functionality specific to the moving, sizing, maximizing, or restoring of the window.
Beyond what exists in the WPF chrome sample, we only ended up needing to make a few changes to have operations work for owned windows (i.e. top-level windows that have their z-order and taskbar status tied to their owner window, the Visual Studio main window).
- When minimizing the Visual Studio main window, we explicitly minimize every floating window. Then, when the main window is restored, we restore each floating window to its previous state. The state transition is important in order to maintain the “semi-maximized” state of the window. Semi-maximized states are special states that Window maintains, such as “docked to one half of the monitor” (done with Win+Left/Win+Right or dragging to the monitor edge) or “expanded to fill the height of the monitor” (done by dragging the top or bottom resize grip to the top or bottom of the monitor, or double-clicking on the top or bottom resize grips). Without this state transition logic, the owned floating windows would restore to a non-semi-maximized state.
- When dragging to move or resize a window, Windows enters a move/size loop during which normal mouse messages aren’t dispatched (see WM_ENTERSIZEMOVE/WM_EXITSIZEMOVE/WM_MOVING). During the move-size loop, Visual Studio needs to be updating the dock adornments and previews mentioned above, so we handle these messages. This gives the added benefit that you can dock a floating window using the keyboard entirely. Alt+Space will show the system menu, and the Move option will enter the move-size loop. Once in the move-size loop, you can use the arrow keys to move the window over a dock adornment, press Enter, and dock the window without ever touching the mouse.
Arranging and reordering tabs
By default, WPF’s TabControl uses a layout panel called TabPanel. However, TabControl and TabPanel has several shortcomings that we needed to add additional support for.
- TabPanel’s implementation will always wrap tabs into multiple rows or columns once the tabstrip overflows. Visual Studio has traditionally displayed only a single row of tabs.
- TabControl doesn’t directly allow for items which are in the tab control not to have tab representations. By default, when the document well overflows, Visual Studio removes tabs that would overflow. When the tool window tabstrip overflows, the tabs themselves are truncated, and the text uses ellipses.
- There isn’t any out-of-the-box functionality for user-reordering of tabs.
In order to add these features, we wrote custom Panel subclasses called for dealing with resizing, single-row layout, and tab hiding.
The reordering logic is perhaps the most interesting. To summarize: when you start to drag tab, we capture the bounds of all of the tabs in the containing Panel. As you drag the tab, we detect whether or not the mouse has moved past the edge of the current tab in either direction. When it does, it swaps the data model order of those tabs (as well as the ordering of the bounds in the list of tab bounds). One tricky area: when the data model is swapped, WPF rebuilds the visual tree, and the UIElement that had captured the mouse for the drag operation is removed and a new control is built from template expansion. To continue the drag when the new element is created, we check in its OnInitialized override and see if the mouse left button is still pressed, and if our DockManager component still reports that a tab is being dragged. If so, we capture the mouse and resume the dragging operation for the new tab UIElement.
One lesson we learned from our Panel subclass is that over-measuring text can be expensive. Our initial implementation would always pass the Panel’s width to Measure on each child item (which is a finite value). However, we discovered that passing Double.PositiveInfinity for the width allowed WPF to optimize text measurement. Only if we detect that the text should be truncated (because of lack of space) do we calculate a finite value and then re-measure the truncated children.
Matthew Johnson – Developer, Visual Studio Platform
Short Bio: Matthew has been at Microsoft for three years and on the Visual Studio Platform for two years. He works on WPF-related features of the Visual Studio window layout system.
Many thanks to Matt for this incredibly informative article. We’d love to hear your feedback on this or any of the other articles in the series. Feel free to use the comment stream below. For next time, I’ve asked another guest writer and software tester extraordinaire, Phil Price, to give his perspective on what it took to test Visual Studio 2010’s new WPF look. – Paul Harrington
Previous posts in the series: