EventTrigger sometimes fails to fire - wpf

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()
{
}
}

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.

Related

WPF: pure XAML when lunch Storyboard base of binding condition

So i have this Storyboard:
<Storyboard x:Key="animate">
<DoubleAnimation BeginTime="0:0:0" Storyboard.TargetProperty="Opacity" From="1" To="0" Duration="0:0:2.0"/>
</Storyboard>
My binding value:
public bool IsFound
{
get { return _isFound; }
set
{
_isFound= value;
NotifyPropertyChanged();
}
}
And my Grid that get this Storyboard:
<Grid name="myGrid">
....
<Grid>
if(IsFound)
{
Storyboard storyboard = Resources["animate"] as Storyboard;
if (storyboard != null)
storyboard.Begin(myGrid);
}
So i am looking for something pure XAML instead of checking this IsFound in code behind.
You can use a DataTrigger:
<Style TargetType="Grid" x:Key="MyAnimatedGrid">
<Style.Triggers>
<DataTrigger Binding="{Binding IsFound}" Value="True">
<DataTrigger.EnterActions>
<BeginStoryboard StoryBoard="{StaticResource animate}" />
</DataTrigger.EnterActions>
</DataTrigger>
</Style.Triggers>
</Style>

WPF Storyboard - same trigger, but reverse behavior

