如何从XAML访问元素资源中的故事板?

Mim*_*imi 9 c# wpf xaml binding storyboard

考虑以下代码:

<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>
Run Code Online (Sandbox Code Playgroud)

上面的代码没有问题.现在,我想将关键帧值绑定到此用户控件MyStory的DP(命名SpecialColor),如下所示:

<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>
Run Code Online (Sandbox Code Playgroud)

这会出错:

无法冻结此Storyboard时间轴树以跨线程使用.

使用后面的代码可以做到这一点.但是我怎么才能在XAML中做到这一点?


代码隐藏辅助解决方案:

步骤1:MyStory故事板放入brdBase资源中.

<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>
Run Code Online (Sandbox Code Playgroud)

错误: 找不到名为"MyStory"的资源.资源名称区分大小写.

第2步:消除TriggerIsMouseOver财产,并开始MyStory从后面的代码.

<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>
Run Code Online (Sandbox Code Playgroud)

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);
}
Run Code Online (Sandbox Code Playgroud)

步骤3:解决方案已经完成,但第一次不起作用.幸运的是,这个问题有一个解决方法.这足以让它成为ControlTemplate一个Style.

(我需要其他Trigger类型EventTrigger而且必须UserControlControlTemplate.包装元素.)


更新:

使用ObjectDataProvider失败的想法.

  1. 一个ObjectDataProvider的资源不能被用于提供故事板!错误报告是:
    • XamlParseException:设置属性'System.Windows.Media.Animation.BeginStoryboard.Storyboard'抛出异常.
    • InnerException: 'System.Windows.Data.ObjectDataProvider'不是属性'Storyboard'的有效值.
  2. AssociatedControl DP总是空.

这是代码:

<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>
Run Code Online (Sandbox Code Playgroud)

StoryboardFinder类:

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();
    }
}
Run Code Online (Sandbox Code Playgroud)

Mim*_*imi 4

如果这段代码是真的怎么办?

\n\n
<UserControl x:Class="MyApp.MyControl"\n             ...\n             xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"\n             xmlns:l="clr-namespace:MyApp"\n             DataContext="{Binding RelativeSource={RelativeSource Mode=Self}}">\n\n    <UserControl.Resources>\n        <Style TargetType="{x:Type l:MyControl}">\n            <Setter Property="Template">\n                <Setter.Value>\n                    <ControlTemplate TargetType="{x:Type l:MyControl}">\n                        <Border x:Name="brdBase" BorderThickness="1" BorderBrush="Cyan" Background="Black">\n                            <Border.Resources>\n                                <Storyboard x:Key="MyStory">\n                                    <ColorAnimationUsingKeyFrames Storyboard.TargetProperty="(Border.BorderBrush).(SolidColorBrush.Color)" Storyboard.TargetName="brdBase">\n                                        <SplineColorKeyFrame KeyTime="0:0:1" Value="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type l:MyControl}}, Path=SpecialColor}"/>\n                                    </ColorAnimationUsingKeyFrames>\n                                </Storyboard>\n                            </Border.Resources>\n\n                            <i:Interaction.Triggers>\n                                <l:InteractiveTrigger Property="IsMouseOver" Value="True">\n                                    <l:InteractiveTrigger.CommonActions>\n                                        <BeginStoryboard Storyboard="{StaticResource MyStory}"/>\n                                    </l:InteractiveTrigger.CommonActions>\n                                </l:InteractiveTrigger>\n                            </i:Interaction.Triggers>\n                        </Border>\n                    </ControlTemplate>\n                </Setter.Value>\n            </Setter>\n        </Style>\n    </UserControl.Resources>\n</UserControl>\n
Run Code Online (Sandbox Code Playgroud)\n\n

如果是这样,我可以在IsMouseOver财产上有一个触发器......

\n\n

我很高兴地说这是一个工作代码:)我只能EventTrigger<Border.Triggers>标签中使用。这是限制。所以我开始思考这个想法:如果我可以有一个可以在FrameworkElement.Triggers范围内工作的自定义触发器怎么办?这是代码:

