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

using (OverlayAdorner<ProgressMessage>.Overlay(LayoutRoot)) 
{ 
   // do some stuff here while overlaid 
}

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

    /// <summary> 
    /// Overlays a control with the specified content 
    /// </summary> 
    /// <typeparam name="TOverlay">The type of content to create the overlay from</typeparam> 
    public class OverlayAdorner<TOverlay> : Adorner, IDisposable where TOverlay : UIElement, new()
    {
        private UIElement _adorningElement; private AdornerLayer _layer; /// <summary> /// Overlay the specified element /// </summary> /// <param name="elementToAdorn">The element to overlay</param> /// <returns></returns> public static IDisposable Overlay(UIElement elementToAdorn) { return Overlay(elementToAdorn, new TOverlay()); } 
        /// <summary> 
        /// Overlays the element with the specified instance of TOverlay 
        /// </summary> 
        /// <param name="elementToAdorn">Element to overlay</param> 
        /// <param name="adorningElement">The content of the overlay</param> 
        /// <returns></returns> 
        public static IDisposable Overlay(UIElement elementToAdorn, TOverlay adorningElement)
        {
            var adorner = new OverlayAdorner<TOverlay>(elementToAdorn, adorningElement);
            adorner._layer = AdornerLayer.GetAdornerLayer(elementToAdorn);
            adorner._layer.Add(adorner);
            return adorner as IDisposable;
        }

        private OverlayAdorner(UIElement elementToAdorn, UIElement adorningElement)
            : base(elementToAdorn)
        {
            this._adorningElement = adorningElement;
            if (adorningElement != null)
            {
                AddVisualChild(adorningElement);
            }
            Focusable = true;
        }

        protected override int VisualChildrenCount
        {
            get { return _adorningElement == null ? 0 : 1; }
        }

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

        protected override Visual GetVisualChild(int index)
        {
            if (index == 0 && _adorningElement != null)
            {
                return _adorningElement;
            }
            return base.GetVisualChild(index);
        }
        public void Dispose()
        {
            _layer.Remove(this);
        }
    }

7 thoughts on “Overlaying Controls in WPF with Adorners

  1. Nick Post author

    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.

  2. Takeo

    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.

  3. gRanslant

    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.

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>