Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / WPF

C# WPF Animated Adorner Popup Control

3.75/5 (3 votes)
14 Oct 2016CPOL3 min read 28.9K   1.1K  
A popup like control embedded in the adorner layer that can be animated

Introduction

Visual space in an application is a premium. Even in the age of 4K monitors with amazing resolutions, designers and developers will always be pressed to present more information in a smaller area. Over the years, new techniques have arisen to meet these needs, e.g., dialog boxes, popup boxes, etc. Another method to display additional information to the user is with the Adorner class. The Adorner class differs from other controls in that it is displayed in the Adorner Layer, which lies on top of all other UIElements in your application. The difficulty with adorners is that there is no out-of-the-box methods to display controls for the user to interact with. Here's how to get around that little shortcoming. There might be more elegant solutions out there but this one gets the job done.

Background

Sometimes, there are data structures that can be presented with a simple description but, in reality, contain a larger breadth of data that would take up valuable screen space. In the past, this was handled using the Multi-Document-Interface (MDI), giving users access to the data. With the visual support provided in WPF, we can display or hide all that additional information with a single click, all within the application's visual presentation area.

The PopupAdorner

The PopupAdorner class serves as the visual container for any additional information that needs to be displayed. To keep this article short, I have not included any code that checks the adorner's position in the window.

C#
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Media;

namespace UI
{
    public class PopupAdorner : Adorner
    {
        private VisualCollection _visuals;
        private ContentPresenter _presenter;
        
        /// <summary>
        /// Creates a new popup adorner with the specified content.
        /// </summary>
        /// <param name="adornedElement">The UIElement that will be adorned</param>
        /// <param name="content">The content that will be display inside the popup</param>
        /// <param name="offset">The popup position in regards to the adorned element</param>
        public PopupAdorner(UIElement adornedElement, UIElement content, Vector offset)
            : base(adornedElement)
        {
            _visuals = new VisualCollection(this);
            _presenter = new ContentPresenter();
            _visuals.Add(_presenter);
            _presenter.Content = content;
            Margin = new Thickness(offset.X, offset.Y, 0, 0);
        }
        
        protected override Size MeasureOverride(Size constraint)
        {
            _presenter.Measure(constraint);
            return _presenter.DesiredSize;
        }
        protected override Size ArrangeOverride(Size finalSize)
        {
            _presenter.Arrange(new Rect(0, 0, finalSize.Width, finalSize.Height));
            return _presenter.RenderSize;
        }
        protected override Visual GetVisualChild(int index)
        {
            return _visuals[index];
        }
        protected override int VisualChildrenCount
        {
            get
            {
                return _visuals.Count;
            }
        }
        
        /// <summary>
        /// Brings the popup into view.
        /// </summary>
        public void Show()
        {
            AdornerLayer adornerLayer = AdornerLayer.GetAdornerLayer(AdornedElement);
            adornerLayer.Add(this);
        }
        /// <summary>
        /// Removes the popup into view.
        /// </summary>
        public void Hide()
        {
            AdornerLayer adornerLayer = AdornerLayer.GetAdornerLayer(AdornedElement);
            adornerLayer.Remove(this);
        }
    }
}

With the VisualCollection class, we can overcome the Adorner class' out-of-the-box design. By overriding default rendering behavior, we can include any kind of control and its children in an Adorner. The limitation is that we can only have one parent object.

Defining the Adorner Content

There are two ways to build the content, a custom UserControl or through code behind. Since using VS to build custom user controls is trivial, I will demonstrate how to build a simple control programmatically.

C#
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;

