The official source of product insight from the Visual Studio Engineering Team
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:
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.
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:
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).
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.
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: