Поведения в Silverlight, обрабатываемые при срабатывании триггера

Недавно я рассказывал об использовании поведений в Silverlight. Мы рассматривали возможности объекта Behavior<T>, позволяющего определить визуальное поведение объекта. Вся логика этого объекта была полностью определена внутри реализации поведения. При этом, если нужно было поменять условие срабатывания того или иного действия, то необходимо было изменить код поведения, что несомненно является большим недостатком такого вида поведений. В этой статье мы рассмотрим другой тип поведений, которые позволяют избежать этого.

Основной проблемой простых поведений, реализованных на базе Behavior<T> является то, что внутри реализации мы жестко задаем набор событий, с которыми будем работать. Это может быть не всегда удобным. Например, если у нас есть поведение для размытия объекта, то было бы удобно в некоторых ситуациях применять поведение при наведении указателя мыши, а в некоторых – при нажатии на кнопку.

Для того, чтобы реализовать такое поведение можно воспользоваться другим типом поведений, реагирующих на заданные события. Такие поведения создаются на базе объекта TriggerAction<T> и позволяют реализовать собственное поведение.

Для создания собственного поведения создадим класс-наследник TriggerAction<T>. В отличие от простых поведений, поведения TriggerAction<T> имеют дополнительный метод Invoke, который срабатывает при наступлении заданного события. Кроме того, в поведениях TriggerAction<T> есть также два перегружаемых метода OnAttached и OnDetaching, позволяющих выполнить предварительные и заключительные действия. Давайте сравним порядок действий, выполняемых для простых поведений и для поведений на основе TriggerAction<T>.

Простые поведения, реализованные на основе объекта Behavior<T>:

  1. Создаем объект-наследник от Behavior<T>;
  2. В методе OnAttached задаем необходимые базовые трансформации, если они необходимы;
  3. Определяем нужные анимации, если они необходимы;
  4. В методе OnAttached подписываемся на нужные события, которые будут обрабатываться поведением;
  5. При наступлении событий запускаем подготовленную ранее анимацию;
  6. Подключаем поведение к элементу управления.

Поведения, реализованные на основе объекта TriggerAction<T>:

  1. Создаем объект-наследник от TriggerAction<T>;
  2. В методе OnAttached задаем необходимые базовые трансформации, если они необходимы;
  3. Определяем нужные анимации, если они необходимы;
  4. Запускаем нужные анимации при вызове метода Invoke;
  5. Подключаем поведение к элементу управления, указав при этом какое именно событие нужно обрабатывать.

Как видно, разница заключается в том, что простые поведения сами подписываются на нужные события в момент выполнения метода OnAttached, а поведения на базе TriggerAction<T> позволяют выбрать события в момент подключения поведения к элементу управления. В этом и состоит разница между этими двумя поведениями.

Создание поведения на основе TriggerAction<T>

Для начала необходимо создать объект-наследник TriggerAction<T>. В качестве generic-параметра будем использовать объект UIElement – это позволит применить поведение к любому элементу управления. С другой стороны, если необходимы какие-то специфичные свойства конкретных элементов управления, то можно использовать более конкретный тип, например, Button, TextBox и т.д.

При создании поведения нужно реализовать единственный метод Invoke.

using System;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Media.Effects;
using System.Windows.Shapes;
using System.Windows.Interactivity;

namespace SilverlightApplication30
{
  public class EventSampleBehavior : TriggerAction<UIElement>
  {
    protected override void Invoke(object parameter)
    {

    }
  }
}

Если необходимо отследить момент подключения/отключения поведения от элемента управления, то можно перегрузить виртуальные методы OnAttaching и OnDeattaching.

using System;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Media.Effects;
using System.Windows.Shapes;
using System.Windows.Interactivity;

namespace SilverlightApplication30
{
  public class EventSampleBehavior : TriggerAction<UIElement>
  {
    protected override void OnAttached()
    {
      base.OnAttached();
    }

    protected override void OnDetaching()
    {
      base.OnDetaching();
    }

    protected override void Invoke(object parameter)
    {
    }
  }
}

Давайте определим логику, которая при срабатывании поведения будет трансформировать объект в виде небольшой анимации. Для этого добавим объекты трансформации и анимации в состав класса поведения. Также нам необходимо задать логику для определения трансформаций – если трансформации уже есть, то объединить их в группу, а если нет, то просто создать новую. Наконец, нам нужно задать поведение анимации. В данном случае воспользуемся анимацией DoubleAnimationUsingKeyFrames. Все эти действия следует выполнить в момент подключения поведения.

using System;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Media.Effects;
using System.Windows.Shapes;
using System.Windows.Interactivity;

namespace SilverlightApplication30
{
  public class EventSampleBehavior : TriggerAction<UIElement>
  {
    private readonly SkewTransform _transofrmation = new SkewTransform();
    private readonly Storyboard _showStoryboard = new Storyboard();

