Определить анимацию и триггеры как ресурс многократного использования?

Есть ли способ определить анимацию где-нибудь в xaml (например, как ресурс) один раз, а затем повторно использовать ее несколько раз? У меня есть много независимых кистей через различные datatemplates, которые независимо должны запускать ту же самую анимацию, основанную на datatrigger. Теперь, поскольку кажется, что анимация должна определять Storyboard.TargetName и Storyboard.TargetProperty. Это в значительной степени побеждает цель повторного использования. Я хотел бы как-то объявить "использовать эту анимационную форму для ресурса, но на этот раз применить ее к другому элементу".

Мне кажется, что это довольно простая, важная и важная просьба, и я удивлен тем, что ее не так прямо, чтобы успеть. Я что-то пропустил?

То же самое относится и к триггерам. Предположим, у меня есть много разных визуальных элементов, которые все представляют один и тот же тип состояния, используя цветные анимации. Например. исчезают до зеленого, когда "активный" исчезает до "красного", когда "ошибка" и т.д. Единственное различие между визуальными эффектами - это их форма/визуальное дерево, то желаемое поведение анимации одинаково, все они имеют элемент где-то в своем визуальном дереве, свойство типа color. Я думаю, что нетрудно представить, насколько утомительно это переопределять одни и те же анимации и набор данных снова и снова. Каждый разработчик ненавидит это. Я отчаянно ищут более легкое решение, для которого не требуется (или, по крайней мере, очень мало) кода С#.

До сих пор я пришел к следующему:

Определите анимации в ресурсе lik this this (повторите это для всех основных состояний, которые есть, например, активируются, активны, неактивны, ошибка):

<ColorAnimationUsingKeyFrames x:Key="deactivatingColorAnimation" 
                    Storyboard.TargetProperty="Material.(MaterialGroup.Children)[0].Brush.(SolidColorBrush.Color)"                    
                    FillBehavior="HoldEnd" RepeatBehavior="Forever" AutoReverse="True">
      <ColorAnimationUsingKeyFrames.KeyFrames>
        <LinearColorKeyFrame KeyTime="00:00:00" Value="Gray"/>
        <LinearColorKeyFrame KeyTime="00:00:0.25" Value="Gray"/>
        <LinearColorKeyFrame KeyTime="00:00:0.5" Value="Gray" />
        <LinearColorKeyFrame KeyTime="00:00:0.75" Value="Gray" />
     </ColorAnimationUsingKeyFrames.KeyFrames>
</ColorAnimationUsingKeyFrames>

Использовать его в раскадровке в триггерах (повторите эти дваллионы раз для каждого состояния X, каждый из которых отличается от stateviusal, всегда придумывает новое имя для раскадровки):

<DataTrigger Binding="{Binding SubstrateHolder.State}" Value="Deactivating">
        <DataTrigger.EnterActions>
            <BeginStoryboard x:Name="someStateVisualDeactivatingStoryboard">
                <Storyboard Storyboard.TargetName="someStateVisual">
                    <StaticResource ResourceKey="deactivatingColorAnimation" />
                </Storyboard>
            </BeginStoryboard>
        </DataTrigger.EnterActions>
        <DataTrigger.ExitActions>
            <RemoveStoryboard BeginStoryboardName="someStateVisualDeactivatingStoryboard" />
        </DataTrigger.ExitActions>
</DataTrigger>

Вы можете легко представить себе, сколько раздувать XAML, которое я должен многократно копировать и вставлять для всех этих zillion DataTriggers.

Было бы здорово определить все эти триггеры один раз и применить его к различным визуальным состояниям. Как что-то подобное в WPF? Любой совет?

Ответы

Ответ 1

Кажется, что нет никакого хорошего решения XAML для этой общей идеи. Я закончил писать свои собственные свойства, которые определяют все поведение анимации для данного элемента. что-то вроде этого:

<DataTemplate>
   <!-- ...  -->
   <Rectangle Fill="Gray">
     <v:AnimationHelper.Animations>
        <v:StandardColorStateAnimation TargetColorProperty="(Rectangle.Fill).(SolidColorBrush.Color)" TraggetSateProperty={Binding State} />
     </v:AnimationHelper.Animations>
   </Rectangle>
