Search code examples
wpfinotifypropertychangeddatatrigger

EventTrigger sometimes fails to fire


I'm yet again a bit stumped, and was hoping someone could please help. Apologies in advance to the long code.

Issue - I have a DataTrigger that starts off firing as expected, but it eventually fails and I can't work out why. In the example provided, it moves a rectangle around a Canvas. Each click of the button moves it in this sequence; N, NE, E, SE, S, SW, W, NW. Then it starts the sequence again, starting with N. Once the first sequence is completed, it won't move North. It will only ever move NW again (ie the last successful move).

The property that triggers the DataTrigger is updating.

thanks

XAML;

<Window.Resources>
    <Style x:Key="TestRectStyle" TargetType="{x:Type Rectangle}">
        <Style.Triggers>
            <DataTrigger Binding="{Binding UI_DirectionOfMovement}" Value="North">
                <DataTrigger.EnterActions>
                    <BeginStoryboard>
                        <Storyboard>
                            <DoubleAnimation Storyboard.TargetProperty="(Canvas.Top)" By="-50" Duration="0:0:0.8" AutoReverse="False" />
                            <DoubleAnimation Storyboard.TargetProperty="(Canvas.Left)" By="0" Duration="0:0:0.8" AutoReverse="False" />
                        </Storyboard>
                    </BeginStoryboard>
                </DataTrigger.EnterActions>
            </DataTrigger>
            <DataTrigger Binding="{Binding UI_DirectionOfMovement}" Value="NorthEast">
                <DataTrigger.EnterActions>
                    <BeginStoryboard>
                        <Storyboard>
                            <DoubleAnimation Storyboard.TargetProperty="(Canvas.Top)" By="-50" Duration="0:0:0.8" AutoReverse="False" />
                            <DoubleAnimation Storyboard.TargetProperty="(Canvas.Left)" By="50" Duration="0:0:0.8" AutoReverse="False" />
                        </Storyboard>
                    </BeginStoryboard>
                </DataTrigger.EnterActions>
            </DataTrigger>
            <DataTrigger Binding="{Binding UI_DirectionOfMovement}" Value="East">
                <DataTrigger.EnterActions>
                    <BeginStoryboard>
                        <Storyboard>
                            <DoubleAnimation Storyboard.TargetProperty="(Canvas.Top)" By="0" Duration="0:0:0.8" AutoReverse="False" />
                            <DoubleAnimation Storyboard.TargetProperty="(Canvas.Left)" By="50" Duration="0:0:0.8" AutoReverse="False" />
                        </Storyboard>
                    </BeginStoryboard>
                </DataTrigger.EnterActions>
            </DataTrigger>
            <DataTrigger Binding="{Binding UI_DirectionOfMovement}" Value="SouthEast">
                <DataTrigger.EnterActions>
                    <BeginStoryboard>
                        <Storyboard>
                            <DoubleAnimation Storyboard.TargetProperty="(Canvas.Top)" By="50" Duration="0:0:0.8" AutoReverse="False" />
                            <DoubleAnimation Storyboard.TargetProperty="(Canvas.Left)" By="50" Duration="0:0:0.8" AutoReverse="False" />
                        </Storyboard>
                    </BeginStoryboard>
                </DataTrigger.EnterActions>
            </DataTrigger>
            <DataTrigger Binding="{Binding UI_DirectionOfMovement}" Value="South">
                <DataTrigger.EnterActions>
                    <BeginStoryboard>
                        <Storyboard>
                            <DoubleAnimation Storyboard.TargetProperty="(Canvas.Top)" By="50" Duration="0:0:0.8" AutoReverse="False" />
                            <DoubleAnimation Storyboard.TargetProperty="(Canvas.Left)" By="0" Duration="0:0:0.8" AutoReverse="False" />
                        </Storyboard>
                    </BeginStoryboard>
                </DataTrigger.EnterActions>
            </DataTrigger>
            <DataTrigger Binding="{Binding UI_DirectionOfMovement}" Value="SouthWest">
                <DataTrigger.EnterActions>
                    <BeginStoryboard>
                        <Storyboard>
                            <DoubleAnimation Storyboard.TargetProperty="(Canvas.Top)" By="50" Duration="0:0:0.8" AutoReverse="False" />
                            <DoubleAnimation Storyboard.TargetProperty="(Canvas.Left)" By="-50" Duration="0:0:0.8" AutoReverse="False" />
                        </Storyboard>
                    </BeginStoryboard>
                </DataTrigger.EnterActions>
            </DataTrigger>
            <DataTrigger Binding="{Binding UI_DirectionOfMovement}" Value="West">
                <DataTrigger.EnterActions>
                    <BeginStoryboard>
                        <Storyboard>
                            <DoubleAnimation Storyboard.TargetProperty="(Canvas.Top)" By="0" Duration="0:0:0.8" AutoReverse="False" />
                            <DoubleAnimation Storyboard.TargetProperty="(Canvas.Left)" By="-50" Duration="0:0:0.8" AutoReverse="False" />
                        </Storyboard>
                    </BeginStoryboard>
                </DataTrigger.EnterActions>
            </DataTrigger>
            <DataTrigger Binding="{Binding UI_DirectionOfMovement}" Value="NorthWest">
                <DataTrigger.EnterActions>
                    <BeginStoryboard>
                        <Storyboard>
                            <DoubleAnimation Storyboard.TargetProperty="(Canvas.Top)" By="-50" Duration="0:0:0.8" AutoReverse="False" />
                            <DoubleAnimation Storyboard.TargetProperty="(Canvas.Left)" By="-50" Duration="0:0:0.8" AutoReverse="False" />
                        </Storyboard>
                    </BeginStoryboard>
                </DataTrigger.EnterActions>
            </DataTrigger>
        </Style.Triggers>
    </Style>

    <DataTemplate x:Key="TestDataTemplate01" DataType="BO:MyPerson">
        <Canvas Width="1000" Height="1000" Background="Transparent">
            <Rectangle Width="50" Height="50" Fill="Red" Style="{StaticResource TestRectStyle}" Canvas.Top="300" Canvas.Left="300" />
        </Canvas>
    </DataTemplate>
