WPF MVVM Property Change Animation

2020-02-08 05:17发布

I am looking for a clean way to start an animation that will have dynamic values. Basically I want to do an animation where an element changes width based on the data of another element. Say I have a TextBlock that's Text Property is Binding. When this property changes I want a visual element say a Rectangle for our sake to do a DoubleAnimation changing the width from previous value to the new value.

I am trying to stay away from putting code in my view if possible. I've looked into DataTriggers but they seem to require that you know what the value would be such as an Enum. In my case it is just the value changing that needs to trigger a storyboard and the animation would need to start at the current(previous) value and move nicely to the new value.

Any ideas. Maybe I just missed a property.

4条回答
虎瘦雄心在
2楼-- · 2020-02-08 05:23

Actually you want to bind the DoubleAnimation.ToProperty to the ViewModel property and animate actual control. The problem is animation should be continued when ToProperty changed. My solution encapsulate all this logic to a MarkupExtenstion which wraps a Binding.

public class AnimateBindingExtension : MarkupExtension {
    static DependencyPropertyDescriptor dpd =
        DependencyPropertyDescriptor.FromProperty(DoubleAnimation.ToProperty, 
            typeof(DoubleAnimation));

    public AnimateBindingExtension(PropertyPath path) {
        Path = path;
    }

    public bool ValidatesOnExceptions { get; set; }
    public IValueConverter Converter { get; set; }
    public object ConverterParamter { get; set; }
    public string ElementName { get; set; }
    public RelativeSource RelativeSource { get; set; }
    public object Source { get; set; }
    public bool ValidatesOnDataErrors { get; set; }
    [ConstructorArgument("path")]
    public PropertyPath Path { get; set; }
    public object TargetNullValue { get; set; }

    public override object ProvideValue(IServiceProvider serviceProvider) {
        var valueProvider = serviceProvider.GetService(typeof(IProvideValueTarget)) as IProvideValueTarget;

        if (valueProvider == null) {
            throw new Exception("could not get IProviderValueTarget service.");
        }

        var bindingTarget = valueProvider.TargetObject as FrameworkElement;
        var bindingProperty = valueProvider.TargetProperty as DependencyProperty;

        if (bindingProperty == null || bindingTarget == null) {
            throw new Exception();
        }

        var binding = new Binding {
            Path = Path,
            Converter = Converter,
            ConverterParameter = ConverterParamter,
            ValidatesOnDataErrors = ValidatesOnDataErrors,
            ValidatesOnExceptions = ValidatesOnExceptions,
            TargetNullValue = TargetNullValue
        };

        if (ElementName != null) binding.ElementName = ElementName;
        else if (RelativeSource != null) binding.RelativeSource = RelativeSource;
        else if (Source != null) binding.Source = Source;

        // you can add a Duration property to this class and use it here
        var anim = new DoubleAnimation {
            Duration = new Duration(TimeSpan.FromSeconds(0.1)),
            AccelerationRatio = 0.2,
            DecelerationRatio = 0.8
        };
        // this can be a new subclass of DoubleAnimation that 
        // overrides ToProperty metadata and add a property 
        // change callback
        dpd.AddValueChanged(anim, (s, e) => bindingTarget.BeginAnimation(bindingProperty, anim));

        BindingOperations.SetBinding(anim, DoubleAnimation.ToProperty, binding);
        // this is because we need to catch the DataContext so add animation object 
        // to the visual tree by adding it to target object's resources.
        bindingTarget.Resources[bindingProperty.Name] = anim;
        // animation will set the value
        return DependencyProperty.UnsetValue;
    }
}

You can do the same with other animation classes to animate other types.

查看更多
女痞
3楼-- · 2020-02-08 05:27

Here is the solution I ended up with. To do the Animation based on data in my ViewModel I used a DataTrigger. Below is my Style for the control.

<Style TargetType="Grid" x:Key="DetailRotation" >
    <Style.Triggers>
        <DataTrigger Binding="{Binding Path=AnimationState}" Value="New">
            <DataTrigger.EnterActions>
                <StopStoryboard BeginStoryboardName="EndAnimation" />
                <BeginStoryboard Name="NewAnimation">
                    <Storyboard>
                        <ThicknessAnimation Storyboard.TargetProperty="Margin" From="0,30,0,0" To="0,0,0,0" Duration="0:0:1" />
                        <DoubleAnimation Storyboard.TargetProperty="Opacity" From="0" To="1" Duration="0:0:1" />
                    </Storyboard>
                </BeginStoryboard>
            </DataTrigger.EnterActions>
            <DataTrigger.ExitActions>

            </DataTrigger.ExitActions>

        </DataTrigger>
        <DataTrigger Binding="{Binding Path=AnimationState}" Value="End">
            <DataTrigger.EnterActions>
                <StopStoryboard BeginStoryboardName="NewAnimation" />
                <BeginStoryboard Name="EndAnimation">
                    <Storyboard>
                        <ThicknessAnimation Storyboard.TargetProperty="Margin" From="0,0,0,0" To="0,-20,0,0" Duration="0:0:1"/>
                        <DoubleAnimation Storyboard.TargetProperty="Opacity" From="1" To="0" Duration="0:0:1" />
                    </Storyboard>
                </BeginStoryboard>
            </DataTrigger.EnterActions>
        </DataTrigger>

    </Style.Triggers>
</Style>
查看更多
看我几分像从前
4楼-- · 2020-02-08 05:35

Since properties modified by animation cannot be set outside the animation 'context', I came up with a code solution since I could not do the same in XAML effectively.

private void UserControl_IsVisibleChanged(object sender, 
    DependencyPropertyChangedEventArgs e)
{
    if (this.Visibility == Visibility.Visible)
    {
        DoubleAnimation fadeIn = new DoubleAnimation();
        fadeIn.From = 1d;
        fadeIn.To = 1d;
        fadeIn.Duration = new Duration(new TimeSpan(0, 0, 0));

        DoubleAnimation fade = new DoubleAnimation();
        fade.From = 1d;
        fade.To = 0d;
        fade.BeginTime = TimeSpan.FromSeconds(5);
        fade.Duration = new Duration(new TimeSpan(0, 0, 1));

        NameScope.SetNameScope(this, new NameScope());
        this.RegisterName(this.Name, this);

        Storyboard.SetTargetName(fadeIn, this.Name);
        Storyboard.SetTargetProperty(fadeIn, new PropertyPath
            (UIElement.OpacityProperty));

        Storyboard.SetTargetName(fade, this.Name);
        Storyboard.SetTargetProperty(fade, new PropertyPath
            (UIElement.OpacityProperty));

        Storyboard sb = new Storyboard();
        sb.Children.Add(fadeIn);
        sb.Children.Add(fade);

        sb.Completed += new EventHandler(sb_Completed);
        sb.Begin(this);
    }
}

void sb_Completed(object sender, EventArgs e)
{
    this.Visibility = Visibility.Hidden;
}
查看更多
▲ chillily
5楼-- · 2020-02-08 05:46

You could explore using Attached Properties to hook up the necessary logic to the Storyboard/Animation that you desire.

This won't necessarily stop you from having to write code, but it will keep it separated from the view and allow it to be re-used across multiple views.

查看更多
登录 后发表回答