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を定義していたからです。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
<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>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
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が定義されていないから何も起きないわけです。

ですので解決策は、

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

つまり

1
2
3
4
5
6
7
8
9
10
<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つめのVisualStateGroupであるState2をGridの下に移動する

になります。

Conclusion

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

Source Code

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