A certain engineer "COMPLEX"

WPF メモ その3 VisualStateManagerとBehaviorの位置関係に注意

What does it mean?


MVVMの場合、コードビハインドにロジックを書くことを厭い、Behaviorを使って、UIとロジックの分離を試みることも多いでしょう。
Behaviorの場合、System.Windows.Interactivity.Behavior を継承し、型パラメータは System.Windows.DependencyObject の派生型である System.Windows.FrameworkElement を指定することがほとんどです。少なくとも私は。
そのため、任意のプロパティのコールバックイベントで、引数に渡ってくる DependencyObject の AssociatedObject プロパティから、コールバックの呼び出し元を取り出します。
すなわち、Behaviorを追加した箇所 の FrameworkElement です。

その後、System.Windows.VisualStateManager.GoToElementState メソッドで状態名を指定してUIの状態を変化させます。
ここまでは普通です。

GoToElementState does Not work!!!


ところが、GoToElementStateがfalseを返すことがあります。
例外を返してくれるか、エラーコードが返るなら良いのですが、bool値を返されても原因がわかりません。

そもそもMSDNのGoToElementStateの説明でも

true コントロールが正常に新しい状態に遷移した場合それ以外の場合、 falseです。

という有様。不親切すぎる。

VisualStateはありますか?


ここで原因ですが、GoToElementStateの第一引数は、FrameworkElementです。つまり、状態を遷移させる対象となる要素になります。
失敗していたのは、VisualStateを定義していない位置にBehaviorを定義していたからです。

以下サンプルソースです。

Sample source code for Demonstration, Experiment and Test


<Window x:Class="WPF3.Views.MainView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:behaviors="clr-namespace:WPF3.Behaviors"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
Title="MainView"
Width="525"
Height="350"
DataContext="{Binding Source={StaticResource Locator},
Path=Main}"
mc:Ignorable="d">
<i:Interaction.Behaviors>
<behaviors:BooleanVisualStateBehavior x:Name="State1Change"
FalseState="False1"
State="{Binding State1}"
TrueState="True1" />
</i:Interaction.Behaviors>
<Grid>

<i:Interaction.Behaviors>
<behaviors:BooleanVisualStateBehavior x:Name="State2Change"
FalseState="False2"
State="{Binding State2}"
TrueState="True2" />
</i:Interaction.Behaviors>

<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>

<Rectangle x:Name="_Rectangle"
Margin="10"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Fill="Black" />

<StackPanel Grid.Column="1" Margin="10">
<CheckBox Margin="0 5 0 0" Content="Switch State1" IsChecked="{Binding State1}" />
<CheckBox Margin="0 5 0 0" Content="Switch State2 (Fail)" IsChecked="{Binding State2}" />
</StackPanel>
</Grid>

<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="State1">
<VisualState x:Name="True1">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="_Rectangle" Storyboard.TargetProperty="Fill">
<DiscreteObjectKeyFrame KeyTime="00:00:00">
<DiscreteObjectKeyFrame.Value>
<SolidColorBrush>Red</SolidColorBrush>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="False1">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="_Rectangle" Storyboard.TargetProperty="Fill">
<DiscreteObjectKeyFrame KeyTime="00:00:00">
<DiscreteObjectKeyFrame.Value>
<SolidColorBrush>Blue</SolidColorBrush>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
</VisualStateGroup>
<VisualStateGroup x:Name="State2">
<VisualState x:Name="True2">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="_Rectangle" Storyboard.TargetProperty="Fill">
<DiscreteObjectKeyFrame KeyTime="00:00:00">
<DiscreteObjectKeyFrame.Value>
<SolidColorBrush>DarkRed</SolidColorBrush>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="False2">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="_Rectangle" Storyboard.TargetProperty="Fill">
<DiscreteObjectKeyFrame KeyTime="00:00:00">
<DiscreteObjectKeyFrame.Value>
<SolidColorBrush>DarkBlue</SolidColorBrush>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</Window>

using System.Windows;
using System.Windows.Interactivity;

namespace WPF3.Behaviors
{

public sealed class BooleanVisualStateBehavior : Behavior<FrameworkElement>
{

public static readonly DependencyProperty StateProperty = DependencyProperty.RegisterAttached(
"State",
typeof(bool),
typeof(BooleanVisualStateBehavior),
new PropertyMetadata(false, PropertyChangedCallback));

private static void PropertyChangedCallback(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs dependencyPropertyChangedEventArgs)
{
var visualStateSettingBehavior = dependencyObject as BooleanVisualStateBehavior;
if (visualStateSettingBehavior == null)
{
return;
}

var state = visualStateSettingBehavior.State
? visualStateSettingBehavior.TrueState
: visualStateSettingBehavior.FalseState;
bool result;
var frameworkElement = visualStateSettingBehavior.AssociatedObject;
result = VisualStateManager.GoToElementState(frameworkElement, state, true);
}

public bool State
{
get
{
return (bool)this.GetValue(StateProperty);
}
set
{
this.SetValue(StateProperty, value);
}
}

public string TrueState
{
get;
set;
}

public string FalseState
{
get;
set;
}

}

}

BooleanVisualStateBehaviorは、依存関係プロパティであるStateをトリガーにして、TrueStateとFalseStateに指定した状態名への遷移を試みるBehaviorです。
このBehaviorがWindowとGridの直下に存在します。
これが問題で、Behaviorで渡ってくるFrameworkElementがWindowとGridになります。
つまり、WindowとGridの直下にあるVisualStateを操作しようにも、Gridの下にはVisualStateが定義されていないから何も起きないわけです。

ですので解決策は、

1. 2つめのBooleanVisualStateBehaviorであるState2Changeを1つめのBooleanVisualStateBehaviorであるState1Changeと同じ位置に持ってくる。

つまり


<i:Interaction.Behaviors>
<behaviors:BooleanVisualStateBehavior x:Name="State1Change"
FalseState="False1"
State="{Binding State1}"
TrueState="True1" />
<behaviors:BooleanVisualStateBehavior x:Name="State2Change"
FalseState="False2"
State="{Binding State2}"
TrueState="True2" />
</i:Interaction.Behaviors>

します。

または、

2. 2つめのVisualStateGroupであるState2をGridの下に移動する

になります。

Conclusion


原因としては、非常に陳腐ですが、メソッドが動かない理由がわかりづらいため、解決に少し時間を取られました。
VisualStateは便利ですが、XAMLでの定義が間違ったりすると例外を投げるくせに、遷移で失敗しても例外を投げないなど、統一感が無くて困ります。
まぁ、一番悪いのは自分ですがね。なんで、Behaviorをルール要素では無く、ネストした要素の下に書いたんですかねぇ。

Source Code

https://github.com/takuya-takeuchi/Demo/tree/WPF3

コメントを残す

メールアドレスが公開されることはありません。

%d人のブロガーが「いいね」をつけました。