Search code examples
wpfstoryboarddatatriggermultidatatrigger

DataTrigger.EnterActions.BeginStoryboard not Beginning


The purpose of this XAML is to animate a ListBox.

  1. The Selected ListBoxItem is Zoomed X2
  2. The NotSelected ListBoxItem is Zoomed X.5
  3. When nothing is selected, they are Zoomed X1

However, these Storyboards are not acting as expected.

(just copy this whole thing into Kaxaml, or your fav. XAML editor)

Is there something obvious here?

<Page 
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
  xmlns:sys="clr-namespace:System;assembly=mscorlib" 
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> 
  <Page.Resources>   

    <x:Array Type="{x:Type sys:String}" x:Key="MyData"> 
      <sys:String>One</sys:String> 
      <sys:String>Two</sys:String> 
      <sys:String>Three</sys:String> 
      <sys:String>Four</sys:String> 
      <sys:String>Five</sys:String> 
      <sys:String>Six</sys:String> 
      <sys:String>Seven</sys:String> 
      <sys:String>Eight</sys:String> 
    </x:Array> 

  </Page.Resources> 

  <ListBox ItemsSource="{Binding Source={StaticResource MyData}}" Name="ListBoxA"> 
    <ListBox.ItemTemplate> 
      <DataTemplate> 
        <DataTemplate.Triggers> 

            <!-- selected (Grow) -->  
            <MultiDataTrigger> 
                <MultiDataTrigger.Conditions> 
                    <Condition Value="True" Binding="{Binding Path=IsSelected, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type ListBoxItem}}}" /> 
                    <Condition Value="1"  Binding="{Binding Path=SelectedItems.Count,  RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type ListBox}}}" /> 
                </MultiDataTrigger.Conditions> 
                <MultiDataTrigger.EnterActions> 
                    <BeginStoryboard Name="BeginStoryboardSelected">
                      <Storyboard> 
                        <ParallelTimeline> 
                            <DoubleAnimation To="2" DecelerationRatio="0.5" Duration="00:00:00.500" Storyboard.TargetName="MyTransform" Storyboard.TargetProperty="ScaleX"  /> 
                            <DoubleAnimation To="2" DecelerationRatio="0.5" Duration="00:00:00.500" Storyboard.TargetName="MyTransform" Storyboard.TargetProperty="ScaleY"  /> 
                        </ParallelTimeline> 
                      </Storyboard>
                    </BeginStoryboard>
                </MultiDataTrigger.EnterActions> 
            </MultiDataTrigger> 

            <!-- none selected --> 
            <MultiDataTrigger> 
                <MultiDataTrigger.Conditions> 
                    <Condition Value="False" Binding="{Binding Path=IsSelected, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type ListBoxItem}}}" /> 
                    <Condition Value="0"  Binding="{Binding Path=SelectedItems.Count,  RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type ListBox}}}" /> 
                </MultiDataTrigger.Conditions> 
                <MultiDataTrigger.EnterActions> 
                    <BeginStoryboard Name="BeginStoryboardNoneSelected">
                      <Storyboard>
                        <ParallelTimeline> 
                            <DoubleAnimation To="1" DecelerationRatio="0.5" Duration="00:00:00.500" Storyboard.TargetName="MyTransform" Storyboard.TargetProperty="ScaleX"  /> 
                            <DoubleAnimation To="1" DecelerationRatio="0.5" Duration="00:00:00.500" Storyboard.TargetName="MyTransform" Storyboard.TargetProperty="ScaleY"  /> 
                        </ParallelTimeline> 
                      </Storyboard>
                    </BeginStoryboard>
                </MultiDataTrigger.EnterActions> 
            </MultiDataTrigger>         

            <!-- shrink --> 
            <MultiDataTrigger> 
                <MultiDataTrigger.Conditions> 
                    <Condition Value="False" Binding="{Binding Path=IsSelected, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type ListBoxItem}}}" /> 
                    <Condition Value="1"  Binding="{Binding Path=SelectedItems.Count, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type ListBox}}}" /> 
                </MultiDataTrigger.Conditions> 
                <MultiDataTrigger.EnterActions>  
                    <BeginStoryboard Name="BeginStoryboardNotSelected">
                      <Storyboard>
                        <ParallelTimeline> 
                            <DoubleAnimation To=".5" DecelerationRatio="0.5" Duration="00:00:00.500" Storyboard.TargetName="MyTransform" Storyboard.TargetProperty="ScaleX"  /> 
                            <DoubleAnimation To=".5" DecelerationRatio="0.5" Duration="00:00:00.500" Storyboard.TargetName="MyTransform" Storyboard.TargetProperty="ScaleY"  /> 
                        </ParallelTimeline> 
                      </Storyboard>
                    </BeginStoryboard>
                </MultiDataTrigger.EnterActions> 
            </MultiDataTrigger>         

        </DataTemplate.Triggers> 

        <!-- debug content -->
        <UniformGrid Columns="3">
          <TextBlock Text="{Binding Path=SelectedItems.Count, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type ListBox}}, StringFormat={} SelectedItems.Count is {0}}" Margin="0,0,10,0" Foreground="Gray" />
          <TextBlock Text="{Binding Path=IsSelected, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type ListBoxItem}}, StringFormat={} ListBoxItem.IsSelected is {0}}" Margin="0,0,10,0" Foreground="Gray" />
          <TextBlock Text="{Binding .}"> 
            <TextBlock.LayoutTransform> 
                <ScaleTransform ScaleX="1" ScaleY="1" x:Name="MyTransform"/> 
            </TextBlock.LayoutTransform> 
          </TextBlock> 
        </UniformGrid>

      </DataTemplate> 
    </ListBox.ItemTemplate> 
  </ListBox> 

 </Page>

