Dynamic View Animations using MVVM
Asked Answered
M

2

6

I've been trying to figure out how to effectively trigger animations in a View when a property in the ViewModel updates, where the animation depends on the value of said property.

I've recreated my problem in a simple application with a single View and ViewModel. The goal here is to transition the color change of a rectangle by using a ColorAnimation. For reference, I've been using the MVVM Foundation package by Josh Smith.

The example project can be downloaded here.

To summarize, I want to animate the color transition in the View whenever the Color property changes.

MainWindow.xaml

<Window x:Class="MVVM.ColorAnimation.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:ColorAnimation="clr-namespace:MVVM.ColorAnimation" Title="MainWindow" Height="350" Width="525">
    <Window.DataContext>
        <ColorAnimation:MainWindowViewModel />
    </Window.DataContext>
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="*" />
            <RowDefinition Height="40" />
        </Grid.RowDefinitions>

        <Rectangle Margin="10">
            <Rectangle.Fill>
                <SolidColorBrush Color="{Binding Color}"/>
            </Rectangle.Fill>
        </Rectangle>

        <StackPanel Orientation="Horizontal" Grid.Row="1">
            <Button Command="{Binding BlueCommand}" Width="100">Blue</Button>
            <Button Command="{Binding GreenCommand}" Width="100">Green</Button>
        </StackPanel>
    </Grid>
</Window>

MainWindowViewModel.cs

namespace MVVM.ColorAnimation
{
    using System.Windows.Input;
    using System.Windows.Media;

    using MVVM;

    public class MainWindowViewModel : ObservableObject
    {
        private ICommand blueCommand;
        private ICommand greenCommand;

        public ICommand BlueCommand
        {
            get
            {
                return this.blueCommand ?? (this.blueCommand = new RelayCommand(this.TurnBlue));
            }
        }

        private void TurnBlue()
        {
            this.Color = Colors.Blue;
        }

        public ICommand GreenCommand
        {
            get
            {
                return this.greenCommand ?? (this.greenCommand = new RelayCommand(this.TurnGreen));
            }
        }

        private void TurnGreen()
        {
            this.Color = Colors.Green;
        }

        private Color color = Colors.Red;

        public Color Color
        {
            get
            {
                return this.color;
            }

            set
            {
                this.color = value;
                RaisePropertyChanged("Color");
            }
        }     
    }
}

Is there anyway from the View to trigger a ColorAnimation instead of an instant transition between the values? The way I'm currently doing this is another application is quite messy, in that I set the ViewModel through a ViewModel property, and then using a PropertyObserver to monitor value changes, then create the Animation and trigger it from the codebehind:

this.colorObserver = new PropertyObserver<Player>(value)
    .RegisterHandler(n => n.Color, this.CreateColorAnimation);

In a situation where I'm dealing with many colors and many potential animations, this becomes quite a mess, and messes up the fact that I'm manually passing in the ViewModel to the View than simply binding the two through a ResourceDictionary. I suppose I could do this in the DataContextChanged event as well, but is there a better way?

Meteoritics answered 8/5, 2011 at 6:57 Comment(1)
The link to the project is no longer valid.Merchantable
W
5

If just for a few animations I would recommend using Visual States. Then you can use GoToAction behavior on the view to trigger different animations. If you are dealing with a lot of similar animations, creating your own behavior would be a better solution.

Update I have created a very simple behaivor to give a Rectangle a little color animation. Here is the code.

   public class ColorAnimationBehavior : TriggerAction<FrameworkElement>
    {
        #region Fill color
        [Description("The background color of the rectangle")]
        public Color FillColor
        {
            get { return (Color)GetValue(FillColorProperty); }
            set { SetValue(FillColorProperty, value); }
        }

        public static readonly DependencyProperty FillColorProperty =
            DependencyProperty.Register("FillColor", typeof(Color), typeof(ColorAnimationBehavior), null);
        #endregion

        protected override void Invoke(object parameter)
        {
            var rect = (Rectangle)AssociatedObject;

            var sb = new Storyboard();
            sb.Children.Add(CreateVisibilityAnimation(rect, new Duration(new TimeSpan(0, 0, 1)), FillColor));

            sb.Begin();
        }

        private static ColorAnimationUsingKeyFrames CreateVisibilityAnimation(DependencyObject element, Duration duration, Color color)
        {
            var animation = new ColorAnimationUsingKeyFrames();

            animation.KeyFrames.Add(new SplineColorKeyFrame { KeyTime = new TimeSpan(duration.TimeSpan.Ticks), Value = color });

            Storyboard.SetTargetProperty(animation, new PropertyPath("(Shape.Fill).(SolidColorBrush.Color)"));
            Storyboard.SetTarget(animation, element);

            return animation;
        }

    }

