2013-04-06 12 views
9

Rozważmy następujący kod:Jak uzyskać dostęp do storyboardu w zasobach elementu z 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> 

Powyższy kod działa bez problemu. Teraz chcę powiązać wartości klucza klatek MyStory do DP (o nazwie SpecialColor) niniejszego łatwość sterowania tak:

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

który popełni błąd:

nie może zamarznąć ten Storyboard drzewa harmonogram używaj w wątkach.

Można to zrobić za pomocą kodu z tyłu. Ale jak mogę to zrobić tylko w XAML?


Code-Behind Aided Rozwiązanie:

Krok 1: Umieszczenie MyStory storyboard do zasobów 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> 

Błąd:Nie można znaleźć zasobu o nazwie 'MyStory'. W nazwach zasobów rozróżniana jest wielkość liter.

Krok 2: Wyeliminowanie Trigger na IsMouseOver nieruchomości i rozpocząć MyStory od kod.

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

Krok 3: Rozwiązanie jest już zrobione, ale to nie działa na pierwszy raz. Na szczęście istnieje obejście tego problemu. Wystarczy umieścić ControlTemplate w Style.

(muszę inne Trigger typy niż EventTrigger i należy owinąć UserControl elementy z ControlTemplate.)


Aktualizacja:

Chodzi o użyciu ObjectDataProvider powiodło się.

  1. ObjectDataProvider zasobów nie mogą być wykorzystywane w celu zapewnienia storyboard !!! Raport o błędzie jest:
    • XamlParseException: Ustaw właściwość 'System.Windows.Media.Animation.BeginStoryboard.Storyboard' zwrócił wyjątek.
    • InnerException: "System.Windows.Data.ObjectDataProvider "nie jest prawidłową wartością dla właściwości" Storyboard ".
  2. AssociatedControl DP jest zawsze zerowa.

Oto kod:

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

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

Odpowiedz

3

Co jeśli ten kod było prawdziwe?

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

Jeśli tak, to mogę mieć Trigger na IsMouseOver własności ...

Cieszę się powiedzieć, że jest to kod roboczych :) mogę użyć tylko w <Border.Triggers>EventTrigger tagu. To było ograniczenie. Więc zacząłem myśleć o tym pomyśle: co jeśli mógłbym mieć spust niestandardowy, który może działać w zakresie FrameworkElement.Triggers? Oto kod:

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

Utworzyłem również inne rodzaje wyzwalania (tj InteractiveMultiTrigger, InteractiveDataTrigger, InteractiveMultiDataTrigger), a także kilka działań, które sprawia, że ​​można mieć warunkowego i multi-warunkowe eventtriggers. Opublikuję je wszystkie, jeśli profesjonalni faceci potwierdzą to rozwiązanie.

Dziękuję za uwagę!

+0

To miło, powinno działać bez problemów. Jedyne, co widzę, to to, że może być mniej efektywny, ponieważ nie zamarzasz już osi czasu storyboardu i oznacza to, że nie używa wielu wątków. –

+0

Masz rację. Szukam sposobu wywołania metody "Invoke" z "TriggerAction". W ten sposób wszystkie wymagane rzeczy powinny być wykonywane wewnętrznie. Czy masz jakiś pomysł? (Oznaczałem to jako "Nie mogłem użyć tego fragmentu".) – Mimi

+0

W innym widoku, faktycznie, z dokumentów, Begin() robi zamrożenie automatycznie. Jestem bardziej zainteresowany tym, dlaczego nie działa Xaml? Dlaczego mimo wszystko próbujesz uzyskać dostęp do wewnętrznych treści? To nigdy nie jest dobry pomysł, chyba że w żaden inny sposób –

4

No, może nie do końca wiąże się z "Do" ani From, ponieważ musi być storyboard zamrożone, w celu wydajnej pracy z poprzecznym gwintowaniem.

Porada1) Najprostsze rozwiązanie bez hacków (dotyczy kodu źródłowego): Dodaj MouseOver obsługi zdarzeń & w obsłudze zdarzenia zlokalizuj niezbędne animację, ustaw „” własności bezpośrednio, więc nie użyje wiązania i "zamrożenie" można zrobić. W ten sposób nic nie utkniesz na stałe :).

Solution2) Istnieje fajny hack, który obsługuje tylko XAML (trochę magii konwerterów oczywiście), ale nie sugeruję tego. Mimo to jest fajnie :) WPF animation: binding to the "To" attribute of storyboard animation Zobacz odpowiedź Jason.

Jest kilka rzeczy bardziej, że można spróbować:

Solution3) Nie używać właściwości zależności, ale raczej wdrożenie INotifyProperthChanged. W ten sposób nadal możesz BIND "To". Zauważ, że myślę, że powinno to teoretycznie działać, ale nie próbowałem.

Rozwiązanie4) Tryb Zastosuj = Czas jednorazowy na oprawę. Może to działa?

Solution5) Napisz własne dołączone zachowanie, które oceni własność zależności na poprawnym wątku i ustawi właściwość "To". Myślę, że będzie to miłe rozwiązanie.

Oto dobry duplikat też: WPF Animation "Cannot freeze this Storyboard timeline tree for use across threads"

+0

Dzięki @ Erti-Chris Eelmaa. Pierwsze rozwiązanie nie jest zamierzone. (Podałem pełną wersję powyżej!) Nie zgadzam się również z używaniem właściwości 'Tag' z powodu ograniczenia wydajności. – Mimi

+0

Mam pomysł. ☼ Czy możemy użyć 'ObjectDataProvider' do wywołania statycznej metody, która zwraca dowolny dowolny element elementu? – Mimi

+0

Dodałem więcej rozwiązań teoretycznych. Nie jestem zaznajomiony z ObjectDataProvider. Nie jestem jednak pewien, co chciałbyś osiągnąć dzięki ObjectDataProvider. Wszystko, co widzę, to to, że jest klasą, która pobiera nazwę metody, ocenia ją i ustawia ją na kolekcję, która jest zwracana, aby można było z nią powiązać. –

Powiązane problemy