Trying to animate the Path StrokeProperty throws Invalid operation exception - wpf

I have code when a certain value is reached, I want to flash the Stroke of the Path to Red. When I try to apply the animation like this, it says "''System.Windows.Media.Animation.DoubleAnimation' animation object cannot be used to animate property 'Stroke' because it is of incompatible type 'System.Windows.Media.Brush'.'" I also tried ColorAnimation and still a similar exception. Please help.
In the ControlTemplate of xaml I have Path defined as follows.
<ControlTemplate x:Key="FlaskProgressBarTemplate" TargetType="{x:Type local:FlaskProgressBar}">
<Grid>
<Path x:Name="Outline"
StrokeThickness="8"
Stroke="Black">
<Path.Data>
<PathGeometry Figures="M 20,15 L 20,60 0,110 60,110 40,60 40,15 Z"/>
</Path.Data>
</Path>
</Grid>
In the code behind on some method exceution,
private Path _contentPath;
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
_contentPath = this.GetTemplateChild("Outline") as Path;
SetValue(this, Value);
}
private static void SetValue(FlaskProgressBar instance, double value)
{
if (instance._content != null)
{
var rect = instance._content.Rect;
instance._content.Rect = new Rect(rect.X, rect.Height - value, rect.Width, rect.Height);
if (value == 100)
{
AnimateBorder(instance); //Error in this method.
}
}
}
private static void AnimateBorder(FlaskProgressBar instance)
{
var path = instance._contentPath;
if (path != null)
{
path.Stroke = new SolidColorBrush(Colors.Red);
var switchOnAnimation = new DoubleAnimation
{
To = 1,
Duration = TimeSpan.Zero,
BeginTime = TimeSpan.FromSeconds(0.5)
};
var blinkStoryboard = new Storyboard
{
Duration = TimeSpan.FromSeconds(1),
RepeatBehavior = RepeatBehavior.Forever
};
Storyboard.SetTarget(switchOnAnimation, path);
Storyboard.SetTargetProperty(switchOnAnimation, new PropertyPath(Path.StrokeProperty));
blinkStoryboard.Children.Add(switchOnAnimation);
path.BeginStoryboard(blinkStoryboard); //I get an exception on this line.
}
}

You can achieve a blinking effect by animating the brush's Opacity property (which is of type Double) rather than the brush itself.
This is the style from my BlinkingBorder class that featured in a recent blog post.
<Style TargetType="{x:Type vctrl:perBlinkingBorder}">
<Style.Triggers>
<Trigger Property="IsBlinking" Value="True">
<Trigger.EnterActions>
<BeginStoryboard>
<Storyboard TargetProperty="(BlinkingBorderBrush).Opacity">
<DoubleAnimation
AutoReverse="True"
RepeatBehavior="Forever"
From="1"
To="0"
Duration="0:0:0.5">
<DoubleAnimation.EasingFunction>
<SineEase EasingMode="EaseInOut" />
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
</BeginStoryboard>
</Trigger.EnterActions>
</Trigger>
</Style.Triggers>
</Style>

Related

Can I use data-binding on the Duration of an Animation in a ControlTemplate?

