Big Nick's Blog …my blog on what I'm learning, working, or playing with in technology

15Oct/097

Overlaying Controls in WPF with Adorners

One of the common things that comes up on multiple projects using WPF is the ability to overlay the screen or a certain portion of it.  Either to create a richer modal-type experience than a message box provides or to block access to a certain portion of the screen while an asynchronous or long running operation is happening.

There are a number of ways to do this but the one I've settled on after tackling it on a few projects is an adorner that automatically overlays and control with any content you want. 

Other options include using the Popup control, which is problematic because popups are not part of the normal visual layout.  They are always on top of all other content and don't move when you resize or move the window, at least not automatically.  Another way you can do it is put everything inside a grid, and add the content you want to overlay with at the end of the Grid's content with no Row or Column specification.  You can set the visibility to collapsed and show or hide based on databinding or triggers, etc.  This works better than the popup for resizing, but is not as reusable.  Even though the adorner is a bit more code, I think it's more reusable and better than the Popup option.

The way I use it is I create a UserControl that will be my overlay, let's call it ProgressMessage.  I've got a Grid I want to overlay called LayoutRoot.  I can then call OverlayAdorner<ProgressMessage>.Overlay(LayoutRoot).  Now my grid will be overlaid with the ProgressMessage user control.  I've also provided an override of the Overlay method so you can actually pass in an instance of the content you want to overlay with.

I use a factory pattern and how IDisposable/using statements work to automatically create/remove the adorner.  You could also store the IDisposable that's returned and call Dispose later to remove the AdornerLayer

<span class="kwrd">using</span> (OverlayAdorner&lt;ProgressMessage&gt;.Overlay(LayoutRoot))
{
  <span class="rem">// do some stuff here while overlaid</span>
}

A couple of quick notes, because of the way WPF layout and hit-testing works, you should not have any height or width set on your overlay content, and the background needs to be non-transparent.  To get a semi-transparent background use the alpha-portion of the aRGB color format on your background.  So instead of Black, use #44000000 and that gives you a semi-transparent gray background.  Additionally, all these methods block mouse input, but the keyboard navigation remains active.  I've started playing with lost focus events and other methods to intercept losing focus and retain that.  Otherwise the user can tab through the controls underneath the overlay and activate them using arrow keys, enter and space bar.  You can either solve this, or once I straighten it out I'll post what I come up with

Here is the rest of the class, OverlayAdorner.cs

  <span class="rem">/// &lt;summary&gt;</span>
   <span class="rem">/// Overlays a control with the specified content</span>
   <span class="rem">/// &lt;/summary&gt;</span>
   <span class="rem">/// &lt;typeparam name=&quot;TOverlay&quot;&gt;The type of content to create the overlay from&lt;/typeparam&gt;</span>
   <span class="kwrd">public</span> <span class="kwrd">class</span> OverlayAdorner&lt;TOverlay&gt; : Adorner, IDisposable <span class="kwrd">where</span> TOverlay : UIElement, <span class="kwrd">new</span>()
   {
      <span class="kwrd">private</span> UIElement _adorningElement;
      <span class="kwrd">private</span> AdornerLayer _layer;

      <span class="rem">/// &lt;summary&gt;</span>
      <span class="rem">/// Overlay the specified element</span>
      <span class="rem">/// &lt;/summary&gt;</span>
      <span class="rem">/// &lt;param name=&quot;elementToAdorn&quot;&gt;The element to overlay&lt;/param&gt;</span>
      <span class="rem">/// &lt;returns&gt;&lt;/returns&gt;</span>
      <span class="kwrd">public</span> <span class="kwrd">static</span> IDisposable Overlay(UIElement elementToAdorn)
      {
         <span class="kwrd">return</span> Overlay(elementToAdorn, <span class="kwrd">new</span> TOverlay());
      }

      <span class="rem">/// &lt;summary&gt;</span>
      <span class="rem">/// Overlays the element with the specified instance of TOverlay</span>
      <span class="rem">/// &lt;/summary&gt;</span>
      <span class="rem">/// &lt;param name=&quot;elementToAdorn&quot;&gt;Element to overlay&lt;/param&gt;</span>
      <span class="rem">/// &lt;param name=&quot;adorningElement&quot;&gt;The content of the overlay&lt;/param&gt;</span>
      <span class="rem">/// &lt;returns&gt;&lt;/returns&gt;</span>
      <span class="kwrd">public</span> <span class="kwrd">static</span> IDisposable Overlay(UIElement elementToAdorn, TOverlay adorningElement)
      {
         var adorner = <span class="kwrd">new</span> OverlayAdorner&lt;TOverlay&gt;(elementToAdorn, adorningElement);
         adorner._layer = AdornerLayer.GetAdornerLayer(elementToAdorn);
         adorner._layer.Add(adorner);

         <span class="kwrd">return</span> adorner <span class="kwrd">as</span> IDisposable;
      }

      <span class="kwrd">private</span> OverlayAdorner(UIElement elementToAdorn, UIElement adorningElement)
         : <span class="kwrd">base</span>(elementToAdorn)
      {
         <span class="kwrd">this</span>._adorningElement = adorningElement;
         <span class="kwrd">if</span> (adorningElement != <span class="kwrd">null</span>)
         {
            AddVisualChild(adorningElement);
         }
         Focusable = <span class="kwrd">true</span>;

      }

      <span class="kwrd">protected</span> <span class="kwrd">override</span> <span class="kwrd">int</span> VisualChildrenCount
      {
         get { <span class="kwrd">return</span> _adorningElement == <span class="kwrd">null</span> ? 0 : 1; }
      }

      <span class="kwrd">protected</span> <span class="kwrd">override</span> Size ArrangeOverride(Size finalSize)
      {
         <span class="kwrd">if</span> (_adorningElement != <span class="kwrd">null</span>)
         {
            Point adorningPoint = <span class="kwrd">new</span> Point(0,0);
            _adorningElement.Arrange(<span class="kwrd">new</span> Rect(adorningPoint, <span class="kwrd">this</span>.AdornedElement.DesiredSize));
         }
         <span class="kwrd">return</span> finalSize;
      }

      <span class="kwrd">protected</span> <span class="kwrd">override</span> Visual GetVisualChild(<span class="kwrd">int</span> index)
      {
         <span class="kwrd">if</span> (index == 0 &amp;&amp; _adorningElement != <span class="kwrd">null</span>)
         {
            <span class="kwrd">return</span> _adorningElement;
         }
         <span class="kwrd">return</span> <span class="kwrd">base</span>.GetVisualChild(index);
      }

      <span class="kwrd">public</span> <span class="kwrd">void</span> Dispose()
      {
         _layer.Remove(<span class="kwrd">this</span>);
      }

   }
Tagged as: , 7 Comments