I have a Stackpanel One, which has some content, an Image, and a defualt hidden SubStackpanel. When clickin the Image, the image should rotate 90 degrees, and slide down the SubStackpanel.
When clicking the Image again, the Image should rotate back to its original position, and the SubStackpanel should slide up to the default hidden position.
I almost got this working, the problem is that I dont know how to use the same Trigger event, on two different Storyboard animations. So right now only the first animation on the button and the SubStackpanel occurs, everytime the Image is clicked.
I´ve tried the AutoReverse property, but it fires immediately after the animation is done. This should of course only be happening when the user clicks the Image the second time.
I would like to achieve this, only using markup.
This is my currently code:
<Grid>
<StackPanel Grid.Row="0" Orientation="Vertical" Background="Beige" >
<StackPanel.Triggers>
<EventTrigger SourceName="ImageShowPanelTwo" RoutedEvent="Image.MouseDown">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetName="SubPanel" Storyboard.TargetProperty="(StackPanel.Height)" From="0" To="66" Duration="0:0:0.5" />
</Storyboard>
</BeginStoryboard>
</EventTrigger>
<EventTrigger SourceName="ImageShowPanelTwo" RoutedEvent="Image.MouseDown">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetName="SubPanel" Storyboard.TargetProperty="(StackPanel.Height)" From="66" To="0" Duration="0:0:0.5" />
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</StackPanel.Triggers>
<TextBlock>Panel One</TextBlock>
<Image Name="ImageShowPanelTwo" Width="26" Height="26" Source="ImageRotate.png" RenderTransformOrigin=".5,.5" >
<Image.RenderTransform>
<RotateTransform x:Name="AnimatedRotateTransform" Angle="0" />
</Image.RenderTransform>
<Image.Triggers>
<EventTrigger RoutedEvent="Image.MouseDown">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetName="AnimatedRotateTransform"
Storyboard.TargetProperty="Angle"
By="0"
To="90"
Duration="0:0:0.5"
AutoReverse="True"/>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
<EventTrigger RoutedEvent="Image.MouseDown">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetName="AnimatedRotateTransform"
Storyboard.TargetProperty="Angle"
By="90"
To="0"
Duration="0:0:0.5"
AutoReverse="True"/>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Image.Triggers>
</Image>
<StackPanel Name="SubPanel" Background="LightGreen" Height="66">
<TextBlock>SubPanel</TextBlock>
<TextBlock>SubPanel</TextBlock>
</StackPanel>
</StackPanel>
</Grid>
Hope you can help :)
Blend for Visual Studio can be used to do it in a better, simpler way, without dealing with a lot of code.
Here is the list of steps of doing it.
Step 1: Open the project in Blend for Visual Studio and in Visual Studio simultaneously.
Step 2: In Blend, create a new storyboard around all the elements that you wish to animate with it.
Let's call it "Storyboard1".
Step 3: Now, open the storyboard that we just created and click on the small arrow just beside the "+" and click on "Duplicate" in the drop down menu. Then, a new storyboard called "Storyboard1_Copy" will be created.
Step 4: Rename this new storyboard to something that you like, say, "Storyboard1_Rev".
Step 5: You must have guessed it by now. Select the duplicated storyboard and from the drop down menu, click on "Reverse".
Step 6: Now you have two storyboards ready: one for animating some elements as you like and the other for reversing that sequence of animation. Just like you call a storyboard from the C# code, you can call the reversing storyboard from the same, subject to some conditions which check if the elements are already animated or not. For this, I use a bool variable, whose value is changed each time some animation occurs on a set of elements(that is, false if the elements are not already animated and true if they are).
Illustration with an example:
I'll create an application with a simple layout. It has a button, an image and a rectangular area on the screen.
The idea is that, whenever you click on the button once, the image should be maximized and should be minimized back to original size when the button is clicked twice and so on. That is, the reverse animation should happen every other time the button is clicked. Here are some screenshots showing how it happens:
You can see that the current state of the image is shown in the button. It shows "Zoom In" when the image is in its initial small size and "Zoom Out" when it is maximized.
And finally, here is the C# code for handling the clicks of the button:
bool flag = false;
private void Button_Click(object sender, RoutedEventArgs e)
{
if (!flag)
{
Button.Content = "Zoom Out";
Storyboard1.Begin();
flag = true;
}
else
{
Button.Content = "Zoom In";
Storyboard1_Rev.Begin();
flag = false;
}
}
All you have to do is have a status flag that shows the current status of the element(s) that you wish to animate and animate it(them) in forward or reverse timeline as per the value of the flag.
Instead of trying to set the Animation using that event, use a bool property to bind to instead:
<Image Name="ImageShowPanelTwo" Width="26" Height="26" Source="ImageRotate.png"
RenderTransformOrigin=".5,.5" >
<Image.RenderTransform>
<RotateTransform x:Name="AnimatedRotateTransform" Angle="0" />
</Image.RenderTransform>
<Image.Style>
<Style>
<Style.Triggers>
<DataTrigger Binding="{Binding IsRotated}" Value="True">
<DataTrigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation
Storyboard.TargetName="AnimatedRotateTransform"
Storyboard.TargetProperty="Angle"
By="0"
To="90"
Duration="0:0:0.5"
AutoReverse="True"/>
</Storyboard>
</BeginStoryboard>
</DataTrigger.EnterActions>
</DataTrigger>
<DataTrigger Binding="{Binding IsRotated}" Value="False">
<DataTrigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation
Storyboard.TargetName="AnimatedRotateTransform"
Storyboard.TargetProperty="Angle"
By="90"
To="0"
Duration="0:0:0.5"
AutoReverse="True"/>
</Storyboard>
</BeginStoryboard>
</DataTrigger.EnterActions>
</DataTrigger>
</Style.Triggers>
</Style>
</Image.Style>
</Image>
I trust that you can define your own bool property and invert it upon each MouseDown event occurrence to complete this functionality. As it is set to true the first Animation will start and as it is set to false the second will start.
This is how I solved my problem:
Created four storyboards for the Stackpanel and the arrow:
<Storyboard x:Key="RotateIconUp">
<DoubleAnimation Storyboard.TargetName="IconExpand" Storyboard.TargetProperty="(TextBlock.RenderTransform).(RotateTransform.Angle)" From="0" To="90" Duration="0:0:0.4" />
</Storyboard>
<Storyboard x:Key="RotateIconDown">
<DoubleAnimation Storyboard.TargetName="IconExpand" Storyboard.TargetProperty="(TextBlock.RenderTransform).(RotateTransform.Angle)" From="90" To="0" Duration="0:0:0.4" />
</Storyboard>
<Storyboard x:Key="SlideGridDown">
<DoubleAnimation Storyboard.TargetName="GridDetails" Storyboard.TargetProperty="(Grid.Height)" From="0" To="180" Duration="0:0:0.4" />
</Storyboard>
<Storyboard x:Key="SlideGridUp">
<DoubleAnimation Storyboard.TargetName="GridDetails" Storyboard.TargetProperty="(Grid.Height)" From="180" To="0" Duration="0:0:0.4" />
</Storyboard>
Then I trigger the storyboards from codebehind when the arrow is clicked:
private void ExpandDetails() {
try {
if (!pm_IsExanded) {
Storyboard Storyboard = (Storyboard)FindResource("RotateIconUp");
Storyboard.Begin(this);
Storyboard = (Storyboard)FindResource("SlideGridDown");
Storyboard.Begin(this);
pm_IsExanded = true;
BorderMain.BorderBrush = pm_BrushConverter.ConvertFromString("#000000") as Brush;
} else {
Storyboard Storyboard = (Storyboard)FindResource("RotateIconDown");
Storyboard.Begin(this);
Storyboard = (Storyboard)FindResource("SlideGridUp");
Storyboard.Begin(this);
pm_IsExanded = false;
BorderMain.BorderBrush = pm_BrushConverter.ConvertFromString("#d0d0d0") as Brush;
}
} catch (Exception ee) {
GlobalResource.WriteToLog("Error in ExpandDetails", ee);
}
}