In xaml, you simply attach this behavior like this,

    <Rectangle x:Name="rectangle" Fill="Black" Margin="203,103,217,227" Stroke="Black">
        <i:Interaction.Triggers>
            <i:EventTrigger EventName="MouseLeftButtonDown">
                <local:ColorAnimationBehavior FillColor="Red"/>
            </i:EventTrigger>
        </i:Interaction.Triggers>
    </Rectangle>

When you click the Rectangle, it should go from Black color to Red.

Wordage answered 8/5, 2011 at 13:37 Comment(3)
Do you have a simple example, preferably in terms of the example above? I've been trying to look up things in the direction you've pointed but haven't figured it out yet. As-is, I'd assume the Color object could be any color by a seperate viewmodel.Meteoritics
I'm not entirely sure this is what I meant, and can't find a way to apply it to my specific case. In this case, the color animation isn't paying attention to the viewmodel at all, and depends on an event. Normally, the event would be that the Color property changed in the viewmodel.Meteoritics
can i take a look at this small app you created?Wordage
B
4

I used the code that Xin posted, and made a few very minor tweeks (code is below). The only 3 material differences:

I created the behavior to work on any UIElement, not just a rectangle

I used a PropertyChangedTrigger instead of an EventTrigger. That let's me Monitor the color property on the ViewModel instead of listening for click events.

I bound the FillColor to the Color property of the ViewModel.

To use this, you will need to download the Blend 4 SDK (it's free, and you only need it if you don't already have Expression Blend), and add references to System.Windows.Interactivity, and Microsoft.Expression.Interactions

Here's the code for the behavior class:


// complete code for the animation behavior
using System.Windows;
using System.Windows.Interactivity;
using System.Windows.Media;
using System.Windows.Media.Animation;

namespace ColorAnimationBehavior
{
    public class ColorAnimationBehavior: TriggerAction<UIElement>
    {
        public Color FillColor
        {
            get { return (Color)GetValue(FillColorProperty); }
            set { SetValue(FillColorProperty, value); }
        }

        public static readonly DependencyProperty FillColorProperty =
            DependencyProperty.Register("FillColor", typeof(Color), typeof(ColorAnimationBehavior), null);

        public Duration Duration
        {
            get { return (Duration)GetValue(DurationProperty); }
            set { SetValue(DurationProperty, value); }
        }

        // Using a DependencyProperty as the backing store for Duration.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty DurationProperty =
            DependencyProperty.Register("Duration", typeof(Duration), typeof(ColorAnimationBehavior), null);

        protected override void Invoke(object parameter)
        {
            var storyboard = new Storyboard();
            storyboard.Children.Add(CreateColorAnimation(this.AssociatedObject, this.Duration, this.FillColor));
            storyboard.Begin();
        }

        private static ColorAnimationUsingKeyFrames CreateColorAnimation(UIElement element, Duration duration, Color color)
        {
            var animation = new ColorAnimationUsingKeyFrames();
            animation.KeyFrames.Add(new SplineColorKeyFrame() { KeyTime = duration.TimeSpan, Value = color });
            Storyboard.SetTargetProperty(animation, new PropertyPath("(Shape.Fill).(SolidColorBrush.Color)"));
            Storyboard.SetTarget(animation, element);
            return animation;
        }
    }
}


Now here's the XAML that hooks it up to your rectangle:


<UserControl x:Class="MVVM.ColorAnimation.Silverlight.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:ColorAnimation="clr-namespace:MVVM.ColorAnimation"
    xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
    xmlns:ei="http://schemas.microsoft.com/expression/2010/interactions"
    xmlns:ca="clr-namespace:ColorAnimationBehavior;assembly=ColorAnimationBehavior"
    mc:Ignorable="d"
    d:DesignHeight="300" d:DesignWidth="400">

    <UserControl.DataContext>
        <ColorAnimation:MainWindowViewModel />
    </UserControl.DataContext>
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="*" />
            <RowDefinition Height="40" />
        </Grid.RowDefinitions>

        <Rectangle x:Name="rectangle" Margin="10" Stroke="Black" Fill="Red">
            <i:Interaction.Triggers>
                <ei:PropertyChangedTrigger Binding="{Binding Color}">
                    <ca:ColorAnimationBehavior FillColor="{Binding Color}" Duration="0:0:0.5" />
                </ei:PropertyChangedTrigger>
            </i:Interaction.Triggers>
        </Rectangle>
        <StackPanel Orientation="Horizontal" Grid.Row="1">
            <Button Command="{Binding BlueCommand}" Width="100" Content="Blue"/>
            <Button Command="{Binding GreenCommand}" Width="100" Content="Green"/>
        </StackPanel>
    </Grid>
</UserControl>


It was really Xin's idea -- I just cleaned it up a bit.

Barrick answered 10/6, 2011 at 17:54 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.