Простые поведения в Silverlight

Анимации и различного рода трансформации – это основа интерактивности любого современного пользовательского интерфейса. В WPF и Silverlight для этих целей есть набор объектов для анимацией и трансформаций. Для инициации того или иного действия используются события или триггеры. В конечном счете создание множества триггеров существенно снижает читаемость кода, а также его повторное использование. VisualStateManager позволяет отчасти решить эту проблему. Однако, есть еще более элегантный способ – использование поведений (Behavior).

Сразу оговорюсь, что здесь я буду говорить о Silverlight в целом. Аналогичные механизмы доступны для веб-приложений на базе Silverlight, для настольных приложений на базе Silverlight и для мобильных приложений на базе Silverlight. Кроме того, аналогичный подход можно использовать и в WPF-приложениях. Поэтому я не буду конкретизировать платформу, ну а где конкретно применять эти механизмы – решать вам.

Итак, что же такое поведения? Поведения (behaviors) – это специальные объекты, которые позволяют добавлять функционал в приложения, не написав ни одной строчки кода (изменив только разметку). Каждое поведение реализует небольшой функционал, например анимацию "дрожания" или "размытия".

Простые поведения являются объектами, унаследованными от класса Behavior<T>. При реализации своего поведения есть возможность обработать момент подключения и отключения поведения. Каждый элемент Behavior может быть подключен к любому визуальному элементу. Давайте попробуем создать простое поведение и подключить его.

Простое поведение

Для начала подключим сборку System.Windows.Interactivity. После этого нужно создать класс-наследник Behavior<T>, где параметр T – это тип элемента управления, к которому можно применить данное поведение. В качестве этого параметра можно указать объект UIElement — тогда это поведение можно будет использовать с любыми элементами управления. Класс Behavior<T> содержит два виртуальных метода – OnAttached и OnDetaching. Эти методы вызываются при подключении или отключении поведения. Чтобы обработать эти события можно переопределить эти методы и определить в них свою логику.

public class SimpleBehavior1 : Behavior<UIElement>
{
  protected override void OnAttached()
  {
    base.OnAttached();
  }

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

Для того, чтобы работать с текущим элементом управления (к которому это поведение присоединено) можно использовать свойство AssociatedObject. Логика поведения обычно определяется в событиях, на которые можно подписаться в момент подключения поведения. Давайте создадим эффект размытия для текущего элемента в момент наведения указателя мыши. Для этого будем инициализировать свойство Effect текущего элемента управления объектом BlurEffect в момент наведения и очищать его, когда указатель находится вне этого элемента управления. Получим несложное определение поведения.

public class SimpleBehavior1 : Behavior<UIElement>
{
  protected override void OnAttached()
  {
    base.OnAttached();
    AssociatedObject.MouseEnter += AssociatedObject_MouseEnter;
    AssociatedObject.MouseLeave += AssociatedObject_MouseLeave;
  }

  protected override void OnDetaching()
  {
    AssociatedObject.MouseEnter -= AssociatedObject_MouseEnter;
    AssociatedObject.MouseLeave -= AssociatedObject_MouseLeave;
    base.OnDetaching();
  }

  void AssociatedObject_MouseEnter(object sender, MouseEventArgs e)
  {
    AssociatedObject.Effect = new BlurEffect() { Radius = 5 };
  }

  void AssociatedObject_MouseLeave(object sender, MouseEventArgs e)
  {
    AssociatedObject.Effect = null;
  }
}

Для того, чтобы применить это поведение, в разметке следует подключить пространство имен System.Windows.Interactivity и пространство имен с определением поведения.

xmlns:b="clr-namespace:Behavior1"
xmlns:interactivity="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"

Добавим теперь кнопку на пустую форму и применим к нему это поведение. Получим следующую разметку.

<UserControl x:Class="Behavior1.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:b="clr-namespace:Behavior1"
    xmlns:interactivity="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
    mc:Ignorable="d"
    d:DesignHeight="300" d:DesignWidth="400">