Reduce Margin of Button when the button is focused

I have a scenario where I want to reduce the margin of a Button control when it receives focus. This is because because I wan't to increase the border property of that Button control and at the same time wan't the button to stay same size (height and width). So a little bit of effort on web guided me to write a ValueConverter and get the margin reduced. But I am still not able to put up the working code till yet. Here is what I have jumbled up
Xaml
<ControlTemplate.Triggers> <!--ControlTemplate for the Button-->
<Trigger Property="IsFocused" Value="True">
<Setter Property="BorderBrush" TargetName="bdrButton" Value="Wheat"/>
<Setter Property="BorderThickness" TargetName="bdrButton" Value="2"/>
<Setter Property="Margin" TargetName="bdrButton" >
<Setter.Value>
<Binding ElementName="bdrButton" Path="Margin" Converter="{StaticResource N_MarginReducer}">
<Binding.ConverterParameter>
<Thickness>1,1,1,1</Thickness>
</Binding.ConverterParameter>
</Binding>
</Setter.Value>
</Setter>
</Trigger>
</ControlTemplate.Triggers>
Converter:
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) {
Thickness newMargin = new Thickness();
Thickness margin = (Thickness)value;
Thickness reduceBy = (Thickness)parameter;
newMargin.Left = margin.Left - reduceBy.Left;
newMargin.Top = margin.Top - reduceBy.Top;
newMargin.Right = margin.Right - reduceBy.Right;
newMargin.Bottom = margin.Bottom - reduceBy.Bottom;
return newMargin;
}
The above code results in StackOverFlowException for the Margin.Left being called recursively. Anyone has better idea or implementation for the scenario that I am trying to achieve.
You will want to use a ThicknessAnimation for the Margin and a DoubleAnimation for the height/width, rather than using a converter.
http://msdn.microsoft.com/en-us/library/system.windows.media.animation.thicknessanimation.aspx
http://msdn.microsoft.com/en-us/library/system.windows.media.animation.doubleanimation.aspx
Example:
<StackPanel>
<StackPanel.Resources>
<Thickness x:Key="unfocusedThickness" Top="15" Left="15" Right="15" Bottom="15"/>
<Thickness x:Key="focusedThickness" Top="5" Left="5" Right="5" Bottom="5"/>
</StackPanel.Resources>
<Button Height="100" Width="100" Margin="15">
<Button.Triggers>
<EventTrigger RoutedEvent="Button.GotFocus">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="Height" From="100" To="120" Duration="0:0:0"/>
<DoubleAnimation Storyboard.TargetProperty="Width" From="100" To="120" Duration="0:0:0"/>
<ThicknessAnimation Storyboard.TargetProperty="Margin" From="{StaticResource unfocusedThickness}" To="{StaticResource focusedThickness}" Duration="0:0:0"/>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
<EventTrigger RoutedEvent="Button.LostFocus">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="Height" From="120" To="100" Duration="0:0:0"/>
<DoubleAnimation Storyboard.TargetProperty="Width" From="120" To="100" Duration="0:0:0"/>
<ThicknessAnimation Storyboard.TargetProperty="Margin" To="{StaticResource unfocusedThickness}" From="{StaticResource focusedThickness}" Duration="0:0:0"/>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Button.Triggers>
</Button>
<TextBox />
</StackPanel>

How to animate the background of a textblock when changing the value of the bound property?

