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>);
      }

   }
Comments (7) Trackbacks (0)
  1. You could probably do this nicely with a Behavior as well. They have a lot better design time support in Blend as well.

  2. See previous post, who’s had time to look at new Blend 3 SDK features? Besides, you should hear some of the stories about some of the people that work on Blend 3, not to be trusted.

  3. nice post! just in time. i need this function in my little project. thank you!

  4. Thanks for the utility. When I call OverlayAdorner with my element, I do not see it on screen. Any ideas?

  5. Hello,

    Seems that I needed to replace “.DesiredSize” by .”RenderSize” in:

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

    in order to get the overlay correctly resized.

  6. Hi

    Thanks a lot for your code, it made overlays very easy to understand for me.

    About keyboard navigation remaining active in the background:
    experimenting with keyboard focus was a big pain and did not give satisfying results (I may have done it wrong) so my solution, to make sure the user can only interact with the following is to do the following:

    using (OverlayAdorner.Overlay(LayoutRoot))
    {
    // disable the whole background
    layoutRoot.IsEnabled = false;

    // do some stuff here while overlaid
    }

    Obviously layoutRoot has to be enabled again when the overlay is disposed of.

    This may be ugly and bruteforce-y, but it does the job. Kinda. Indeed if layoutRoot has many items, then it will take some time to render all of them disabled, and this may mess up any animation done to make the overlay appear.

  7. buy at my estore with confident


Leave a comment

(required)

Trackbacks are disabled.