2013-04-06 1 views
9

Betrachten Sie diesen Code:Zugriff auf ein Storyboard innerhalb einer Elementressourcen von XAML?

<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> 

Der obige Code funktioniert ohne Probleme. Nun, ich will binden Key-Frame-Wert von MyStory an einen DP (benannt SpecialColor) diese Benutzersteuerung wie folgt:

<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> 

, die einen Fehler macht:

Kann nicht diese Storyboard Timeline Baumes einfrieren für Verwenden Sie über Threads.

Es ist möglich, dies mit Code hinter zu tun. Aber wie kann ich es nur in XAML machen?


-Code-Behind Aided Lösung:

Schritt 1: Inbetriebnahme des MyStory Storyboard in die brdBase Ressourcen.

<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> 

Fehler:Kann nicht Ressource finden namens 'MyStory'. Bei Ressourcennamen wird zwischen Groß- und Kleinschreibung unterschieden

Schritt 2: Beseitigung Trigger auf IsMouseOver Eigenschaft und beginnen, die MyStory von Code hinter.

<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); 
} 

Schritt 3: Die Lösung wird bereits getan, aber es beim ersten Mal nicht funktioniert. Glücklicherweise gibt es eine Problemumgehung für dieses Problem. Es genügt, die ControlTemplate in eine Style zu legen.

(Ich brauche andere Trigger Typen als EventTrigger und müssen die UserControl Elemente mit dem ControlTemplate wickeln.)


Update:

Die Idee zur Verwendung von ObjectDataProvider fehlgeschlagen.

  1. Eine Object Ressource kann nicht ein Storyboard verwendet werden, um !!! Der Fehlerbericht ist:
    • XamlParseException: Set Eigenschaft 'System.Windows.Media.Animation.BeginStoryboard.Storyboard' eine Ausnahme ausgelöst hat.
    • InnerException: 'System.Windows.Data.ObjectDataProvider 'ist kein gültiger Wert für die Eigenschaft' Storyboard '.
  2. Die AssociatedControl DP ist immer null. Hier

ist der 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> 

Die StoryboardFinder Klasse:

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

Antwort

3

Was passiert, wenn dieser Code wahr?

<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> 

Wenn ja, könnte ich einen Trigger auf IsMouseOver Eigenschaft haben ...

Ich bin froh, einen funktionierenden Code zu sagen, es ist :) ich nur EventTrigger in <Border.Triggers> Tag nutzen könnten. Es war die Beschränkung. Also fing ich an, über diese Idee nachzudenken: Was wäre, wenn ich einen benutzerdefinierten Trigger hätte, der in FrameworkElement.Triggers Scope arbeiten könnte? Hier ist der 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 
    } 
} 

ich auch andere Triggertypen erstellt haben (das heißt InteractiveMultiTrigger, InteractiveDataTrigger, InteractiveMultiDataTrigger) sowie einige weitere Aktionen, die es möglich macht, eine bedingte und Multi-bedingten Eventtriggers zu haben. Ich werde sie alle veröffentlichen, wenn Sie professionelle Jungs diese Lösung bestätigen.

Vielen Dank für Ihre Aufmerksamkeit!

+0

das ist nett, sollte es ohne Probleme funktionieren. Das einzige, was ich sehen kann, ist, dass es weniger effizient ist, da Sie Ihre Storyboard-Timeline nicht mehr einfrieren und es bedeutet, dass es nicht mehrere Threads verwendet. –

+0

Sie haben Recht. Ich suche nach einer Möglichkeit, die 'Invoke' Methode von' TriggerAction' aufzurufen. Auf diese Weise sollten alle erforderlichen Dinge intern erledigt werden. Haben Sie eine Idee? (Ich habe es als "Konnte dieses Snippet nicht verwenden" markiert.) – Mimi

+0

In einer anderen Ansicht, eigentlich aus der Dokumentation, startet Begin() das Einfrieren automatisch. Mich interessiert mehr, warum Xaml nicht richtig funktioniert? Warum versuchst du sowieso auf interne Sachen zuzugreifen? Es ist nie eine gute Idee, es sei denn, absolut keine andere Möglichkeit –

4

Nun, kann man nicht wirklich binden an "To" noch aus, weil das Drehbuch zu sein hat eingefroren, um effizient mit Cross-Threading zu arbeiten.

Solution1) Simplest Lösung ohne Hacks (betrifft die Code-Behind): Mouseover-Event-Handler & im Ereignishandler hinzufügen, sucht notwendig Animation, die direkt "To" Eigenschaft festgelegt, so dass Sie nicht verbindlich verwenden und das "Einfrieren" kann getan werden. Auf diese Weise werden Sie nichts hart codieren :).

Solution2) Es gibt einen coolen Hack, der nur XAML unterstützt (ein bisschen Converter Magie natürlich), aber ich schlage es nicht vor. Es ist trotzdem cool :) WPF animation: binding to the "To" attribute of storyboard animation Siehe Antwort von Jason.

Es gibt wenige Dinge mehr, die Sie ausprobieren können:

Solution3) Verwenden Abhängigkeitseigenschaften nicht, sondern INotifyProperthChanged implementieren. Auf diese Weise können Sie immer noch an "BIND" binden. Beachten Sie, dass das theoretisch funktionieren sollte, aber ich habe es nicht versucht.

Lösung4) Apply-Modus = OneTime zu Ihrer Bindung. Vielleicht funktioniert es?

Lösung5) Schreiben Sie Ihr eigenes angehängtes Verhalten, das die Abhängigkeitseigenschaft im korrekten Thread bewertet und die Eigenschaft "To" setzt. Ich denke, das wird eine schöne Lösung sein.

ist hier gut doppelt zu: WPF Animation "Cannot freeze this Storyboard timeline tree for use across threads"

+0

Danke @ Erti-Chris Eelmaa. Die erste Lösung ist nicht beabsichtigt. (Ich gab die vollständige Version oben!) Auch ich bin nicht einverstanden mit der Verwendung der Eigenschaft "Tag" wegen seiner Leistungseinschränkung. – Mimi

+0

Ich habe eine Idee. ☼ Können wir 'ObjectDataProvider' verwenden, um eine statische Methode aufzurufen, die eine beliebige Ressource eines Elements zurückgibt? – Mimi

+0

Ich fügte mehr theoretische Lösungen hinzu. Übrigens bin ich nicht mit ObjectDataProvider vertraut. Ich bin mir nicht sicher, was Sie mit ObjectDataProvider erreichen möchten. Alles, was ich sehe, ist, dass es eine Klasse ist, die einen Methodennamen nimmt, sie auswertet und auf eine Sammlung setzt, die zurückgegeben wird, damit Sie dagegen binden können. –