Consider this code:
<UserControl x:Class="MyApp.MyControl"
...
xmlns:local="clr-namespace:MyApp"
DataContext="{Binding RelativeSource={RelativeSource Mode=Self}}">
<UserControl.Template>
<ControlTemplate>
<ControlTemplate.Resources>
<Storyboard x:Key="MyStory">
<ColorAnimationUsingKeyFrames Storyboard.TargetProperty="(Border.BorderBrush).(SolidColorBrush.Color)" Storyboard.TargetName="brdBase">
<SplineColorKeyFrame KeyTime="0:0:1" Value="Red"/>
</ColorAnimationUsingKeyFrames>
</Storyboard>
</ControlTemplate.Resources>
<Border x:Name="brdBase" BorderThickness="1" BorderBrush="Cyan" Background="Black">
...
</Border>
<ControlTemplate.Triggers>
<Trigger SourceName="brdBase" Property="IsMouseOver" Value="True">
<Trigger.EnterActions>
<BeginStoryboard Storyboard="{StaticResource MyStory}"/>
</Trigger.EnterActions>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</UserControl.Template>
</UserControl>
The above code works with no problem. Now, I wanna bind key-frame value of MyStory
to a DP (named SpecialColor
) of this user-control like so:
<Storyboard x:Key="MyStory">
<ColorAnimationUsingKeyFrames Storyboard.TargetProperty="(Border.BorderBrush).(SolidColorBrush.Color)" Storyboard.TargetName="brdBase">
<SplineColorKeyFrame KeyTime="0:0:1" Value="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type local:MyControl}}, Path=SpecialColor}"/>
</ColorAnimationUsingKeyFrames>
</Storyboard>
which makes an error:
Cannot freeze this Storyboard timeline tree for use across threads.
It's possible to do this using code behind. But how can I do it in XAML only?
Code-Behind Aided Solution:
►Step 1: Putting the MyStory
storyboard into the brdBase
resources.
<UserControl.Template>
<ControlTemplate>
<Border x:Name="brdBase" BorderThickness="1" BorderBrush="Cyan" Background="Black">
<Border.Resources>
<Storyboard x:Key="MyStory">
<ColorAnimationUsingKeyFrames Storyboard.TargetProperty="(Border.BorderBrush).(SolidColorBrush.Color)" Storyboard.TargetName="brdBase">
<SplineColorKeyFrame KeyTime="0:0:1" Value="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type local:MyControl}}, Path=SpecialColor}"/>
</ColorAnimationUsingKeyFrames>
</Storyboard>
</Border.Resources>
...
</Border>
<ControlTemplate.Triggers>
<Trigger SourceName="brdBase" Property="IsMouseOver" Value="True">
<Trigger.EnterActions>
<BeginStoryboard Storyboard="{StaticResource MyStory}"/>
</Trigger.EnterActions>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</UserControl.Template>
Error: Cannot find resource named 'MyStory'. Resource names are case sensitive.
►Step 2: Eliminating Trigger
on IsMouseOver
property and begin the MyStory
from code behind.
<UserControl.Template>
<ControlTemplate>
<Border x:Name="brdBase" BorderThickness="1" BorderBrush="Cyan" Background="Black" MouseEnter="brdBase_MouseEnter">
<Border.Resources>
<Storyboard x:Key="MyStory">
<ColorAnimationUsingKeyFrames Storyboard.TargetProperty="(Border.BorderBrush).(SolidColorBrush.Color)" Storyboard.TargetName="brdBase">
<SplineColorKeyFrame KeyTime="0:0:1" Value="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type local:MyControl}}, Path=SpecialColor}"/>
</ColorAnimationUsingKeyFrames>
</Storyboard>
</Border.Resources>
</Border>
</ControlTemplate>
</UserControl.Template>
C# Code-Behind:
private void brdBase_MouseEnter(object sender, MouseEventArgs e)
{
Border grdRoot = (Border)this.Template.FindName("brdBase", this);
Storyboard story = grdRoot.Resources["MyStory"] as Storyboard;
story.Begin(this, this.Template);
}
►Step 3: The solution is already done, but it doesn't work at the first time. Fortunately, there is a workaround for this issue. It's enough to put the ControlTemplate
in a Style
.
(I need other Trigger
types than EventTrigger
and must wrap the UserControl
elements with the ControlTemplate
.)
Update:
The idea about using ObjectDataProvider
failed.
Here is the code:
<UserControl.Template>
<ControlTemplate>
<ControlTemplate.Resources>
<local:StoryboardFinder x:Key="StoryboardFinder1" AssociatedControl="{Binding ElementName=brdBase}"/>
<ObjectDataProvider x:Key="dataProvider" ObjectInstance="{StaticResource StoryboardFinder1}" MethodName="Finder">
<ObjectDataProvider.MethodParameters>
<sys:String>MyStory</sys:String>
</ObjectDataProvider.MethodParameters>
</ObjectDataProvider>
</ControlTemplate.Resources>
<Border x:Name="brdBase" BorderThickness="1" BorderBrush="Cyan" Background="Black">
<Border.Resources>
<Storyboard x:Key="MyStory">
<ColorAnimationUsingKeyFrames Storyboard.TargetProperty="(Border.BorderBrush).(SolidColorBrush.Color)" Storyboard.TargetName="brdBase">
<SplineColorKeyFrame KeyTime="0:0:1" Value="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type local:MyControl}}, Path=SpecialColor}"/>
</ColorAnimationUsingKeyFrames>
</Storyboard>
</Border.Resources>
...
</Border>
<ControlTemplate.Triggers>
<Trigger SourceName="brdBase" Property="IsMouseOver" Value="True">
<Trigger.EnterActions>
<BeginStoryboard Storyboard="{StaticResource dataProvider}"/>
</Trigger.EnterActions>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</UserControl.Template>
The StoryboardFinder class:
public class StoryboardFinder : DependencyObject
{
#region ________________________________________ AssociatedControl
public Control AssociatedControl
{
get { return (Control)GetValue(AssociatedControlProperty); }
set { SetValue(AssociatedControlProperty, value); }
}
public static readonly DependencyProperty AssociatedControlProperty =
DependencyProperty.Register("AssociatedControl",
typeof(Control),
typeof(StoryboardFinder),
new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.None));
#endregion
public Storyboard Finder(string resourceName)
{
//
// Associated control is always null :(
//
return new Storyboard();
}
}
What if this code was true?
<UserControl x:Class="MyApp.MyControl"
...
xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
xmlns:l="clr-namespace:MyApp"
DataContext="{Binding RelativeSource={RelativeSource Mode=Self}}">
<UserControl.Resources>
<Style TargetType="{x:Type l:MyControl}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type l:MyControl}">
<Border x:Name="brdBase" BorderThickness="1" BorderBrush="Cyan" Background="Black">
<Border.Resources>
<Storyboard x:Key="MyStory">
<ColorAnimationUsingKeyFrames Storyboard.TargetProperty="(Border.BorderBrush).(SolidColorBrush.Color)" Storyboard.TargetName="brdBase">
<SplineColorKeyFrame KeyTime="0:0:1" Value="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type l:MyControl}}, Path=SpecialColor}"/>
</ColorAnimationUsingKeyFrames>
</Storyboard>
</Border.Resources>
<i:Interaction.Triggers>
<l:InteractiveTrigger Property="IsMouseOver" Value="True">
<l:InteractiveTrigger.CommonActions>
<BeginStoryboard Storyboard="{StaticResource MyStory}"/>
</l:InteractiveTrigger.CommonActions>
</l:InteractiveTrigger>
</i:Interaction.Triggers>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</UserControl.Resources>
</UserControl>
If so, I could have a Trigger on IsMouseOver
property...
I'm glad to say it's a working code :) I could only use EventTrigger
in <Border.Triggers>
tag. It was the limitation. So I started thinking about this idea: What if I could have a custom trigger which can work in FrameworkElement.Triggers
scope? Here is the code:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Windows;
using System.Windows.Interactivity;
using System.Windows.Media.Animation;
namespace TriggerTest
{
/// <summary>
/// InteractiveTrigger is a trigger that can be used as the System.Windows.Trigger but in the System.Windows.Interactivity.
/// <para>
/// Note: There is neither `EnterActions` nor `ExitActions` in this class. The `CommonActions` can be used instead of `EnterActions`.
/// Also, the `Actions` property which is of type System.Windows.Interactivity.TriggerAction can be used.
/// </para>
/// <para> </para>
/// <para>
/// There is only one kind of triggers (i.e. EventTrigger) in the System.Windows.Interactivity. So you can use the following triggers in this namespace:
/// <para>1- InteractiveTrigger : Trigger</para>
/// <para>2- InteractiveMultiTrigger : MultiTrigger</para>
/// <para>3- InteractiveDataTrigger : DataTrigger</para>
/// <para>4- InteractiveMultiDataTrigger : MultiDataTrigger</para>
/// </para>
/// </summary>
public class InteractiveTrigger : TriggerBase<FrameworkElement>
{
#region ___________________________________________________________________________________ Properties
#region ________________________________________ Value
/// <summary>
/// [Wrapper property for ValueProperty]
/// <para>
/// Gets or sets the value to be compared with the property value of the element. The comparison is a reference equality check.
/// </para>
/// </summary>
public object Value
{
get { return (object)GetValue(ValueProperty); }
set { SetValue(ValueProperty, value); }
}
public static readonly DependencyProperty ValueProperty =
DependencyProperty.Register("Value",
typeof(object),
typeof(InteractiveTrigger),
new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.None, OnValuePropertyChanged));
private static void OnValuePropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
InteractiveTrigger instance = sender as InteractiveTrigger;
if (instance != null)
{
if (instance.CanFire)
instance.Fire();
}
}
#endregion
/// <summary>
/// Gets or sets the name of the object with the property that causes the associated setters to be applied.
/// </summary>
public string SourceName
{
get;
set;
}
/// <summary>
/// Gets or sets the property that returns the value that is compared with this trigger.Value property. The comparison is a reference equality check.
/// </summary>
public DependencyProperty Property
{
get;
set;
}
/// <summary>
/// Gets or sets a collection of System.Windows.Setter objects, which describe the property values to apply when the trigger object becomes active.
/// </summary>
public List<Setter> Setters
{
get;
set;
}
/// <summary>
/// Gets or sets the collection of System.Windows.TriggerAction objects to apply when this trigger object becomes active.
/// </summary>
public List<System.Windows.TriggerAction> CommonActions
{
get;
set;
}
/// <summary>
/// Gets a value indicating whether this trigger can be active to apply setters and actions.
/// </summary>
private bool CanFire
{
get
{
if (this.AssociatedObject == null)
{
return false;
}
else
{
object associatedValue;
if (string.IsNullOrEmpty(SourceName))
associatedValue = this.AssociatedObject.GetValue(Property);
else
associatedValue = (this.AssociatedObject.FindName(SourceName) as DependencyObject).GetValue(Property);
TypeConverter typeConverter = TypeDescriptor.GetConverter(Property.PropertyType);
object realValue = typeConverter.ConvertFromString(Value.ToString());
return associatedValue.Equals(realValue);
}
}
}
#endregion
#region ___________________________________________________________________________________ Methods
/// <summary>
/// Fires (activates) current trigger by setting setter values and invoking all actions.
/// </summary>
private void Fire()
{
//
// Setting setters values to their associated properties..
//
foreach (Setter setter in Setters)
{
if (string.IsNullOrEmpty(setter.TargetName))
this.AssociatedObject.SetValue(setter.Property, setter.Value);
else
(this.AssociatedObject.FindName(setter.TargetName) as DependencyObject).SetValue(setter.Property, setter.Value);
}
//
// Firing actions..
//
foreach (System.Windows.TriggerAction action in CommonActions)
{
Type actionType = action.GetType();
if (actionType == typeof(BeginStoryboard))
{
(action as BeginStoryboard).Storyboard.Begin();
}
else
throw new NotImplementedException();
}
this.InvokeActions(null);
}
#endregion
#region ___________________________________________________________________________________ Events
public InteractiveTrigger()
{
Setters = new List<Setter>();
CommonActions = new List<System.Windows.TriggerAction>();
}
protected override void OnAttached()
{
base.OnAttached();
if (Property != null)
{
object propertyAssociatedObject;
if (string.IsNullOrEmpty(SourceName))
propertyAssociatedObject = this.AssociatedObject;
else
propertyAssociatedObject = this.AssociatedObject.FindName(SourceName);
//
// Adding a property changed listener to the property associated-object..
//
DependencyPropertyDescriptor dpDescriptor = DependencyPropertyDescriptor.FromProperty(Property, propertyAssociatedObject.GetType());
dpDescriptor.AddValueChanged(propertyAssociatedObject, PropertyListener_ValueChanged);
}
}
protected override void OnDetaching()
{
base.OnDetaching();
if (Property != null)
{
object propertyAssociatedObject;
if (string.IsNullOrEmpty(SourceName))
propertyAssociatedObject = this.AssociatedObject;
else
propertyAssociatedObject = this.AssociatedObject.FindName(SourceName);
//
// Removing previously added property changed listener from the associated-object..
//
DependencyPropertyDescriptor dpDescriptor = DependencyPropertyDescriptor.FromProperty(Property, propertyAssociatedObject.GetType());
dpDescriptor.RemoveValueChanged(propertyAssociatedObject, PropertyListener_ValueChanged);
}
}
private void PropertyListener_ValueChanged(object sender, EventArgs e)
{
if (CanFire)
Fire();
}
#endregion
}
}
I've also created other trigger types (i.e. InteractiveMultiTrigger
, InteractiveDataTrigger
, InteractiveMultiDataTrigger
) as well as some more actions which makes it possible to have a conditional and multi-conditional EventTriggers. I'll publish them all if you professional guys confirm this solution.
Thanks for your attention!
Well, you can't really bind to "To" nor From, because the storyboard has to be frozen, in order to work efficiently with cross-threading.
Solution1) Simplest solution without hacks(involves code-behind): Add MouseOver event handler & in the event handler, locate necessary animation, set the "To" property directly, so you won't use binding and the "freezing" can be done. This way you won't hardcode anything :).
Solution2) There is a cool hack that supports XAML only( a little bit of converter magic ofcourse ), but I do not suggest it. It's cool nonetheless :) WPF animation: binding to the "To" attribute of storyboard animation See answer by Jason.
There are few things more that you can try:
Solution3) Don't use Dependency properties, but rather implement INotifyProperthChanged. This way you still can BIND "To". Note that I think this should theoretically work, but I have not tried.
Solution4) Apply Mode=OneTime to your binding. Maybe it works?
Solution5) Write your own attached behavior that will evaluate dependency property on correct thread and set "To" property. I think that will be nice solution.
Here is good duplicate too: WPF Animation "Cannot freeze this Storyboard timeline tree for use across threads"
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With