I'm trying to translate this C# code. My attempt so far:
type MyButtonSimple() as self =
inherit Button()
static let TapEvent =
EventManager.RegisterRoutedEvent
( "Tap", RoutingStrategy.Bubble,
typeof<RoutedEventHandler>, typeof<MyButtonSimple>)
let tapEvent = new Event<RoutedEventHandler, RoutedEventArgs>()
let raiseTapEvent() =
let newEventArgs = new RoutedEventArgs(TapEvent)
tapEvent.Trigger(self, newEventArgs)
[<CLIEvent>]
member x.Tap = tapEvent.Publish
// For demonstration purposes we raise the event when clicked
override self.OnClick() =
raiseTapEvent()
MainWindow.xaml
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:funk="clr-namespace:funk;assembly=appWPF"
Title="Routed" Height="300" Width="400">
<StackPanel Background="LightGray">
<funk:MyButtonSimple Content="Spin" Background="#808080" Foreground="LightGray">
<funk:MyButtonSimple.RenderTransform>
<TransformGroup>
<RotateTransform x:Name="rotate"/>
</TransformGroup>
</funk:MyButtonSimple.RenderTransform>
<funk:MyButtonSimple.Triggers>
<EventTrigger RoutedEvent="funk:MyButtonSimple.Tap">
<BeginStoryboard>
<Storyboard RepeatBehavior="Forever">
<DoubleAnimation
Storyboard.TargetName="rotate"
Storyboard.TargetProperty="Angle"
From="0" To="90" Duration="0:0:2"/>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</funk:MyButtonSimple.Triggers>
</funk:MyButtonSimple>
</StackPanel>
</Window>
Clicking the button doesn't start the animation.
Changing the code RoutedEvent="funk:MyButtonSimple.Tap" to RoutedEvent="GotFocus" gives a working example (Button.Click is overridden).
Any ideas what I'm doing wrong?
This is tricky because it's doing quite a bit of things that aren't standard in F#.
A translation of that C# code would be:
type MyButtonSimple() as self =
inherit Button()
static let tapEvent =
EventManager.RegisterRoutedEvent
( "Tap", RoutingStrategy.Bubble,
typeof<RoutedEventHandler>, typeof<MyButtonSimple>)
// Create a custom event so you can override AddHandler/RemoveHandler behavior
let tapEvent =
{ new IDelegateEvent<RoutedEventHandler> with
member this.AddHandler del = self.AddHandler(MyButtonSimple.TapEvent, del)
member this.RemoveHandler del = self.RemoveHandler(MyButtonSimple.TapEvent, del) }
// Raise via routed eventing strategy
let raiseTapEvent() =
let newEventArgs = new RoutedEventArgs(MyButtonSimple.TapEvent)
self.RaiseEvent newEventArgs
// This isn't exactly the same, but public static fields aren't allowed in F#, and
// this works for WPF
static member TapEvent with get() = tapEvent
[<CLIEvent>]
member x.Tap = tapEvent
// For demonstration purposes we raise the event when clicked
override self.OnClick() =
raiseTapEvent()
The main "gotchas" here are that you need to override the standard event behavior, which requires implementing IDelegateEvent yourself instead of using the normal F# event management. Also, you can't do public static readonly fields in F#, so this wraps the event into a property. WPF seems to be fine with this, even though it's not the "standard" way to implement routed events.
Related
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}"
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 want to have a slider that returns to 0 when the user stops dragging.
So far I have this:
<Window x:Class="CenteredSliderTest.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<DockPanel>
<!--Value="{Binding ZSpeed}"-->
<Slider DockPanel.Dock="Left"
x:Name="ZSlider"
Minimum="-100" Maximum="100"
SelectionStart="-20" SelectionEnd="20"
Orientation="Vertical"
TickFrequency="10"
TickPlacement="TopLeft"
AutoToolTipPlacement="TopLeft"
AutoToolTipPrecision="2"
LargeChange="10"
SmallChange="1"
IsDirectionReversed="True"
Focusable="False"
>
<Slider.Triggers>
<EventTrigger RoutedEvent="LostMouseCapture" SourceName="ZSlider">
<EventTrigger.Actions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation
Storyboard.TargetName="ZSlider"
Storyboard.TargetProperty="Value"
From="{Binding Value, ElementName=ZSlider}"
To="0.0"
Duration="0:0:1.5"
FillBehavior="Stop"
/>
</Storyboard>
</BeginStoryboard>
</EventTrigger.Actions>
</EventTrigger>
</Slider.Triggers>
</Slider>
<TextBlock Text="{Binding ZSpeed}" />
</DockPanel>
</Window>
This works as long as I don't bind the slider value to my DependencyProperty ZSpeed.
As soon as I do this, the slider jumps back to the original value and at the second attempt the slider can't be dragged anymore.
So what can I do (preferable in xaml) in order to get the animation modify not only the slider but also the ZSpeed property?
EDIT
Code in MainWindow:
public partial class MainWindow : Window
{
public double ZSpeed
{
get { return (double)GetValue(ZSpeedProperty); }
set { SetValue(ZSpeedProperty, value); }
}
// Using a DependencyProperty as the backing store for ZSpeed. This enables animation, styling, binding, etc...
public static readonly DependencyProperty ZSpeedProperty =
DependencyProperty.Register("ZSpeed", typeof(double), typeof(MainWindow), new UIPropertyMetadata(0.0));
public MainWindow()
{
InitializeComponent();
this.DataContext = this;
Binding binding = new Binding("Value") { Source = ZSlider };
this.SetBinding(ZSpeedProperty, binding);
}
}
You might reverse the direction of the binding. Instead of binding the Slider's Value to ZSpeed you could bind ZSpeed to Value. This would also be the "natural" binding direction if the Slider is meant to change ZSpeed, but ZSpeed won't change otherwise.
EDIT: If ZSpeed is a dependency property in some data class MyData you could create a binding in code like this:
MyData dataObject = ...
Binding binding = new Binding("Value") { Source = ZSlider };
dataObject.SetBinding(MyData.ZSpeedProperty, binding);
SECOND EDIT: Picking up Daniels suggestion, you might animate ZSpeed instead of the Slider's Value. Bind the Value to ZSpeed as before, remove the EventTrigger and add an event handler for LostMouseCapture:
<Slider x:Name="ZSlider" ...
Value="{Binding ZSpeed}"
LostMouseCapture="ZSlider_LostMouseCapture"/>
Code behind:
private void ZSlider_LostMouseCapture(object sender, MouseEventArgs e)
{
DoubleAnimation animation = new DoubleAnimation
{
From = ZSpeed,
To = 0d,
Duration = TimeSpan.FromSeconds(1.5 * Math.Abs(ZSpeed) / 100d),
FillBehavior = FillBehavior.Stop
};
ZSpeed = 0d;
BeginAnimation(ZSpeedProperty, animation);
}
You should be using FillBehavior.HoldEnd.
Edit: That apparently doesn't work. You could set the ZSpeed value to 0 manually in the StoryBoard.Completed event.
I'm quite new in WPF-Animations so apologize if it's too easy, but I can't find any answer (nor my question). So:
I have very simple animation - some canvas is rotating from angle -45 degrees to 45 degrees. All animation is made in XAML (got some issues with code-behind animation). I would like to bind function when value equals 0 (e.g. make some noise then). How can I approach this?
Thank you for all hints.
I have two options to solve this problem. One is intrusive but gives you more control over the actual value, another is not intrusive but gives you only indirect control over the value. I'll give the sample code with both options at the end of the answer.
Non intrusive solution
Subscribe to the CurrentTimeInvalidated event on your DoubleAnimation object. If you know the animation function and its duration you can approximately say when the animation value is close to your event. For say, animation duration is 500 ms, and the animation function is linear. Then you can say, that at 250ms you are halfway through.
Intrusive solution
Remember: DoubleAnimation (like any other animation) is just a class and you are welcome to inherit it and override any virtual member. In case of DoubleAnimation of particular interest is GetCurrentValueCore() method. And of course you can define any events or dependency properties on this new class. Now you see where it's all going. Inherit DoubleAnimation, override GetCurrentValueCore(), define ValueChanged event, and fire it on every call to GetCurrentValueCore().
Code example
MainWindow.xaml
<Window x:Class="WpfPlayground.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525"
xmlns:l="clr-namespace:WpfPlayground">
<Grid>
<Grid.Triggers>
<EventTrigger RoutedEvent="Loaded">
<BeginStoryboard>
<Storyboard Duration="00:00:00.500" Storyboard.TargetName="rectangle" RepeatBehavior="Forever">
<l:DoubleAnimationWithCallback From="0"
To="180" Duration="00:00:00.500"
Storyboard.TargetProperty="(FrameworkElement.LayoutTransform).(RotateTransform.Angle)"
Callback="{Binding AnimationCallback, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type l:MainWindow}}}"
CurrentTimeInvalidated="OnCurrentTimeInvalidated" />
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Grid.Triggers>
<!--We animate this rectangle-->
<Rectangle x:Name="rectangle" Width="50" Height="50" Fill="Green">
<Rectangle.LayoutTransform>
<RotateTransform />
</Rectangle.LayoutTransform>
</Rectangle>
<!--Debug information-->
<TextBlock x:Name="tbTime" HorizontalAlignment="Center" VerticalAlignment="Top"/>
<TextBlock x:Name="tbAngle" HorizontalAlignment="Center" VerticalAlignment="Bottom"/>
</Grid>
</Window>
MainWindow.xaml.cs
using System;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Media.Animation;
using System.Globalization;
namespace WpfPlayground
{
public partial class MainWindow : Window
{
public Func<double, double> AnimationCallback { get { return AnimationCallbackImpl; } }
public MainWindow()
{
InitializeComponent();
}
private double AnimationCallbackImpl(double value)
{
tbAngle.Text = value.ToString(CultureInfo.CurrentCulture);
return value;
}
private void OnCurrentTimeInvalidated(object sender, EventArgs e)
{
tbTime.Text = ((AnimationClock)sender).CurrentTime.ToString();
}
}
}
DoubleAnimationWithCallback.cs
using System;
using System.Windows;
using System.Windows.Media.Animation;
namespace WpfPlayground
{
public class DoubleAnimationWithCallback : DoubleAnimation
{
// Cache Callback DP, to avoid performance hit.
private Func<double, double> _callback;
// reference to frozen instance. See comments below for explanation.
private DoubleAnimationWithCallback _coreInstance;
public Func<double, double> Callback
{
get { return (Func<double, double>)GetValue(CallbackProperty); }
set { SetValue(CallbackProperty, value); }
}
public static readonly DependencyProperty CallbackProperty =
DependencyProperty.Register("Callback", typeof(Func<double, double>), typeof(DoubleAnimationWithCallback), new PropertyMetadata(null, OnCallbackChanged));
private static void OnCallbackChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
{
var dawc = o as DoubleAnimationWithCallback;
if (dawc != null)
{
dawc.UpdateCallback(e.NewValue as Func<double, double>);
}
}
private void UpdateCallback(Func<double, double> callback)
{
_callback = callback;
if (_coreInstance != null)
{
_coreInstance._callback = _callback;
}
}
protected override Freezable CreateInstanceCore()
{
if (_coreInstance == null)
{
// When callback changes we update corresponding callback on
// the frozen object too.
_coreInstance = new DoubleAnimationWithCallback()
{
Callback = Callback
};
}
return _coreInstance;
}
protected override double GetCurrentValueCore(double defaultOriginValue, double defaultDestinationValue, AnimationClock animationClock)
{
var value = base.GetCurrentValueCore(defaultOriginValue, defaultDestinationValue, animationClock);
if (_callback != null)
{
return _callback(value);
}
return value;
}
}
}
There is one caveat though: animation pipeline works with Freezable objects, so you'll have to override CreateInstanceCore() method and return proper instance. Furthermore, if you change Callback dependency property on the real object you'll have to also update the frozen one. It is not quite welcome practice and that's why I call it intrusive. Be very careful with this code and test it throughly. It just shows a possible direction and is not the final destination.
Hope this helps
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());
}
}