I found a good explanation here on SO of how to bind the Duration property of a ColorAnimation to the Value property of a Slider. One uses a converter to convert the Double value from the slider to a Duration, and a Binding to have that set the Duration of the ColorAnimation. Here, abbreviated, is how that works:
<Window.Resources>
<local:DoubleToDurationConverter x:Key="DoubleToDurationConverter" />
</Window.Resources>
<Slider x:Name="slider" />
<Button Content="Click me for an animation">
<Button.Triggers>
<EventTrigger RoutedEvent="Button.Click">
<BeginStoryboard>
<Storyboard>
<ColorAnimation To="Green"
Storyboard.TargetProperty="(Button.Background).(SolidColorBrush.Color)"
FillBehavior="Stop"
Duration="{Binding ElementName=slider,
Path=Value,
Mode=OneWay,
Converter={StaticResource DoubleToDurationConverter}}" />
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Button.Triggers>
</Button>
I tried that and it worked fine for me. But what I want to do is bind the Duration to a dependency property called FadeTime I've added to my custom control. So, in that control's ControlTemplate I have this:
<ControlTemplate.Triggers>
<Trigger Property="IsLit" Value="true">
<Trigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetName="glow"
Storyboard.TargetProperty="Opacity"
To="1"
Duration="{Binding FadeTime, Mode=OneWay,
RelativeSource={RelativeSource TemplatedParent}}"/>
</Storyboard>
</BeginStoryboard>
</Trigger.EnterActions>
</Trigger>
</ControlTemplate.Triggers>
This compiles, but gives me an error message at run-time:
InvalidOperationException: Cannot freeze this Storyboard timeline tree
for use across threads.
How can I bind my DoubleAnimation's Duration to a dependency variable in a custom control's ControlTemplate?
Thanks!
UPDATE
Data-binding is actually gross overkill for what I want to do. Real data-binding would allow for the property's value to change at run-time. All I really want is a way for the developer who is using my custom control to be able to set the Duration of the DoubleAnimation at design time, without having to edit the ControlTemplate. It's okay if the value the developer chooses never changes at run time.
Instead of defining the animation in your XAML markup, you could define it programmatically in the PropertyChangedCallback for the IsLit property.
You could simply define another property that lets the consumer of the control specify the duration of the animation.
Here is an example for you.
Control:
public class MyCustomControl : Control
{
private UIElement glow;
public static readonly DependencyProperty DurationProperty =
DependencyProperty.Register("Duration", typeof(TimeSpan),
typeof(MyCustomControl), new PropertyMetadata(TimeSpan.FromSeconds(1)));
public TimeSpan Duration
{
get { return (TimeSpan)GetValue(DurationProperty); }
set { SetValue(DurationProperty, value); }
}
public static readonly DependencyProperty IsLitProperty =
DependencyProperty.Register("IsLit", typeof(bool),
typeof(MyCustomControl), new PropertyMetadata(false, new PropertyChangedCallback(OnIsLitChanged)));
public bool IsLit
{
get { return (bool)GetValue(IsLitProperty); }
set { SetValue(IsLitProperty, value); }
}
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
glow = Template.FindName("glow", this) as UIElement;
if (glow != null && IsLit)
Animate(glow);
}
private static void OnIsLitChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
bool newValue = (bool)e.NewValue;
if(newValue)
{
MyCustomControl c = d as MyCustomControl;
if(c != null && c.glow != null)
{
c.Animate(c.glow);
}
}
}
private void Animate(UIElement glow)
{
DoubleAnimation animation = new DoubleAnimation();
animation.To = 1;
animation.Duration = Duration;
glow.BeginAnimation(OpacityProperty, animation);
}
}
Template:
<ControlTemplate x:Key="ct" TargetType="local:MyCustomControl">
<Border x:Name="glow" Width="100" Height="100" Background="Red" Opacity="0.1">
</Border>
</ControlTemplate>
Usage:
<local:MyCustomControl Template="{StaticResource ct}" Duration="0:0:5" IsLit="True" />
Basically, you can't use normal bindings inside the storyboard of a control template. Since you just want a way for developers to change the value, one of the following options might work for you:
(1) Use StaticResource: Place a duration object somewhere outside the control template, where it's easier to change for developers. However, it needs to be somewhere statically accessible to the control template, since DynamicResource won't work in this place.
<Duration x:Key="MyCustomDuration">0:0:1</Duration>
... then later
Duration="{StaticResource MyCustomDuration}"
(2) Use a static code behind field with x:Static:
public static class SettingsClass
{
public static Duration MyCustomDuration = new Duration(new TimeSpan(0, 0, 1));
}
and use:
Duration="{x:Static local:SettingsClass.MyCustomDuration}"

Inherited WPF Style with triggers doesn't work