<DataTemplate>

Остальное (создание анимации и т.д.) выполняется в коде.

Ответ 2

Не могли бы вы попробовать что-то вроде этого?

  • Оберните все текущие шаблоны управления невидимым корневым элементом, например. Border или StackPanel, чей ограничивающий прямоугольник будет охватывать весь элемент управления.
  • Создайте шаблон стиля или управления для этого невидимого блока, который содержит все триггеры и анимации.
  • Анимируйте анимацию произвольного свойства Color в невидимом поле.
  • В визуальных деревьях для всех ваших различных элементов управления свяжите все свойства, которые вы хотите оживить, к свойству "Цвет" в невидимом корневом элементе.

Ответ 3

Я понимаю, что эта проблема немного мертва во время этой публикации, но я нашел решение, для которого требуется очень мало кода.

Вы можете сделать UserControl с настраиваемыми свойствами (прокрутите вниз до примерно 8), который содержит ваш прямоугольник, а также анимацию и состояние триггеры. Этот пользовательский элемент управления будет определять публичное свойство, такое как статус, который приведет к изменению цвета при изменении.

Единственный необходимый код - это создать переменные в коде.

Пользовательский элемент управления может повторно использоваться повторно, не переписывая раскадровки XAML или триггеры данных.

Ответ 4

Самый "способ XAML" для достижения этой цели я могу представить, это создать выделенный MarkupExtension, который вытащил бы анимацию из словаря ресурсов и установил необходимые свойства. Я предполагаю, что они ограничены подмножеством Storyboard.Target, Storyboard.TargetName и Storyboard.TargetProperty. Хотя для этого требуется некоторый код, это одноразовые усилия, более того, MarkupExtension предназначены для использования с XAML. Вот простейшая версия:

[MarkupExtensionReturnType(typeof(Timeline))]
public class AnimationResourceExtension : StaticResourceExtension
{
    //This property is for convienience so that we
    //don't have to remember to set x:Shared="False"
    //on all animation resources, but have an option
    //to avoid redundant cloning if it is
    public bool IsShared { get; set; } = true;

    public DependencyObject Target { get; set; }

    public string TargetName { get; set; }

    public PropertyPath TargetProperty { get; set; }

    public override object ProvideValue(IServiceProvider serviceProvider)
    {
        if (base.ProvideValue(serviceProvider) is Timeline animation)
        {
            //If the animation is shared we shall clone it
            //Checking if it is frozen is optional and we can
            //either clone it or throw an exception
            //(or simply proceed knowing one will be thrown anyway)
            if (IsShared || animation.IsFrozen)
                animation = animation.Clone();
            Storyboard.SetTarget(animation, Target);
            Storyboard.SetTargetName(animation, TargetName);
            Storyboard.SetTargetProperty(animation, TargetProperty);
            return animation;
        }
        else
            throw new XamlException("The referenced resource is not an animation");
    }
}

Использование очень просто:

<FrameworkElement.Resources>
    <DoubleAnimation x:Key="MyAnimation" From="0" To="1" Duration="0:0:1" />
</FrameworkElement.Resources>
(...)
<Storyboard>
    <utils:AnimationResource ResourceKey="MyAnimation" TargetName="SomeElement" TargetProperty="Opacity" />
</Storyboard>

Быть таким простым, каким может быть это решение, имеет свои ограничения - он не поддерживает ни расширения Binding, ни DynamicResource для вышеупомянутых свойств. Это, однако, достижимо, но требует дополнительных усилий. Поддержка Binding должна быть довольно простой - вопрос правильного использования XamlSetMarkupExtensionAttribute  (плюс некоторый шаблонный код). Поддержка DynamicResource была бы немного сложнее, и в дополнение к использованию XamlSetMarkupExtensionAttribute потребовалось бы обернуть IServiceProvider для возврата адекватного IProvideValueTarget, но все еще возможно.