I have a pretty simple wpftoolkit:datagrid to show stock market bid and ask.
My grid is bound to an ObservableCollection<PriceViewModel>. My PriceViewModel implements INotifyPropertyChanged.
The grid correctly updates and I have managed to get the background color to animate but it is intermittent in apply the animation.
Below is the XAML and snippet of the view model class.
The idea is just to color red when a price update is lower than the previous and green when it's higher...nothing too fancy.
<WpfToolkit:DataGrid Name="PriceDataGrid" RowHeaderWidth="5"
AutoGenerateColumns="False" VerticalContentAlignment="Center" Margin="0,33,0,0" HorizontalAlignment="Left" Width="868">
<WpfToolkit:DataGrid.Columns>
<WpfToolkit:DataGridTemplateColumn Header="Bid" MinWidth="40">
<WpfToolkit:DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding Bid}" Margin="3,1" x:Name="txtTextBlock">
<TextBlock.Background>
<SolidColorBrush Color="Transparent"></SolidColorBrush>
</TextBlock.Background>
</TextBlock>
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding BidUp}" Value="True">
<DataTrigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<ColorAnimation
BeginTime="00:00:00"
Duration="0:0:0.1"
To="Green"
AutoReverse="True"
Storyboard.TargetName="txtTextBlock"
Storyboard.TargetProperty="(TextBlock.Background).(SolidColorBrush.Color)">
</ColorAnimation>
</Storyboard>
</BeginStoryboard>
</DataTrigger.EnterActions>
</DataTrigger>
<DataTrigger Binding="{Binding BidDown}" Value="True">
<DataTrigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<ColorAnimation
BeginTime="00:00:00"
Duration="0:0:0.1"
To="Red"
AutoReverse="True"
Storyboard.TargetName="txtTextBlock"
Storyboard.TargetProperty="(TextBlock.Background).(SolidColorBrush.Color)">
</ColorAnimation>
</Storyboard>
</BeginStoryboard>
</DataTrigger.EnterActions>
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
</WpfToolkit:DataGridTemplateColumn.CellTemplate>
</WpfToolkit:DataGridTemplateColumn>
<WpfToolkit:DataGridTextColumn Header="Ask" Binding="{Binding Path=Ask}" MinWidth="40" />
</WpfToolkit:DataGrid.Columns>
</WpfToolkit:DataGrid>
And the view model:
public class PriceViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
Price _price;
private bool _bidUp = false;
private bool _bidDown = false;
public bool BidUp
{
get
{
return _bidUp;
}
set
{
_bidUp = value;
OnPropertyChanged("BidUp");
}
}
public bool BidDown
{
get
{
return _bidDown;
}
set
{
_bidDown = value;
OnPropertyChanged("BidDown");
}
}
public double Bid
{
get { return _price.Bid; }
set
{
BidUp = (value > _price.Bid);
BidDown = (value < _price.Bid);
_price.Bid = value;
OnPropertyChanged("Bid");
}
}
public double Ask
{
get { return _price.Ask; }
set
{
AskUp = (value > _price.Ask);
_price.Ask = value;
OnPropertyChanged("Ask");
}
}
public PriceViewModel(Price price)
{
_price = price;
}
private void OnPropertyChanged(string propertyName)
{
if(PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
I tried this and it seems to work better if you stop the Storyboards before you start the new one. To stop a Storyboard, name it and call
<StopStoryboard BeginStoryboardName="bidUpStoryboard"/>
Try it like this
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding BidUp}" Value="True">
<DataTrigger.EnterActions>
<StopStoryboard BeginStoryboardName="bidUpStoryboard"/>
<StopStoryboard BeginStoryboardName="bidDownStoryboard"/>
<BeginStoryboard Name="bidUpStoryboard">
<Storyboard BeginTime="00:00:00">
<ColorAnimation
BeginTime="00:00:00"
Duration="0:0:0.1"
To="Green"
AutoReverse="True"
Storyboard.TargetName="txtTextBlock"
Storyboard.TargetProperty="(TextBlock.Background).(SolidColorBrush.Color)">
</ColorAnimation>
</Storyboard>
</BeginStoryboard>
</DataTrigger.EnterActions>
</DataTrigger>
<DataTrigger Binding="{Binding BidDown}" Value="True">
<DataTrigger.EnterActions>
<StopStoryboard BeginStoryboardName="bidUpStoryboard"/>
<StopStoryboard BeginStoryboardName="bidDownStoryboard"/>
<BeginStoryboard Name="bidDownStoryboard">
<Storyboard BeginTime="00:00:00">
<ColorAnimation
BeginTime="00:00:00"
Duration="0:0:0.1"
To="Red"
AutoReverse="True"
Storyboard.TargetName="txtTextBlock"
Storyboard.TargetProperty="(TextBlock.Background).(SolidColorBrush.Color)">
</ColorAnimation>
</Storyboard>
</BeginStoryboard>
</DataTrigger.EnterActions>
</DataTrigger>
</DataTemplate.Triggers>
Also, if BidUp is set to true two times in a row, it won't trigger the second time since it will go from true to true, so if you want the flashing effect to appear each time a value changes you will have to set it to false at some point. e.g.
public double Bid
{
get { return _price.Bid; }
set
{
BidUp = false;
BidDown = false;
BidUp = (value > _price.Bid);
BidDown = (value < _price.Bid);
_price.Bid = value;
OnPropertyChanged("Bid"); }
}
An alternative may be to have a couple of properties on your PriceViewModel - one for each of the bid and askbackgrounds. You could then have a collection that kept track of what items in the ObserveableCollection had been updated. A timer would periodically check this collection, and reset the cell back colours that were due to be reset.
An example here:
http://noelwatson.com/blog/2012/05/01/WPFBlotterflashingCellsWhenUpdated.aspx

WPF - Confusing DataTrigger/DoubleAnimation behaviour

I'm trying to animate the ScaleY property of a LayoutTransform based on a DataTrigger bound to a boolean on my ViewModel class. The animation happens when the value is first seen to be false by the DataTrigger (when the application first starts) and when i first change it to true in a checkbox's checked event but not when i set it to false in the same checkbox's unchecked event.
A simplified version of what i'm doing is listed below.
The ViewModel class is very simple, containing a single boolean DependencyProperty called Selected.
public class VM : DependencyObject
{
public bool Selected
{
get { return (bool)GetValue(SelectedProperty); }
set { SetValue(SelectedProperty, value); }
}
// Using a DependencyProperty as the backing store for Selected. This enables animation, styling, binding, etc...
public static readonly DependencyProperty SelectedProperty =
DependencyProperty.Register("Selected", typeof(bool), typeof(VM), new UIPropertyMetadata(false));
}
The Window.xaml contains a button and a checkbox. When the checkbox is checked, i set the ViewModel's 'Selected' property to true and false when it is unchecked. Here's the code for both the xaml and it's code-behind.
<Window x:Class="DataTriggers.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:y="clr-namespace:DataTriggers"
Title="Window1" Height="300" Width="300">
<Window.Resources>
<y:VM x:Key="VM"/>
<Style TargetType="Button" x:Key="but">
<Style.Triggers>
<DataTrigger Binding="{Binding Selected}" Value="False">
<DataTrigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Duration="0:0:1"
To="0"
Storyboard.TargetProperty="(LayoutTransform).(ScaleTransform.ScaleY)"/>
</Storyboard>
</BeginStoryboard>
</DataTrigger.EnterActions>
</DataTrigger>
<DataTrigger Binding="{Binding Selected}" Value="True">
<DataTrigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Duration="0:0:1"
To="1"
Storyboard.TargetProperty="(LayoutTransform).(ScaleTransform.ScaleY)"/>
</Storyboard>
</BeginStoryboard>
</DataTrigger.EnterActions>
</DataTrigger>
</Style.Triggers>
</Style>
</Window.Resources>
<StackPanel>
<Button Style="{StaticResource but}" DataContext="{StaticResource VM}">
<Button.LayoutTransform>
<ScaleTransform></ScaleTransform>
</Button.LayoutTransform>
me
</Button>
<CheckBox Checked="CheckBox_Checked" Unchecked="CheckBox_Unchecked"/>
</StackPanel>
public partial class Window1 : Window
{
public Window1()
{
InitializeComponent();
}
private void CheckBox_Checked(object sender, RoutedEventArgs e)
{
VM vm = this.FindResource("VM") as VM;
vm.Selected = true;
}
private void CheckBox_Unchecked(object sender, RoutedEventArgs e)
{
VM vm = this.FindResource("VM") as VM;
vm.Selected = false;
}
}
I know that the DataTrigger fires when the property is false because if i change the DoubleAnimation to a simple Setter operating on the Opacity property then i see the correct results. So it would seem to be a problem with how I'm using the DoubleAnimation.
Any help would be appriciated.
This is ODD behavior but i decided to refactor the 'False' case into the DataTrigger's ExitActions like this -
<DataTrigger Binding="{Binding Selected}" Value="True">
<DataTrigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Duration="0:0:1"
To="1"
Storyboard.TargetProperty="(RenderTransform).(ScaleTransform.ScaleY)"/>
</Storyboard>
</BeginStoryboard>
</DataTrigger.EnterActions>
<DataTrigger.ExitActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Duration="0:0:1"
To="0"
Storyboard.TargetProperty="(RenderTransform).(ScaleTransform.ScaleY)"/>
</Storyboard>
</BeginStoryboard>
</DataTrigger.ExitActions>
</DataTrigger>
That works as intended. I don't know what the difference is between the two cases but at least it's an answer.

Resources