</Window.Resources>

<Canvas Width="1000" Height="1000">
    <ItemsControl Name="ic_People" ItemTemplate="{StaticResource TestDataTemplate01}">
        <ItemsControl.ItemsPanel>
            <ItemsPanelTemplate>
                <Canvas Width="1000" Height="1000" />
            </ItemsPanelTemplate>
        </ItemsControl.ItemsPanel>
    </ItemsControl>

    <Button Canvas.Right="0" Click="Button_Click_1" Width="120">Next Move</Button>
</Canvas>

Codebehind;

public partial class Window1 : Window
{
    private ObservableCollection<MyPerson> _personList = new ObservableCollection<MyPerson>();

    public Window1()
    {
        InitializeComponent();

        MyPerson person1 = new MyPerson();
        _personList.Add(person1);
        ic_People.ItemsSource = _personList;
    }

    private void Button_Click_1(object sender, RoutedEventArgs e)
    {
        DoNextMove();
    }

    private int debugDirection = 0;
    private void DoNextMove()
    {
        if (debugDirection > 15)
            debugDirection = 0;

        _personList[0].MoveOneTile(debugDirection);
        debugDirection += 2; // increase by 2 to as I've not implemented the odd numbers yet
    }
}

MyPerson code;

public class MyPerson : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    protected void OnPropertyChanged(string name)
    {
        PropertyChangedEventHandler handler = PropertyChanged;
        if (handler != null)
        {
            handler(this, new PropertyChangedEventArgs(name));
        }
    }

    private string _dirMov = "";
    public string UI_DirectionOfMovement
    {
        get { return _dirMov; }
        set
        {
            _dirMov = value;
            OnPropertyChanged("UI_DirectionOfMovement");
        }
    }

    public void MoveOneTile(int directionToMove)
    {
        this.UI_DirectionOfMovement = "clear"; // clearing it first forces an update

        switch (directionToMove)
        {
            case 0: { this.UI_DirectionOfMovement = "North"; break; }
            case 2: { this.UI_DirectionOfMovement = "NorthEast"; break; }
            case 4: { this.UI_DirectionOfMovement = "East"; break; }
            case 6: { this.UI_DirectionOfMovement = "SouthEast"; break; }
            case 8: { this.UI_DirectionOfMovement = "South"; break; }
            case 10: { this.UI_DirectionOfMovement = "SouthWest"; break; }
            case 12: { this.UI_DirectionOfMovement = "West"; break; }
            case 14: { this.UI_DirectionOfMovement = "NorthWest"; break; }
            default: { throw new Exception(); }
        }
    }

    public MyPerson()
    {
    }
}

Solution

  • The problem here ultimately comes down to the fact that animations don't actually change the values of the Canvas.Left and Canvas.Top properties. They only appear to do so, as values obtained from animation override values obtained via data binding.

    After each animation finishes, the animation 'holds' the value of the Canvas.Left or Canvas.Top dependency properties at its final value. This 'held' value is what is returned if you get the value of the dependency property, and it overrides any value set via data binding. As you start a second animation, the dependency property's value is obtained by working from the previous animation's held value. As more and more animations happen, WPF has to determine the location of the rectangle by going back through a chain of more and more animations.

    I can't say why only the last (NW) animation runs after you've been run all of them. It quite probably has something to do with dependency property value precedence. This page doesn't say what happens if there are multiple animations on a dependency property, but in this situation I would assume that the last animation to start on that property takes precedence. I suspect that the DataTriggers are firing, but the WPF dependency property system for some reason is ignoring values coming from the 'overridden' animations.

    I would recommend avoiding having a chain of animations like this. Instead, change your Person objects to keep track of where they are on the canvas, for example, by adding Left and Top properties. You can then bind Canvas.Left and Canvas.Top to these properties. Your DoNextMove method should also set the values of these properties to where the animation moves them to. Do this after changing the value of theUI_DirectionOfMovement property. Finally, stop your animations from 'holding' their final values by setting FillBehavior="Stop" on each DoubleAnimation.

    As animated values take precedence over locally-set values, it's not a problem to set the property values for Left and Top at the start of the animation. While the animation is running, the animated values for Canvas.Left and Canvas.Top take precedence over any value set via data binding. As the animation finishes, the animation releases its hold over the Canvas.Left and Canvas.Top dependency properties, and the location of the rectangle reverts to being obtained via data binding. With any luck, this location will be the same as at the end of the animation.