やる気駆動型エンジニアの備忘録

WPF(XAML+C#)の話題を中心に.NET/Android/CI やたまに趣味に関するブログです

ViewModelのプロパティ変更をトリガーにアニメーションする方法

WPF でViewModel のプロパティが変更されたときにStoryboard を開始する方法で少し躓いたのでメモ。
開発環境は以下の通り。

ViewModel のプロパティの変更をトリガーにアニメーションするのが目的なので、まず思い浮かんだのがこれ↓

<Window 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:local="clr-namespace:BindingSoureChangedAnimation"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
        xmlns:ei="http://schemas.microsoft.com/expression/2010/interactions"
        x:Class="BindingSoureChangedAnimation.MainWindow"
        Title="MainWindow"
        Width="525"
        Height="350"
        d:DataContext="{d:DesignInstance {x:Type local:MainWindowViewModel}, IsDesignTimeCreatable=True}"
        mc:Ignorable="d">
    <Window.Style>
        <!--
            Style のDataTrigger でViewModel のプロパティの値によってストーリーボードを開始する。
        -->
        <Style TargetType="{x:Type Window}">
            <Style.Triggers>
                <DataTrigger Binding="{Binding IsAnimation}"
                             Value="True">
                    <DataTrigger.EnterActions>
                        <BeginStoryboard Storyboard="{DynamicResource ColorChangeStoryboard}"
                                         x:Name="Begin_ColorChangeStoryboard" />
                    </DataTrigger.EnterActions>
                    <DataTrigger.ExitActions>
                        <StopStoryboard BeginStoryboardName="Begin_ColorChangeStoryboard" />
                    </DataTrigger.ExitActions>
                </DataTrigger>
            </Style.Triggers>
        </Style>
    </Window.Style>
    <Window.Resources>
        <!-- 実行したいStoryboard  -->
        <Storyboard x:Key="ColorChangeStoryboard">
            <ColorAnimationUsingKeyFrames Storyboard.TargetProperty="(Shape.Fill).(SolidColorBrush.Color)"
                                          Storyboard.TargetName="rectangle">
                <EasingColorKeyFrame KeyTime="0"
                                     Value="#FF7A7AFF" />
                <EasingColorKeyFrame KeyTime="0:0:1"
                                     Value="#FFFFFFA9" />
            </ColorAnimationUsingKeyFrames>
        </Storyboard>
    </Window.Resources>
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>
        <CheckBox Content="アニメーション"
                  IsChecked="{Binding IsAnimation, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
        </CheckBox>
        <Border Grid.Row="1">
            <Rectangle x:Name="rectangle"
                       Fill="#FF7A7AFF"
                       Stroke="Black"
                       Margin="32" />
        </Border>
    </Grid>
</Window>

Window のStyle.Trigger のDataTrigger を使用するパターン。
これを実行するとこうなります。
f:id:iyemon018:20170609002314p:plain

Window のStyle からでは、Window.Resources 内のリソースにはアクセスできないとのこと…。
同様にWindow.Content のGrid のStyle でも実現はできません。

で、次の案。
Behavior のControlStoryboardAction を使用する。
ただ、このBehavior はEventTrigger になっているのでイベントの着火をトリガーとするため今回の目的には合致しません。(※)

いろいろ調べた結果、以下の方法で実現することができました。

<Window 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:local="clr-namespace:BindingSoureChangedAnimation"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
        xmlns:ei="http://schemas.microsoft.com/expression/2010/interactions"
        x:Class="BindingSoureChangedAnimation.MainWindow"
        Title="MainWindow"
        Width="525"
        Height="350"
        d:DataContext="{d:DesignInstance {x:Type local:MainWindowViewModel}, IsDesignTimeCreatable=True}"
        mc:Ignorable="d">
    <Window.Resources>
        <!-- 実行したいStoryboard  -->
        <Storyboard x:Key="ColorChangeStoryboard"
                    RepeatBehavior="Forever">
            <ColorAnimationUsingKeyFrames Storyboard.TargetProperty="(Shape.Fill).(SolidColorBrush.Color)"
                                          Storyboard.TargetName="rectangle">
                <EasingColorKeyFrame KeyTime="0"
                                     Value="#FF7A7AFF" />
                <EasingColorKeyFrame KeyTime="0:0:1"
                                     Value="#FFFFFFA9" />
            </ColorAnimationUsingKeyFrames>
        </Storyboard>
    </Window.Resources>
    <i:Interaction.Triggers>
        <!--
            IsAnimation プロパティの値が"True" ならStoryboard を開始する。
            "True" 以外の値ならStoryboard を停止する。
        -->
        <ei:DataTrigger Binding="{Binding IsAnimation, Mode=OneWay}"
                        Value="True">
            <ei:ControlStoryboardAction Storyboard="{StaticResource ColorChangeStoryboard}" />
        </ei:DataTrigger>
        <ei:DataTrigger Binding="{Binding IsAnimation, Mode=OneWay}"
                        Value="True"
                        Comparison="NotEqual">
            <ei:ControlStoryboardAction Storyboard="{StaticResource ColorChangeStoryboard}"
                                        ControlStoryboardOption="Stop" />
        </ei:DataTrigger>
    </i:Interaction.Triggers>
    <Window.DataContext>
        <local:MainWindowViewModel />
    </Window.DataContext>
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>
        <CheckBox Content="アニメーション"
                  IsChecked="{Binding IsAnimation, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
        <Border Grid.Row="1">
            <Rectangle x:Name="rectangle"
                       Fill="#FF7A7AFF"
                       Stroke="Black"
                       Margin="32" />
        </Border>
    </Grid>
</Window>

変更点はここ

    <i:Interaction.Triggers>
        <!--
            IsAnimation プロパティの値が"True" ならStoryboard を開始する。
            "True" 以外の値ならStoryboard を停止する。
        -->
        <ei:DataTrigger Binding="{Binding IsAnimation, Mode=OneWay}"
                        Value="True">
            <ei:ControlStoryboardAction Storyboard="{StaticResource ColorChangeStoryboard}" />
        </ei:DataTrigger>
        <ei:DataTrigger Binding="{Binding IsAnimation, Mode=OneWay}"
                        Value="True"
                        Comparison="NotEqual">
            <ei:ControlStoryboardAction Storyboard="{StaticResource ColorChangeStoryboard}"
                                        ControlStoryboardOption="Stop" />
        </ei:DataTrigger>
    </i:Interaction.Triggers>

ControlStoryboardAction 使ってるんですが、そのトリガーに"ei:DataTrigger" を使用しています。
通常の"DataTrigger" とは違って"Microsoft.Expression.Interactivity.Core" 名前空間にいるやつです。
こいつを探し出すのが苦労したorz

通常、Blend からBehavior をドラッグ・アンド・ドロップで配置するとトリガーは、"EventTrigger" になります。
これを"ei:DataTrigger" に変更するには以下の手順を実行します。

1.Behavior を配置する。

2.EventTrigger をDataTrigger に変更する。
  プロパティ ウィンドウの[トリガー] - [新規] をクリックします。
f:id:iyemon018:20170609004623p:plain

  一覧から"DataTrigger" を選択して[OK] をクリックします。
f:id:iyemon018:20170609004816p:plain

3.トリガーの値を設定する。
  プロパティ ウィンドウの[トリガー] "TriggerType" が"DataTrigger" に変わっていることを確認してViewModel のプロパティの値をバインドする。
f:id:iyemon018:20170609004947p:plain

これを探し出すのに一番時間がかかりました。
ちなみにControlStoryboardAction は、Play とStop それぞれのアクションを設定しないとStoryboard を開始、終了することができません。
また、値の比較種別として"Equal", "NotEqual" 以外にも"LessThan (未満)", "GreaterThan (~より大きい)" 等があるので、ViewModel のプロパティがある数値以上の場合にアニメーションを制御する といったこともできそうです。(未確認)


今回使用したソースはこちら↓↓↓
github.com