marko devcic

  • github:
    deva666
  • email:
    madevcic {at} gmail.com

Animating WPF ContentControl Part 3

Posted on 16 August 2014

For the final part of this series ( part 1, part 2 ), we're going to build one more animated content control. This one can be used for displaying various information messages to the user. It will be designed so whenever a message is queued, it slides in to the center of display, waits there for a second (or how long we specify) and then slides out. If more messages are queued up, they will all be presented, one by one in order they were queued.

I used this in one project where I needed to present the user with some information, e.g. if user deleted some file, confirmation string would slide in and if the user did something that else that needed confirmation new message would queue up and be displayed after previous one.

So again we'll be extending the ContentControl class and adding 2 new Storyboards that will be doing all the animation.

Complete code of the new class:

  public class ScrollingContentControl : ContentControl
    {
        bool _processing = false;

        DispatcherTimer _centerTimer;

        Storyboard _scrollIn;
        Storyboard _scrollOut;

        Queue<string> _requests = new Queue<string>(); //replace with ConcurrentQueue<string> if multiple threads will be adding to InfoContent property and lock _processing variable

        public double CenterTime
        {
            get { return (double)GetValue(CenterTimeProperty); }
            set { SetValue(CenterTimeProperty, value); }
        }

        public static readonly DependencyProperty CenterTimeProperty =
            DependencyProperty.Register("CenterTime", typeof(double), typeof(ScrollingContentControl), new PropertyMetadata(2.0d));

        private static void CenterTimeCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            if (((double)e.NewValue) < 0)
                throw new ArgumentException("Value must be 0 or greater");
        }

        public string InfoContent
        {
            get { return (string)GetValue(InfoContentProperty); }
            set { SetValue(InfoContentProperty, value); }
        }

        public static readonly DependencyProperty InfoContentProperty =
            DependencyProperty.Register("InfoContent", typeof(string), typeof(ScrollingContentControl), new PropertyMetadata(string.Empty, InfoContentCallback));

        private static void InfoContentCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            ScrollingContentControl control = (ScrollingContentControl)d;
            if (!string.IsNullOrEmpty(e.NewValue as string))
                control.EnqueueNewInfo(e.NewValue as string);
        }

        public ScrollingContentControl()
        {
            InitStoryboards();
            this.RenderTransform = new TranslateTransform();
            this.Visibility = System.Windows.Visibility.Collapsed;
        }

        private void InitStoryboards()
        {
            Contract.Ensures(_scrollIn != null, "Failed to init scroll in animation");
            Contract.Ensures(_scrollOut != null, "Failed to init scroll out animation");
            _scrollIn = FindResource("scrollIn") as Storyboard;
            _scrollOut = FindResource("scrollOut") as Storyboard;
        }

        private void EnqueueNewInfo(string info)
        {
            _requests.Enqueue(info);
            if (!_processing)
                HandleQueue();
        }

        private void HandleQueue()
        {
            _processing = true;

            if (_centerTimer == null)
                InitTimer();

            string info = _requests.Peek();
            this.Content = info;

            var scrollInClone = _scrollIn.Clone();
            scrollInClone.Completed += (s, e) =>
            {
                _centerTimer.Tick += TimerTick;
                _centerTimer.Start();
            };

            scrollInClone.Begin(this);
            this.Visibility = System.Windows.Visibility.Visible;
        }

        private void TimerTick(object sender, EventArgs args)
        {
            _centerTimer.Tick -= TimerTick;
            _centerTimer.Stop();
            var scrollOutClone = _scrollOut.Clone();
            scrollOutClone.Completed += (snd, ear) =>
            {
                this.Visibility = System.Windows.Visibility.Collapsed;
                if (_requests.Count > 0)
                    _requests.Dequeue();
                if (_requests.Count == 0)
                    _processing = false;
                else
                {
                    CheckTimeInterval(); //Check if CenterTime has changed
                    HandleQueue();
                }
            };
            scrollOutClone.Begin(this);
        }

        private void InitTimer()
        {
            _centerTimer = new DispatcherTimer();
            _centerTimer.Interval = TimeSpan.FromSeconds(CenterTime);
        }

        private void CheckTimeInterval()
        {
            if (_centerTimer == null)
                return;
            if (_centerTimer.Interval != TimeSpan.FromSeconds(CenterTime))
                _centerTimer.Interval = TimeSpan.FromSeconds(CenterTime);
        }
    }