\n\n
using System;\nusing System.Collections.Generic;\nusing System.ComponentModel;\nusing System.Windows;\nusing System.Windows.Interactivity;\nusing System.Windows.Media.Animation;\n\nnamespace TriggerTest\n{\n    /// <summary>\n    /// InteractiveTrigger is a trigger that can be used as the System.Windows.Trigger but in the System.Windows.Interactivity.\n    /// <para>\n    /// Note: There is neither `EnterActions` nor `ExitActions` in this class. The `CommonActions` can be used instead of `EnterActions`.\n    /// Also, the `Actions` property which is of type System.Windows.Interactivity.TriggerAction can be used.\n    /// </para>\n    /// <para>\xc2\xa0</para>\n    /// <para>\n    /// 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:\n    /// <para>1- InteractiveTrigger : Trigger</para>\n    /// <para>2- InteractiveMultiTrigger : MultiTrigger</para>\n    /// <para>3- InteractiveDataTrigger : DataTrigger</para>\n    /// <para>4- InteractiveMultiDataTrigger : MultiDataTrigger</para>\n    /// </para>\n    /// </summary>\n    public class InteractiveTrigger : TriggerBase<FrameworkElement>\n    {\n        #region ___________________________________________________________________________________  Properties\n\n        #region ________________________________________  Value\n\n        /// <summary>\n        /// [Wrapper property for ValueProperty]\n        /// <para>\n        /// Gets or sets the value to be compared with the property value of the element. The comparison is a reference equality check.\n        /// </para>\n        /// </summary>\n        public object Value\n        {\n            get { return (object)GetValue(ValueProperty); }\n            set { SetValue(ValueProperty, value); }\n        }\n\n        public static readonly DependencyProperty ValueProperty =\n            DependencyProperty.Register("Value",\n                                        typeof(object),\n                                        typeof(InteractiveTrigger),\n                                        new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.None, OnValuePropertyChanged));\n\n        private static void OnValuePropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)\n        {\n            InteractiveTrigger instance = sender as InteractiveTrigger;\n\n            if (instance != null)\n            {\n                if (instance.CanFire)\n                    instance.Fire();\n            }\n        }\n\n        #endregion\n\n\n        /// <summary>\n        /// Gets or sets the name of the object with the property that causes the associated setters to be applied.\n        /// </summary>\n        public string SourceName\n        {\n            get;\n            set;\n        }\n\n        /// <summary>\n        /// Gets or sets the property that returns the value that is compared with this trigger.Value property. The comparison is a reference equality check.\n        /// </summary>\n        public DependencyProperty Property\n        {\n            get;\n            set;\n        }\n\n        /// <summary>\n        /// Gets or sets a collection of System.Windows.Setter objects, which describe the property values to apply when the trigger object becomes active.\n        /// </summary>\n        public List<Setter> Setters\n        {\n            get;\n            set;\n        }\n\n        /// <summary>\n        /// Gets or sets the collection of System.Windows.TriggerAction objects to apply when this trigger object becomes active.\n        /// </summary>\n        public List<System.Windows.TriggerAction> CommonActions\n        {\n            get;\n            set;\n        }\n\n        /// <summary>\n        /// Gets a value indicating whether this trigger can be active to apply setters and actions.\n        /// </summary>\n        private bool CanFire\n        {\n            get\n            {\n                if (this.AssociatedObject == null)\n                {\n                    return false;\n                }\n                else\n                {\n                    object associatedValue;\n\n                    if (string.IsNullOrEmpty(SourceName))\n                        associatedValue = this.AssociatedObject.GetValue(Property);\n                    else\n                        associatedValue = (this.AssociatedObject.FindName(SourceName) as DependencyObject).GetValue(Property);\n\n                    TypeConverter typeConverter = TypeDescriptor.GetConverter(Property.PropertyType);\n                    object realValue = typeConverter.ConvertFromString(Value.ToString());\n\n                    return associatedValue.Equals(realValue);\n                }\n            }\n        }\n\n        #endregion\n\n\n        #region ___________________________________________________________________________________  Methods\n\n        /// <summary>\n        /// Fires (activates) current trigger by setting setter values and invoking all actions.\n        /// </summary>\n        private void Fire()\n        {\n            //\n            // Setting setters values to their associated properties..\n            //\n            foreach (Setter setter in Setters)\n            {\n                if (string.IsNullOrEmpty(setter.TargetName))\n                    this.AssociatedObject.SetValue(setter.Property, setter.Value);\n                else\n                    (this.AssociatedObject.FindName(setter.TargetName) as DependencyObject).SetValue(setter.Property, setter.Value);\n            }\n\n            //\n            // Firing actions.. \n            //\n            foreach (System.Windows.TriggerAction action in CommonActions)\n            {\n                Type actionType = action.GetType();\n\n                if (actionType == typeof(BeginStoryboard))\n                {\n                    (action as BeginStoryboard).Storyboard.Begin();\n                }\n                else\n                    throw new NotImplementedException();\n            }\n\n            this.InvokeActions(null);\n        }\n\n        #endregion\n\n\n        #region ___________________________________________________________________________________  Events\n\n        public InteractiveTrigger()\n        {\n            Setters = new List<Setter>();\n            CommonActions = new List<System.Windows.TriggerAction>();\n        }\n\n        protected override void OnAttached()\n        {\n            base.OnAttached();\n\n            if (Property != null)\n            {\n                object propertyAssociatedObject;\n\n                if (string.IsNullOrEmpty(SourceName))\n                    propertyAssociatedObject = this.AssociatedObject;\n                else\n                    propertyAssociatedObject = this.AssociatedObject.FindName(SourceName);\n\n                //\n                // Adding a property changed listener to the property associated-object..\n                //\n                DependencyPropertyDescriptor dpDescriptor = DependencyPropertyDescriptor.FromProperty(Property, propertyAssociatedObject.GetType());\n                dpDescriptor.AddValueChanged(propertyAssociatedObject, PropertyListener_ValueChanged);\n            }\n        }\n\n        protected override void OnDetaching()\n        {\n            base.OnDetaching();\n\n            if (Property != null)\n            {\n                object propertyAssociatedObject;\n\n                if (string.IsNullOrEmpty(SourceName))\n                    propertyAssociatedObject = this.AssociatedObject;\n                else\n                    propertyAssociatedObject = this.AssociatedObject.FindName(SourceName);\n\n                //\n                // Removing previously added property changed listener from the associated-object..\n                //\n                DependencyPropertyDescriptor dpDescriptor = DependencyPropertyDescriptor.FromProperty(Property, propertyAssociatedObject.GetType());\n                dpDescriptor.RemoveValueChanged(propertyAssociatedObject, PropertyListener_ValueChanged);\n            }\n        }\n\n        private void PropertyListener_ValueChanged(object sender, EventArgs e)\n        {\n            if (CanFire)\n                Fire();\n        }\n\n        #endregion\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

我还创建了其他触发器类型(即InteractiveMultiTrigger,,InteractiveDataTriggerInteractiveMultiDataTrigger)以及更多操作,这使得有条件和多条件 EventTriggers 成为可能。如果你们专业人士确认了这个解决方案,我将全部发布。

\n\n

感谢您的关注!

\n