    protected override void OnAttached()
    {
      base.OnAttached();

      if (AssociatedObject.RenderTransform == null)
      {
          AssociatedObject.RenderTransform = _transofrmation;
      }
      else if (AssociatedObject.RenderTransform is TransformGroup)
      {
          ((TransformGroup)AssociatedObject.RenderTransform).Children.Add(_transofrmation);
      }
      else
      {
          var transformGroup = new TransformGroup();
          var sourceTransform = AssociatedObject.RenderTransform;
          transformGroup.Children.Add(sourceTransform);
          transformGroup.Children.Add(_transofrmation);

          AssociatedObject.RenderTransform = transformGroup;
      }


      var animation = new DoubleAnimationUsingKeyFrames();
      animation.KeyFrames.Add(new SplineDoubleKeyFrame { KeyTime = KeyTime.FromTimeSpan(TimeSpan.Zero), Value = 0 });
      animation.KeyFrames.Add(new SplineDoubleKeyFrame { KeyTime = KeyTime.FromTimeSpan(TimeSpan.FromMilliseconds(100)), Value = 20 });
      animation.KeyFrames.Add(new SplineDoubleKeyFrame { KeyTime = KeyTime.FromTimeSpan(TimeSpan.FromMilliseconds(300)), Value = -20 });
      animation.KeyFrames.Add(new SplineDoubleKeyFrame { KeyTime = KeyTime.FromTimeSpan(TimeSpan.FromMilliseconds(500)), Value = 10 });
      animation.KeyFrames.Add(new SplineDoubleKeyFrame { KeyTime = KeyTime.FromTimeSpan(TimeSpan.FromMilliseconds(700)), Value = -10 });
      animation.KeyFrames.Add(new SplineDoubleKeyFrame { KeyTime = KeyTime.FromTimeSpan(TimeSpan.FromMilliseconds(900)), Value = 0 });

      Storyboard.SetTarget(animation, _transofrmation);
      Storyboard.SetTargetProperty(animation, new PropertyPath(SkewTransform.AngleXProperty));
      _showStoryboard.Children.Add(animation);
    }

    protected override void OnDetaching()
    {
        base.OnDetaching();
    }

    protected override void Invoke(object parameter)
    {
    }
  }
}

Все, что теперь осталось сделать, это запустить анимацию в момент вызова метода Invoke. В итоге получим следующий финальный код поведения.

using System;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Media.Effects;
using System.Windows.Shapes;
using System.Windows.Interactivity;

namespace SilverlightApplication30
{
  public class EventSampleBehavior : TriggerAction<UIElement>
  {
    private readonly SkewTransform _transofrmation = new SkewTransform();
    private readonly Storyboard _showStoryboard = new Storyboard();

    protected override void OnAttached()
    {
      base.OnAttached();

      if (AssociatedObject.RenderTransform == null)
      {
        AssociatedObject.RenderTransform = _transofrmation;
      }
      else if (AssociatedObject.RenderTransform is TransformGroup)
      {
        ((TransformGroup)AssociatedObject.RenderTransform).Children.Add(_transofrmation);
      }
      else
      {
        var transformGroup = new TransformGroup();
        var sourceTransform = AssociatedObject.RenderTransform;
        transformGroup.Children.Add(sourceTransform);
        transformGroup.Children.Add(_transofrmation);

        AssociatedObject.RenderTransform = transformGroup;
      }


      var animation = new DoubleAnimationUsingKeyFrames();
      animation.KeyFrames.Add(new SplineDoubleKeyFrame { KeyTime = KeyTime.FromTimeSpan(TimeSpan.Zero), Value = 0 });
      animation.KeyFrames.Add(new SplineDoubleKeyFrame { KeyTime = KeyTime.FromTimeSpan(TimeSpan.FromMilliseconds(100)), Value = 20 });
      animation.KeyFrames.Add(new SplineDoubleKeyFrame { KeyTime = KeyTime.FromTimeSpan(TimeSpan.FromMilliseconds(300)), Value = -20 });
      animation.KeyFrames.Add(new SplineDoubleKeyFrame { KeyTime = KeyTime.FromTimeSpan(TimeSpan.FromMilliseconds(500)), Value = 10 });
      animation.KeyFrames.Add(new SplineDoubleKeyFrame { KeyTime = KeyTime.FromTimeSpan(TimeSpan.FromMilliseconds(700)), Value = -10 });
      animation.KeyFrames.Add(new SplineDoubleKeyFrame { KeyTime = KeyTime.FromTimeSpan(TimeSpan.FromMilliseconds(900)), Value = 0 });

      Storyboard.SetTarget(animation, _transofrmation);
      Storyboard.SetTargetProperty(animation, new PropertyPath(SkewTransform.AngleXProperty));
      _showStoryboard.Children.Add(animation);
    }

    protected override void OnDetaching()
    {
      base.OnDetaching();
    }

    protected override void Invoke(object parameter)
    {
      _showStoryboard.Begin();
    }
  }
}

Теперь, после создания поведения его необходимо подключить к элементу управления. Для этого необходимо подключить нужное пространство имен.

Добавим на форму кнопку и зададим для нее поведение.

<UserControl x:Class="SilverlightApplication30.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
    xmlns:local="clr-namespace:SilverlightApplication30"
    mc:Ignorable="d"
    d:DesignHeight="300" d:DesignWidth="400">

  <Grid x:Name="LayoutRoot" Background="White">
    <Button Content="Button" Width="400" Height="300" HorizontalAlignment="Center" VerticalAlignment="Center">
      <i:Interaction.Triggers>
        <i:EventTrigger EventName="MouseMove">
          <local:EventSampleBehavior />
        </i:EventTrigger>
      </i:Interaction.Triggers>
    </Button>
  </Grid>
</UserControl>

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

<UserControl x:Class="SilverlightApplication30.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
    xmlns:local="clr-namespace:SilverlightApplication30"
    mc:Ignorable="d"
    d:DesignHeight="300" d:DesignWidth="400">

  <Grid x:Name="LayoutRoot" Background="White">
    <Button Content="Button" Width="400" Height="300" HorizontalAlignment="Center" VerticalAlignment="Center">
      <i:Interaction.Triggers>
        <i:EventTrigger EventName="Click">
          <local:EventSampleBehavior />
        </i:EventTrigger>
      </i:Interaction.Triggers>
    </Button>
  </Grid>
</UserControl>

Этим поведения на базе TriggerAction и являются более универсальными, чем простые поведения.