namespace UI
{
    public static class DynamicAdornerContent
    {
        public static Border GetSimpleInfoPopup(string[] textInfo)
        {
            Border popupBorder = new Border() 
                { BorderBrush = Brushes.Black, BorderThickness = new Thickness(1D) };
            Grid container = new Grid() 
                { Background = Brushes.White };
            
            Style txtStyle = new Style();
            txtStyle.TargetType = typeof(TextBlock);
            txtStyle.Setters.Add(new Setter() 
                { 
                    Property = FrameworkElement.HorizontalAlignmentProperty, 
                    Value = HorizontalAlignment.Left 
                });
            txtStyle.Setters.Add(new Setter() 
                { 
                    Property = FrameworkElement.VerticalAlignmentProperty, 
                    Value = VerticalAlignment.Center 
                });
            
            for (int i = 0; i < textInfo.Length; ++i)
            {
                container.RowDefintions.Add(new RowDefinition() 
                    { Height = new GridLength(26D) };

                var tbox = new TextBlock();
                tbox.Style = txtStyle;
                tbox.Text = textInfo[i];
                
                Grid.SetRow(tbox, i);
                container.Children.Add(tbox);
            }
            
            popupBorder.Child = container;
            return popupBorder;
        }
    }
}

Using the PopupAdorner

The Adorner can be used to decorate any kind of UIElement. For this, I will build a custom user control that contains a TextBlock and a simple arrow image the users can click on to expand or collapse the PopupAdorner.

XAML

XAML
<UserControl>
    <UserControl.Resources>
        <DrawingImage x:Key="img_Arrow">
            <DrawingImage.Drawing>
                <DrawingGroup>
                    <GeometryDrawing Brush="Gray">
                        <GeometryDrawing.Pen>
                            <Pen Brush="DarkGray" 
                                EndLineCap="Round" 
                                LineJoin="Round" 
                                StartLineCap="Round" 
                                Thickness="1"/>
                        </GeometryDrawing.Pen>
                        <GeometryDrawing.Geometry>
                            <PathGeometry>
                                <PathFigure IsFilled="True" StartPoint="0,5">
                                    <PathFigure.Segments>
                                        <LineSegment Point="5,0"/>
                                        <LineSegment Point="5,10"/>
                                        <LineSegment Point="0,5"/>
                                    </Pathfigure.Segments>
                                </PathFigure>
                            </PathGeometry>
                        </GeometryDrawing.Geometry>
                    </GeometryDrawing>
                </DrawingGroup>
            </DrawingImage.Drawing>
        </DrawingImage>
    </UserControl.Resources>
    <Grid Background="White" Height="28" Width="300" x:Name="grid_Container">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="1*"/>
            <ColumnDefinition Width="20"/>
        </Grid.ColumnDefinitions>
        <TextBlock Grid.Column="0" 
            HorizontalAlignment="Stretch" 
            TextTrimming="CharacterEllipsis" 
            TextWrapping="NoWrap" 
            VerticalAlignment="Center" 
            x:Name="txt_DisplayText"/>
        <Image Grid.Column="1" 
            Height="18" 
            MouseDown="TogglePopup" 
            Source="{StaticResource ResourceKey=img_Arrow}" 
            ToolTip="Expand" 
            Width="16" 
            x:Name="ctl_Expander"/>
    </Grid>
</UserControl>

Code Behind

C#
using System;
using System.Windows;
using System.Windows.Controls;
using System.Widnows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;

namespace UI
{
    public class ControlWithPopup : UserControl
    {
        private Border _infoContainer;
        private PopupAdorner _infoPopup;
        private bool _isExpanded;
        
        public ControlWithPopup(string displayText, string[] additionalInfo)
        {
            InitializeComponent();
            _displayText.Text = displayText;
            _infoContainer = DynamicAdornerContent.GetSimpleInfoPopup(additionalInfo);
        }
        
