WPF in Visual Studio 2010 - Part 5 : Window Management

WPF in Visual Studio 2010 - Part 5 : Window Management

Rate This
  • Comments 21

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

Background

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.

Layout

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).

Control model

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.

LayoutDataStructure

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

DockAdornments

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:

MainWindowStructure

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.

HeaderDockTarget CenterDockTarget TabDockTarget

Header DockTarget

Center DockTarget

Tab DockTarget

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).

Auto-hide overlays

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).

HwndOverlapGreen

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:

  1. 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.
  2. 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.
  3. 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?

FloatingToolWindow

FloatingDocument 

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.

MattMatthew 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.

 

 

Thanks

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:

Leave a Comment
  • Please add 8 and 8 and type the answer here:
  • Post
  • Hi.

    Great article!

    But I can't help but wonder why these dock controls aren't public so we can use them in our own projects.

    Or maybe there is a way and I haven't found it yet?

    If they are, what library should I reference to access them?

    Regards

    Gert

  •  While the controls are being used across a couple of products they aren't simply 'drop in docking' support.  That coupled with the fact that if we released them for 'public use' that would also mean supporting them in all kinds of use patterns / third party apps, fixing/investigating bugs that don't crop up in VS/Blend, and a host of other things that Microsoft would probably not be willing to do :)  So long story short, they aren't intended to be widely used externally, perhaps in the future but not in the now.

    Ryan

  • @Ryan.

    I'm sorry to hear that.

    I really think it's a feature that has been missing since .NET V1. Especially considering that it's a standard feature of MFC.

    This is, without a doubt, the no. 1 request on my wish list. That and floating toolbars...

    But I appreciate your answer.

    Regards

    Gert

  • Thanks for your comments.

    While the Visual Studio 2010 window management library is not available for public use, you may be interested in third party solutions, just a few of which are linked below.

    http://www.codeproject.com/KB/WPF/WPFdockinglib.aspx

    http://www.devzest.com/WpfDocking.aspx?Show=Overview

    http://www.devexpress.com/Products/NET/Controls/WPF/Dock_Windows/

    http://www.telerik.com/products/wpf/docking.aspx

    http://www.actiprosoftware.com/Products/DotNet/WPF/Docking/Default.aspx

    http://www.devcomponents.com/dotnetbar-wpf/

    http://www.divelements.com/net/controls/sanddockwpf/

    Note: This is not an exhaustive list and neither is this meant as an endorsement of any of them.

    It has been said that imitation is the sincerest form of flattery :-)

  • @Paul.

    Thank you for the info.

    I have tried some of the free versions and while there are some good ones among them, I still think they lack something. Either persistance is not good, the structure of the controls arent as logical as I'd like them to be, or they just display odd behaviour.

    Also, I think it's sad to have to buy expensive 3rd party tools to achieve something that MFC programmers get for free.

    Regards

    Gert

  • I would like to know how MEF is used in Visual Studio 2010.

  • @DanielD

     That is a big topic :)  As far as the shell goes we have no usage of MEF.  That is likely to change moving forward, but we didn't have resourcing to provide MEF based methods of extending the shell proper in this release.  

     The new editor has its extensibility story built entirely around MEF (it still supports the 'old style' of extensibility through the old COM interfaces but they lack all of the new functionality the editor has and moving forward all extenders should use the MEF based extensibility for the editor).

     If there is a more specific question I can try to wrangle up some answers.  I believe a number of internal teams may also have added MEF based extensibility to their components in Dev10, but I don't have the list off the top of my head.

    Ryan

  • I'm interested in how MEF extensions are composed. Is there a single object that is composed and includes all imports? Or each service gets exports that it needs?

  • Thanks for the comments.

    @Gert: There is, as demonstrated by the links, some commercial value in providing a supported, fully-debugged and re-hostable WPF-based window management system. We (Microsoft) regard these "controls vendors" as a valuable and healthy part of our platform ecosystem. I'm not speaking in any official capacity here, but I would think that giving away the Visual Studio 2010 window management components for free would be detrimental to that ecosystem.

    Of note, however, is that the Visual Studio SDK includes the ability to create “Visual Studio Shell (Isolated)” applications which include the full window management system. This may be an option for you: http://msdn.microsoft.com/en-us/library/bb685691(VS.100).aspx

    @DanielD: MEF would make a great topic for a future article, or maybe even a series. Thanks for the suggestion.

  • @ @DanielD: Replying to your specific question about MEF composition, VS 2010 has a default MEF composition that can be queried from VS packages via SComponentModel service. As with any MEF composition, once you request a particular export from it, MEF will try to satisfy all its imports. Right now the traditional VS services are not exposed as MEF exports, but from your MEF component you can import SVsServiceProvider and access any VS service that way.

  • @Dmitry Goncharenko: thanks for your answer. it helped me.

    @Paul Harrington: would be great to read article about MEF :) But don't stop publishing WPF articles, they are also very interesting.

  • @Paul.

    I see and understand your point about 3rd party vendors.

    And I agree, they have to make a living too :o)

    But it can be a very expensive solution for a small timer like myself.

    They Visual Studio Shell looks very promising.

    I will definitly look into that.

    Thank you very much for that.

    Regards

    Gert

  • I am interested in your serializer generator you used for persistence. Did you use the VS2010 DSL tools to model and generate the serializers, or is this something you did by hand?

    Are there any (other) areas you've used DSL tools/T4 to generate object models for VS2010?

    I use the VS2008 DSL tools a lot and I'm wondering how much they are used internally.

    Cheers

    Ged

  • "...I'm not speaking in any official capacity here, but I would think that giving away the Visual Studio 2010 window management components for free would be detrimental to that ecosystem."

    The only reason this "Ecosystem" of third-party addons even exists is because Microsoft has repeatedly presented users year after year with a UI system of Menubars, tear-off windows and other enticing controls only to turn around and short-change developers with lackluster controls in comparison. The excuse offered up by Ryan earlier with regards to such controls not being "drop-in ready" is ridiculous, as if we are expected to believe that all the the internal code used in their own products' docking features over the past 12+ years have not been made portable for their own convenience in portability and future refinement.

    To other developers out there, think of it in this fashion: If you earned a living off of selling a product you created - only to find out that in doing so that a market had sprung up designed around the premise of making up for your products' short-comings - that would be nothing to be proud of. Unless, of course...it was by your design to begin with.

    Let's be honest. Developers have been clamoring for this and other features for years, and Microsoft has not been willing to share their versions of these controls for very good reasons, but those reasons are not based on support or compatibility. It's because the visual beauty and novelty of these controls - integrated and centered around a core architecture that Microsoft has explicit internal knowledge and control over - gives them an edge in the presentation of their products into the marketplace. Anyone can deliver a product that works as intended, but these controls are very visually appealing. They add style and enjoyment to using those products, and when you combine form, function and style into a single package - it is a win/win situation for the consumer.

    The last thing Microsoft will ever do is give developers access to their tools that given them that edge - because why would Microsoft want to make it easier for developers to make products with the same visual crispness and appeal that could potentially be used to create products that directly compete with Microsoft?

  • Can anyone tell me how I can lock a WPF window?

    I want to make it so a WPF window  can not be moved, resized, dragged , maximized or minimized? If anyone has a sample project or  some source code  that can lock a wpf window you can email me at steve_44@inbox.com

    Thanks

Page 1 of 2 (21 items) 12