Andrew
Andrew

Reputation: 173

WPF: DoubleAnimation "To" property binding timings

I'm trying to make a Button that opens/closes some panel using animation on the Height property of that panel.

Approach is to bind "To" value of the animation to viewmodel property, which is changed in the button's command.

I suppose that it should work like this:

  1. Button is clicked
  2. The viewmodel checks its "IsOpened" property
  3. If it is True, then vm sets it to False and TargetHeight to 0; If it is False, then vm sets it to True and TargetHeight to 128
  4. Animation fires and changes Height to desired value

But in reality it works this way:

  1. Button is clicked
  2. Animation fires and changes Height to previously set value
  3. The viewmodel checks its "IsOpened" property and changes its value

Because of that, viewmodel is setting target height not for current click on the button, but for next time in future.

What should I change to make it right? Already tried setting BeginTime property to delay animation, it didn't help, unfortunately.

Code is as follows:

XAML

<Button Command="{Binding SwitchBottomPanel}">
        <Button.Triggers>
            <EventTrigger RoutedEvent="Button.Click">
                <BeginStoryboard>
                    <Storyboard>
                        <DoubleAnimation Storyboard.TargetName="BottomPanel"
                                         Storyboard.TargetProperty="Height"
                                         To="{Binding TargetHeight}"
                                         Duration="0:0:0.3"
                                         BeginTime="0:0:0.3"/>
                    </Storyboard>
                </BeginStoryboard>
            </EventTrigger>
        </Button.Triggers>
    </Button>

Viewmodel class:

    class MainViewModel : ViewModelBase
{
    private double _targetHeight;
    private bool _isBottomOpened;
    private double _bottomHeightBig = 128;

    public double TargetHeight
    {
        get { return _targetHeight; }
        set
        {
            _targetHeight = value;
            RaisePropertyChanged(nameof(TargetHeight));
        }
    }

    public bool IsBottomOpened
    {
        get { return _isBottomOpened; }
        set
        {
            _isBottomOpened = value;

            if (value == true) TargetHeight = _bottomHeightBig;
            else TargetHeight = 0;

            RaisePropertyChanged(nameof(IsBottomOpened));
        }
    }

    public ICommand SwitchBottomPanel { get; set; }

    public MainViewModel()
    {
        SwitchBottomPanel = new DelegateCommand(() => IsBottomOpened = !IsBottomOpened);
        RaisePropertyChanged(nameof(SwitchBottomPanel));

        IsBottomOpened = false;
    }
}

Upvotes: 2

Views: 813

Answers (1)

mm8
mm8

Reputation: 169200

You could handle the PropertyChanged event for the view model and start the animation programmatically in the view:

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
        DataContext = new MainViewModel();
        Loaded += OnLoaded;
        Unloaded += OnUnloaded;
    }

    private MainViewModel ViewModel => DataContext as MainViewModel;

    private void OnLoaded(object sender, RoutedEventArgs e) =>
        ViewModel.PropertyChanged += ViewModel_PropertyChanged;

    private void OnUnloaded(object sender, RoutedEventArgs e) =>
        ViewModel.PropertyChanged += ViewModel_PropertyChanged;

    private void ViewModel_PropertyChanged(object sender, PropertyChangedEventArgs e) =>
        BottomPanel.BeginAnimation(HeightProperty, new DoubleAnimation()
        {
            To = ViewModel.TargetHeight,
            Duration = TimeSpan.FromSeconds(0.3)
        });
}

Just because you can implement things like animations in pure XAML, it doesn't mean that you always should. This is an example where it makes perfect sense to implement your view-logic using an expressive programming language like C# instead of using a markup language like XAML.

You might also want to consider specifying the actual height in the view, and bind to the IsBottomOpened property:

<Grid x:Name="BottomPanel" Background="Yellow" Height="0" Width="200">
    <Grid.Style>
        <Style TargetType="Grid">
            <Style.Triggers>
                <DataTrigger Binding="{Binding IsBottomOpened}">
                    <DataTrigger.EnterActions>
                        <BeginStoryboard>
                            <Storyboard>
                                <DoubleAnimation Storyboard.TargetProperty="Height"
                                         To="128"
                                         Duration="0:0:0.3" />
                            </Storyboard>
                        </BeginStoryboard>
                    </DataTrigger.EnterActions>
                </DataTrigger>
            </Style.Triggers>
        </Style>
    </Grid.Style>
</Grid>

If you do this, you could also replace the Button element with a ToggleButton and bind to its IsChecked property rather than using an IsBottomOpened source property. Whether you actually need these properties in the view model depends on your requirements.

Upvotes: 1

Related Questions