        private TogglePopup(sender object, MouseButtonEventArgs e)
        {
            if (_isExpanded)
            {
                _infoPopup.Hide();
                _isExpanded = false;
            }
            else
            {
                if (_infoPopup == null)
                {
                    _infoPopup = new PopupAdorner(
                        grid_Container, 
                        _infoContainer, 
                        new Vector(0, 29));
                }
                
                _infoPopup.Show();
                _isExpanded = true;
            }
        }
    }
}
With this, when the user clicks the arrow image, the additional information supplied will pop into view just below the custom user control and then disappears from view when the arrow is clicked again.

Adding the Animations

At this point, the control doesn't do much of anything visual except show and hide the additional information. What we need are some animations to make the transition more visually appealing.

C#
// add these animations, transforms, and method to the ControlWithPopup class
private DoubleAnimation _extendAnimation;
private DoubleAnimation _collapseAnimation;
private DoubleAnimation _extendArrowAnimation;
private DoubleAnimation _collapseArrowAnimation;
private RotateTransform _extendArrowTransform;
private RotateTransform _collapseArrowTransform;

// call this in the constructor to build the animations 
// for rotating the arrow in response to user interactions
private void InitializeAnimations()
{
    _extendArrowAnimation = 
        new DoubleAnimation(0.0, -90.0, new Duration(TimeSpan.FromSeconds(0.25)));
    _collapseArrowAnimation = 
        new DoubleAnimation(-90.0, 0.0, new Duration(TimeSpan.FromSeconds(0.25)));
    _extendArrowTransform = 
        new RotateTransform() { Angle = -90, CenterX = 0.5, CenterY = 0.5 };
    _collapseArrowTransform = 
        new RotateTransform() { Angle = 0, CenterX = 0.5, CenterY = 0.5 };
}

// alter the TogglePopup function to include animation calls
private TogglePopup(sender object, MouseButtonEventArgs e)
{
    ctl_Expander.RenderTransformOrigin = new Point(0.5, 0.5);
    
    if (_isExpanded)
    {
        ctl_Expander.RenderTransform = _collapseArrowTransform;
        ctl_Expander.BeginAnimation(RotateTransform.AngleProperty, _collapseArrowAnimation);
        
        _collapseAnimation = new DoubleAnimation(
            _infoContainer.ActualHeight, 
            0.0, 
            new Duration(TimeSpan.FromSeconds(0.25)), 
            FillBehavior.Stop);
        // this next line will prevent the popup from being removed from the
        // AdornerLayer until the animation completes
        _collapseAnimation.Completed += (s, ev) => { _infoPopup.Hide(); }; 
        
        _infoPopup.BeginAnimation(HeightProperty, _collapseAnimation);
        _isExpanded = false;
    }
    else
    {
         if (_infoPopup == null)
        {
             _infoPopup = 
                 new PopupAdorner(grid_Container, _infoContainer, new Vector(0, 29));
             _infoContainer.Measure
                 (new Size(Double.PositiveInfinity, Double.PositiveInifity));
             
             _extendAnimation = new DoubleAnimation(
                0.0, 
                _infoContainer.DesiredSize.Height, 
                new Duration(TimeSpan.FromSeconds(0.25)), 
                FillBehavior.Stop);
             
            // this line will cause the extend animation to begin after the container
            // information is loaded
             _infoContainer.Loaded += (s, ev) => 
                { 
                    _infoContainer.BeginAnimation(HeightProperty, _extendAnimation; 
                }
        }
        
        ctl_Expander.RenderTransform = extendArrowTransform;
        ctl_Expander.BeginAnimation(RotateTransform.AngleProperty, _extendArrowAnimation);
        
        _infoPopup.Show();
        _isExpanded = true;
    }
}
What is added here are four DoubleAnimation objects and two RotateTransform objects. These animations and transforms are responsible for altering the visual in response to the user. When the user clicks the extend arrow, the arrow is rotated to point down and the adorner popup slides down into view. When clicked again, the arrow rotates back to its original position, the adorner popup slides back up and out of view, and when the closing animation finishes, the adorner removes itself from the AdornerLayer. I have not included any code to check for click-happy users and it will error if the expand arrow is clicked during a transition.

Special Recognition

I just wanted to include the links to all the data sources that helped me in this project:

History

  • 10/13/2016 Initial publication
  • 10/14/2016 Fixed errors; added sample project

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)