I have a style that fades out a control with animation:
<Style x:Key="ExpireFadeStyle">
<Style.Resources>
<!--Change this in a derived style if required-->
<sys:Double x:Key="FinalVal">0.25</sys:Double>
</Style.Resources>
<Style.Triggers>
<DataTrigger Binding="{Binding IsExpired}" Value="True">
<DataTrigger.EnterActions>
<BeginStoryboard x:Name="ExpireAnimation">
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="Opacity"
From="1" To="{StaticResource FinalVal}" Duration="0:0:3" />
</Storyboard>
</BeginStoryboard>
</DataTrigger.EnterActions>
<DataTrigger.ExitActions>
<StopStoryboard BeginStoryboardName="ExpireAnimation" />
</DataTrigger.ExitActions>
</DataTrigger>
</Style.Triggers>
</Style>
(The IsExpired property always exists in the DataContext of the control where the style is supposed to be used.)
When I use this style directly in a control, it all works fine:
<StackPanel Style="{StaticResource ExpireFadeStyle}">
...etc
But when I derive from this style, just as simple as
<Style x:Key="ExpireTextFadeStyle" BasedOn="{StaticResource ExpireFadeStyle}"/>
...and then use the derived style on the same control the same way, it doesn't work. (The intention is, of course, to change it a bit, particularly the FinalVal, but it has to work in a trivial case first).
The inherited style itself does seem to work: if I add some Setter to it, I see its effect. It just seems that Triggers are not inherited or simply don't work. How to work around it?
That's really a good question. I created an example test application to check this out and I have faced exactly the same problem. In debug mode I checked the value of the Style property and it was correct (BasedOn was set correctly and BasedOn.Triggers contains your Trigger). Nevertheless, the Trigger doesn't fire up when needed.
I have found a solution that make this work, however it is not a perfect one...
I defined an attached dependency property which copies the triggers when is set to true.
public class StyleTriggersInheritanceHelper
{
public static readonly DependencyProperty AddInheritedTriggersProperty =
DependencyProperty.RegisterAttached("AddInheritedTriggers", typeof(bool), typeof(FrameworkElement), new FrameworkPropertyMetadata(new PropertyChangedCallback(OnAddInheritedTriggers)));
private static void OnAddInheritedTriggers(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
var b = e.NewValue as bool?;
if (b.HasValue && b.Value)
{
FrameworkElement element = sender as FrameworkElement;
if(element != null)
{
Style style = element.Style;
if(style != null)
{
Style baseStyle = element.Style.BasedOn;
Style newStyle = new Style() { BasedOn = style };
if(baseStyle != null)
{
foreach (var tr in style.Triggers)
newStyle.Triggers.Add(tr);
foreach (var tr in baseStyle.Triggers)
newStyle.Triggers.Add(tr);
}
element.Style = newStyle;
}
}
}
}
public static bool GetAddInheritedTriggers(DependencyObject obj)
{
return (bool)obj.GetValue(AddInheritedTriggersProperty);
}
public static void SetAddInheritedTriggers(DependencyObject obj, bool value)
{
obj.SetValue(AddInheritedTriggersProperty, value);
}
}
And xaml looks as follows:
<local:ExpirdedControl IsExpired="{Binding IsExpired}" Style="{StaticResource InheritedStyle}" x:Name="ExpiredName" local:StyleTriggersInheritanceHelper.AddInheritedTriggers="True"/>
Note that AddInheritedTriggers attached property is set to true and then Triggers are copied. One important thing is that you have to set AddInheritedTriggers after applying the Style, otherwise it wouldn't work!
For me it works, however the solution is not elegant, so I would like to see a better try of solving this issue.

Two element hit in canvas