Solution

  • I'm not sure of the best workaround, but the problem is the triggers will still need to "unapply" their animated values when the trigger is no longer valid. So in your case, the first trigger may be applied, but it is effectively removed by the ExitAction of the last trigger.

    Since you don't specify an ExitAction, it probably just performs a BeginAnimation(..., null) to clear the EnterAction's animation. You can verify this by reordering the triggers, and you will see the last one always takes effect.

    A similar question can be found here. But it seems even with that, it does not work as expected.

    I'd probably go with a custom control that handles the zoom animation for you. Something like:

    public class AnimatedZoomDecorator : Decorator {
    
        public static readonly DependencyProperty ZoomLevelProperty = DependencyProperty.Register("ZoomLevel",
            typeof(double), typeof(AnimatedZoomDecorator), new FrameworkPropertyMetadata(1.0, OnZoomLevelPropertyValueChanged));
    
        public double ZoomLevel {
            get { return (double)this.GetValue(ZoomLevelProperty); }
            set { this.SetValue(ZoomLevelProperty, value); }
        }
    
        private static void OnZoomLevelPropertyValueChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) {
            AnimatedZoomDecorator control = d as AnimatedZoomDecorator;
            if (control != null) {
                ScaleTransform scaleTransform = control.LayoutTransform as ScaleTransform;
                if (scaleTransform == null)
                    control.LayoutTransform = scaleTransform = new ScaleTransform();
    
                DoubleAnimation animation = new DoubleAnimation() {
                    To = control.ZoomLevel,
                    DecelerationRatio = 0.5,
                    Duration = new Duration(TimeSpan .FromMilliseconds(500)),
                };
    
                scaleTransform.BeginAnimation(ScaleTransform.ScaleXProperty, animation);
                scaleTransform.BeginAnimation(ScaleTransform.ScaleYProperty, animation);
            }
        }
    
    }
    

    Which could then be used like this:

    <DataTemplate>
        <local:AnimatedZoomDecorator x:Name="zoom">
            <TextBlock Text="{Binding .}" />
        </local:AnimatedZoomDecorator>
    
        <DataTemplate.Triggers>
    
            <!-- shrink -->
            <MultiDataTrigger>
                <MultiDataTrigger.Conditions>
                    <Condition Value="False"
                            Binding="{Binding Path=IsSelected, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type ListBoxItem}}}" />
                    <Condition Value="1"
                            Binding="{Binding Path=SelectedItems.Count, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type ListBox}}}" />
                </MultiDataTrigger.Conditions>
                <Setter TargetName="zoom" Property="ZoomLevel" Value="0.5" />
            </MultiDataTrigger>
    
            <!-- selected (Grow) -->
            <MultiDataTrigger>
                <MultiDataTrigger.Conditions>
                    <Condition Value="True"
                            Binding="{Binding Path=IsSelected, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type ListBoxItem}}}" />
                    <Condition Value="1"
                            Binding="{Binding Path=SelectedItems.Count,  RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type ListBox}}}" />
                </MultiDataTrigger.Conditions>
                <Setter TargetName="zoom" Property="ZoomLevel" Value="2" />
            </MultiDataTrigger>
    
        </DataTemplate.Triggers>
    </DataTemplate>