There are 2 dependency properties, CenterTime sets for how long the content will be presented to the user and the InfoContent is the string that will be presented.

Let's add two new storyboards and the Template for the new control.
I used a label for my ContentControl template, you can use whatever suits your needs.

 <Style TargetType="controls:ScrollingContentControl">
            <Setter Property="HorizontalContentAlignment"
                    Value="Stretch" />
            <Setter Property="VerticalContentAlignment"
                    Value="Stretch" />
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="controls:ScrollingContentControl">
                        <Grid x:Name="root"
                              RenderTransformOrigin="0.5,0.5">
                            <Label Content="{TemplateBinding Content}"
                                   FontSize="20"
                                   FontFamily="Tahoma">
                            </Label>
                        
                        </Grid>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>

        <Storyboard x:Key="scrollIn">
            <DoubleAnimationUsingKeyFrames BeginTime="00:00:00"
                                           Storyboard.TargetProperty="(UIElement.Opacity)">
                <SplineDoubleKeyFrame KeyTime="00:00:00"
                                      Value="0" />
                <SplineDoubleKeyFrame KeyTime="00:00:01.5"
                                      Value="1" />
            </DoubleAnimationUsingKeyFrames>
            <DoubleAnimationUsingKeyFrames BeginTime="00:00:00"
                                           Storyboard.TargetProperty="(UIElement.RenderTransform).(TranslateTransform.X)">
                <SplineDoubleKeyFrame KeyTime="00:00:00"
                                      Value="600" />
                <EasingDoubleKeyFrame KeyTime="00:00:01.5"
                                      Value="0">
                    <EasingDoubleKeyFrame.EasingFunction>
                        <CubicEase EasingMode="EaseOut" />
                    </EasingDoubleKeyFrame.EasingFunction>
                </EasingDoubleKeyFrame>
            </DoubleAnimationUsingKeyFrames>
        </Storyboard>

        <Storyboard x:Key="scrollOut">
            <DoubleAnimationUsingKeyFrames BeginTime="00:00:00"
                                           Storyboard.TargetProperty="(UIElement.Opacity)">
                <SplineDoubleKeyFrame KeyTime="00:00:00"
                                      Value="1" />
                <SplineDoubleKeyFrame KeyTime="00:00:01.5"
                                      Value="0" />
            </DoubleAnimationUsingKeyFrames>
            <DoubleAnimationUsingKeyFrames BeginTime="00:00:00"
                                           Storyboard.TargetProperty="(UIElement.RenderTransform).(TranslateTransform.X)">
                <SplineDoubleKeyFrame KeyTime="00:00:00"
                                      Value="0" />
                <SplineDoubleKeyFrame KeyTime="00:00:01.5"
                                      Value="-600" />
            </DoubleAnimationUsingKeyFrames>
        </Storyboard>

Now we can add our new control and a button that will queue up the messages to the view.

<Button Content="Start scrolling"
                Command="{Binding StartScrolling}" 
                Margin="20"/>
<controls:ScrollingContentControl InfoContent="{Binding InfoContent}"
                                          HorizontalAlignment="Center"
                                          CenterTime="1"/>

The view model will have a string property InfoContent that the control binds to.
Start scrolling command will queue up 5 dummy messages that will slide across the screen, one by one.

private ICommand _startScrolling;

public ICommand StartScrolling
        {
            get
            {
                return _startScrolling == null ? _startScrolling = new DelegateCommand(Scroll) : _startScrolling;
            }
        }

private void Scroll()
        {
            string[] infos = new string[] { "Info1", "Info2", "Info3", "Info4", "Info5" };
            foreach (var info in infos)
            {
                InfoContent = info;
            }

        }

private string _infoContent;

public string InfoContent
        {
            get
            {
                return _infoContent;
            }
            set
            {
                _infoContent = value;
                OnPropertyChanged("InfoContent");
            }
        }

Download the complete Visual studio project here.

Browse the full source code on Bitbucket.