I have a bomb(element) in the canvas that is moving with doublanimation(vertical only) and I want to see if it hits an airplane(also an element on the canvas).
I wrote this code:
private bool HitCheck()
{
AirPlaneRect.Location = airplane.PointToScreen(new Point(Canvas.GetLeft(airplane), Canvas.GetTop(airplane)));
Rect BombPos = new Rect(bombPoint.X, Canvas.GetTop(this),this.bombImage.Height, this.bombImage.Width);
if (BombPos.IntersectsWith(AirPlaneRect))
{
return true;
}
return false;
}
but for some reason the bomb location is always the starting one.
I'm using a timer that call this method every half a second.
Instead of using a Timer to check periodic if they intersects, it is more accurate to check it when the DoubleAnimation is changed. When that happens, CurrentTimeInvalidated is raised.
I created a little sample to demonstrate what I mean. Here is the XAML:
<Canvas>
<!--be shure to assign Top and Left of all objects-->
<Rectangle Canvas.Left="0" Canvas.Top="0" Name="bomb"
Width="20" Height="100" Fill="gray"/>
<Rectangle Canvas.Left="0" Canvas.Top="200" Name="airPlane"
Width="300" Height="50" Fill="lightblue"/>
<Canvas.Triggers>
<!--Run the animation at startup-->
<EventTrigger RoutedEvent="Loaded">
<BeginStoryboard >
<Storyboard >
<DoubleAnimation Name="doubleAnimation"
Storyboard.TargetName="bomb"
Storyboard.TargetProperty="(Canvas.Top)"
From="0" To="400" Duration="0:0:5"
CurrentTimeInvalidated="CurrentTimeInvalidated"/>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Canvas.Triggers>
</Canvas>
And the code behind:
private void CurrentTimeInvalidated(object sender, EventArgs e)
{
if (HitTest(airPlane, bomb))
{
MessageBox.Show("Hit!");
//unregister the event handler to avoid to see the messagebox more than once
doubleAnimation.CurrentTimeInvalidated -= CurrentTimeInvalidated;
}
}
static bool HitTest(FrameworkElement airPlane, FrameworkElement bomb)
{
var r1 = new Rect(Canvas.GetLeft(airPlane), Canvas.GetTop(airPlane), airPlane.ActualWidth, airPlane.ActualHeight);
var r2 = new Rect(Canvas.GetLeft(bomb), Canvas.GetTop(bomb), bomb.ActualWidth, bomb.ActualHeight);
return r1.IntersectsWith(r2);
}
If you do it similar to me, it should work. Or is your code structure significant different to my?

WPF Animate property of child without using Name

I'm trying to create a storyboard in XAML that animates a property of one of the child elements of an element which raises an event. But I can't seem to get it to work without using Names, which is something I can't really do in this specific situation.
I'm basically trying something like this (much simplified of course):
<Canvas>
<Canvas.Triggers>
<EventTrigger RoutedEvent="FrameworkElement.Loaded">
<EventTrigger.Actions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="Children[0].(Canvas.Left)" From="0" To="400" />
</Storyboard>
</BeginStoryboard>
</EventTrigger.Actions>
</EventTrigger>
</Canvas.Triggers>
<Button Canvas.Left="20" Canvas.Top="20">A</Button>
<Button Canvas.Left="40" Canvas.Top="20">B</Button>
</Canvas>
Any ideas on how this could be achieved?
Providing that the UIElement you are indexing in the animation exists (i.e. already present on the Canvas) then you can do the following:
<Canvas x:Name="MyCanvas">
<Button x:Name="btn" Canvas.Left="20" Canvas.Top="20">A</Button>
<Button Canvas.Left="40" Canvas.Top="20">B</Button>
<Canvas.Triggers>
<EventTrigger RoutedEvent="FrameworkElement.Loaded">
<EventTrigger.Actions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.Target="{Binding ElementName=MyCanvas, Path=Children[0]}"
Storyboard.TargetProperty="(Canvas.Left)" From="0" To="400" />
</Storyboard>
</BeginStoryboard>
</EventTrigger.Actions>
</EventTrigger>
</Canvas.Triggers>
</Canvas>
Notice how I have moved the addition of the Buttons above the Trigger. If the Buttons are below the Trigger as in your question, trying to access Children[0] will throw an ArgumentOutOfRangeException because there are no children at this point.
To use the Storyboard.TargetProperty in the animation, it should always be a dependency property. Children property gets a UIElementCollection of child elements of this Panel (Canvas). Therefore, the following construction Children [n] return UIElement, which should lead to a certain type, to access its dependency property.
This can be done in the code as follows:
Button MyButton = (Button)MyCanvas.Children[0];
MessageBox.Show(MyButton.Width.ToString());
All of these actions missing in the animation by default, this is your construction will not work.
I propose to create animations in the code where this conversion possible.
To demonstrate this, I created a Canvas, in the event Loaded having registered animation. Element number is set via an attached dependency property (of course, the example can be implemented in various ways). Below is my example:
XAML
<Grid>
<local:MyCanvas x:Name="MyCanvas" local:ClassForAnimation.Children="1">
<Button Canvas.Left="20" Canvas.Top="20">A</Button>
<Button Canvas.Left="40" Canvas.Top="20">B</Button>
</local:MyCanvas>
</Grid>
Code behind
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
}
public class MyCanvas : Canvas
{
public MyCanvas()
{
this.Loaded += new RoutedEventHandler(MyCanvas_Loaded);
}
private void MyCanvas_Loaded(object sender, RoutedEventArgs e)
{
MyCanvas myCanvas = sender as MyCanvas;
// Get No. of children
int children = ClassForAnimation.GetChildren(myCanvas);
// Get current Button for animation
Button MyButton = (Button)myCanvas.Children[children];
if (myCanvas != null)
{
DoubleAnimation doubleAnimation = new DoubleAnimation();
doubleAnimation.From = 0;
doubleAnimation.To = 400;
MyButton.BeginAnimation(Button.WidthProperty, doubleAnimation);
}
}
}
public class ClassForAnimation : DependencyObject
{
public static readonly DependencyProperty ChildrenProperty;
public static void SetChildren(DependencyObject DepObject, int value)
{
DepObject.SetValue(ChildrenProperty, value);
}
public static int GetChildren(DependencyObject DepObject)
{
return (int)DepObject.GetValue(ChildrenProperty);
}
static ClassForAnimation()
{
PropertyMetadata MyPropertyMetadata = new PropertyMetadata(0);
ChildrenProperty = DependencyProperty.RegisterAttached("Children",
typeof(int),
typeof(ClassForAnimation),
MyPropertyMetadata);
}
}
Note: Access to the items in the Canvas should only be done in the event Loaded, or when it ended. Otherwise, the items are not available because they are not loaded.