  <Grid x:Name="LayoutRoot" Background="White">
    <Button Margin="50" Content="Счастье" Width="200" Height="150">
      <interactivity:Interaction.Behaviors>
        <b:SimpleBehavior1 />
      </interactivity:Interaction.Behaviors>
    </Button>
  </Grid>
</UserControl>

Запустим приложение и увидим, что при наведении указателя мыши на кнопку она становится размытой.

Анимация

Приведенный выше пример очень простой. Давайте усложним пример и сделаем размытие анимированным. Для этого я создам два объекта Storyboard (для появления и исчезновения размытия) и буду запускать эти анимации при возникновении тех же событий.

public class SimpleBehavior2 : Behavior<UIElement>
{
  private readonly BlurEffect _effect = new BlurEffect() {Radius = 0};

  private readonly Storyboard _showStoryboard = new Storyboard();
  private readonly Storyboard _hideStoryboard = new Storyboard();

  protected override void OnAttached()
  {
    base.OnAttached();
    AssociatedObject.MouseEnter += AssociatedObject_MouseEnter;
    AssociatedObject.MouseLeave += AssociatedObject_MouseLeave;
    AssociatedObject.Effect = _effect;
    

    var showAnimation = new DoubleAnimation
    {
        To = 100
    };
    
    Storyboard.SetTarget(showAnimation, _effect);
    Storyboard.SetTargetProperty(showAnimation, new PropertyPath(BlurEffect.RadiusProperty));
    showAnimation.Duration = new Duration(TimeSpan.FromMilliseconds(900));
    _showStoryboard.Children.Add(showAnimation);


    var hideAnimation = new DoubleAnimation
    {
        To = 0,
    };
    Storyboard.SetTarget(hideAnimation, _effect);
    Storyboard.SetTargetProperty(hideAnimation, new PropertyPath(BlurEffect.RadiusProperty));
    hideAnimation.Duration = new Duration(TimeSpan.FromMilliseconds(200));
    
    _hideStoryboard.Children.Add(hideAnimation);
  }

  protected override void OnDetaching()
  {
    AssociatedObject.MouseEnter -= AssociatedObject_MouseEnter;
    AssociatedObject.MouseLeave -= AssociatedObject_MouseLeave;
    AssociatedObject.Effect = null;
    base.OnDetaching();
  }


  void AssociatedObject_MouseEnter(object sender, MouseEventArgs e)
  {
    _showStoryboard.Begin();
  }

  void AssociatedObject_MouseLeave(object sender, MouseEventArgs e)
  {
    _hideStoryboard.Begin();
  }
}

Аналогичным образом определим данное поведение в разметке и запустим пример. Теперь размытие появляется плавно. Я не буду делать здесь снимки экрана, поскольку анимацию будет показать очень сложно – запустите пример, чтобы увидеть самим.

Трансформации

Давайте еще немного усложним пример и попробуем добавить трансформации к объекту. Как известно, для того, чтобы трансформировать объект нужно использовать свойство RenderTransform. Если используется несколько трансформаций, то это свойство необходимо инициализировать объектом TransformGroup, в который поместить все нужные трансформации.

Поскольку мы незнаем заранее применяются ли уже к объекту трансформации мы должны рассмотреть все варианты и написать несложный код по добавлению своей трансформации к объекту в момент присоединения поведения.

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

Остальное – аналогично тому, как мы это делали в предыдущих примерах – определяем анимации и подписываемся на события. Итого получим несложное поведение, реагирующее также на наведения указателя мыши.

public class SimpleBehavior3 : Behavior<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;
    }

    AssociatedObject.MouseEnter += AssociatedObject_MouseEnter;



    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()
  {
    AssociatedObject.MouseEnter -= AssociatedObject_MouseEnter;
    base.OnDetaching();
  }

  void AssociatedObject_MouseEnter(object sender, MouseEventArgs e)
  {
    _showStoryboard.Begin();
  }
}

Запустим приложение и увидим анимированный объект при наведении указателя мыши.

Что интересно, мы можем применить одновременно несколько поведений к одному объекту. Например, можем применить размытие и трансформацию одновременно. Для этого используем следующую разметку.

<Button Margin="50" Content="Счастье" Width="200" Height="150">
  <interactivity:Interaction.Behaviors>
    <b:SimpleBehavior2 />
    <b:SimpleBehavior3 />
  </interactivity:Interaction.Behaviors>
</Button>