I can't animate a custom property in WPF

I've been struggling with this for hours and I can't find out what I'm doing wrong. Please help me find my mistake.
I created a user control with one custom dependency property and I want to animate this property.
Here is my class:
public partial class UserControl1 : UserControl
{
public UserControl1()
{
InitializeComponent();
}
public long Val
{
get { return (long)GetValue(ValProperty); }
set {
SetValue(ValProperty, value);
// Update a text block for debug
((Class1)this.Resources["class1"]).Val = value;
}
}
public static readonly DependencyProperty ValProperty =
DependencyProperty.Register("Val", typeof(long), typeof(UserControl1), new UIPropertyMetadata(0L));
}
Here is the code that is supposed to animate it (there's an instance of UserControl1 called usercontrol11):
Int64Animation myAnimation = new Int64Animation();
myAnimation.From = 100;
myAnimation.To = 200;
myAnimation.Duration = new Duration(TimeSpan.FromSeconds(1));
Storyboard.SetTargetName(myAnimation, userControl11.Name);
Storyboard.SetTargetProperty(myAnimation, new PropertyPath(UserControl1.ValProperty));
Storyboard myStoryboard = new Storyboard();
myStoryboard.Children.Add(myAnimation);
myStoryboard.Begin(this);
I also tried the XAML approach, but it didn't work either (in the following XAML, the Width animation works fine, but the Val doesn't):
<my:UserControl1 HorizontalAlignment="Left" Width="150" Margin="72,45,0,0" x:Name="userControl11" VerticalAlignment="Top" Background="#FFFFD100">
<my:UserControl1.Triggers>
<EventTrigger RoutedEvent="MouseEnter">
<EventTrigger.Actions>
<BeginStoryboard>
<Storyboard>
<Int64Animation To="300" Duration="0:0:1"
Storyboard.TargetProperty="Val" />
<DoubleAnimation To="300" Duration="0:0:1"
Storyboard.TargetProperty="Width" />
</Storyboard>
</BeginStoryboard>
</EventTrigger.Actions>
</EventTrigger>
</my:UserControl1.Triggers>
</my:UserControl1>
Any help will be much appreciated!
Well, it's actually animated. You're just checking in the wrong place, animation won't call Val property set accessor. You can, for example, override OnPropertyChanged method to see changes:
protected override void OnPropertyChanged(DependencyPropertyChangedEventArgs e)
{
base.OnPropertyChanged(e);
if(e.Property == ValProperty)
{
MessageBox.Show(e.NewValue.ToString());
}
}

Resources