2015/07/27

VisualStateManagerのVisualStateを確認する

WPFのコントロールの遷移をVisualStateManagerでデザインしていると、現在のVisualStateを直接確認したいときがあります。

VisualStateGroupが一つだけなら同じVisualStateGroup内のVisualStateは排他なので結果を見て判断することも難しくないですが、VisualStateGroupが複数になると異なるVisualStateGroup間のVisualStateは併存するので、複数のVisualStateの効果が重なることになり、何がどうなっているか判断に迷うことがあります。というか、かなり迷いました。

そこで、デバッグ用に現在のVisualStateを出力する添付プロパティを書いてみました。
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Media;
public class VisualStateMonitor : DependencyObject
{
public static int GetInterval(DependencyObject obj)
{
return (int)obj.GetValue(IntervalProperty);
}
public static void SetInterval(DependencyObject obj, int value)
{
obj.SetValue(IntervalProperty, value);
}
public static readonly DependencyProperty IntervalProperty =
DependencyProperty.RegisterAttached(
"Interval",
typeof(int),
typeof(VisualStateMonitor),
new FrameworkPropertyMetadata(0, OnIntervalChanged));
private static void OnIntervalChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var element = d as FrameworkElement;
if (element == null)
return;
var interval = (int)e.NewValue;
if (interval <= 0)
return;
Task.Run(async () =>
{
while (true)
{
element.Dispatcher.Invoke(() => CheckVisualState(element));
await Task.Delay(TimeSpan.FromSeconds(interval));
}
});
}
private static void CheckVisualState(FrameworkElement element)
{
var groups = GetVisualStateGroups(element);
if (groups != null)
{
foreach (var group in groups)
Debug.WriteLine($"Element: {element.Name} -> Group: {group.Name} -> State: {group.CurrentState?.Name}");
}
}
private static IEnumerable<VisualStateGroup> GetVisualStateGroups(FrameworkElement element)
{
if (VisualTreeHelper.GetChildrenCount(element) <= 0) // If the ControlTemplate has not been applied yet
return null;
foreach (var descendant in GetDescendants(element).OfType<FrameworkElement>())
{
var groups = VisualStateManager.GetVisualStateGroups(descendant)?.Cast<VisualStateGroup>();
if (groups != null)
return groups;
}
return null;
}
private static IEnumerable<DependencyObject> GetDescendants(DependencyObject reference)
{
if (reference == null)
yield break;
var queue = new Queue<DependencyObject>();
do
{
var parent = (queue.Count == 0) ? reference : queue.Dequeue();
var count = VisualTreeHelper.GetChildrenCount(parent);
for (int i = 0; i < count; i++)
{
var child = VisualTreeHelper.GetChild(parent, i);
queue.Enqueue(child);
yield return child;
}
}
while (queue.Count > 0);
}
}
このキモは、VisualStateGroupは対象のコントロール自体ではなく、たいていその中のGridなどに定義されるので、子要素を下りながら探す必要があることです。なお、VisualStateManagerではなくトリガーに依っている場合は、当然何も出てきません。

VisualStateの変化を捉える方法が見つからなかったのでIntervalで指定された秒ごとに永久ループで出力しますが、そこはあくまでデバッグ用ということで。

これをToggleButtonに使った例。
<Window x:Class="VisualStateTest.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:VisualStateTest"
Title="MainWindow"
Height="200" Width="200">
<Window.Resources>
<SolidColorBrush x:Key="ToggleButton.Normal.Background" Color="Orchid" Opacity="0.6"/>
<Color x:Key="ToggleButton.Selected.BackgroundColor">Gold</Color>
<ControlTemplate x:Key="ToggleButtonTemplate" TargetType="{x:Type ToggleButton}">
<Grid>
<Border x:Name="border"
Background="{StaticResource ToggleButton.Normal.Background}"
BorderThickness="1"
BorderBrush="Gray">
<ContentPresenter Margin="{TemplateBinding Padding}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
Content="{TemplateBinding Content}"/>
</Border>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal"/>
<VisualState x:Name="MouseOver">
<Storyboard>
<DoubleAnimation Storyboard.TargetName="border"
Storyboard.TargetProperty="(Button.Background).(SolidColorBrush.Opacity)"
To="0.8"
Duration="0:0:0.1"/>
</Storyboard>
</VisualState>
<VisualState x:Name="Pressed">
<Storyboard>
<DoubleAnimation Storyboard.TargetName="border"
Storyboard.TargetProperty="(Button.Background).(SolidColorBrush.Opacity)"
To="1.0"
Duration="0:0:0.1"/>
</Storyboard>
</VisualState>
<VisualState x:Name="Disabled"/>
</VisualStateGroup>
<VisualStateGroup x:Name="CheckStates">
<VisualState x:Name="Checked">
<Storyboard>
<ColorAnimation Storyboard.TargetName="border"
Storyboard.TargetProperty="(Button.Background).(SolidColorBrush.Color)"
To="{StaticResource ToggleButton.Selected.BackgroundColor}"
Duration="0:0:0.1"/>
</Storyboard>
</VisualState>
<VisualState x:Name="Unchecked"/>
<VisualState x:Name="Indeterminate"/>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</Grid>
</ControlTemplate>
</Window.Resources>
<Grid>
<ToggleButton x:Name="TestToggleButton"
Template="{StaticResource ToggleButtonTemplate}"
Content="Test"
local:VisualStateMonitor.Interval="3"/>
</Grid>
</Window>
view raw MainWindow.xaml hosted with ❤ by GitHub
このToggleButtonにはCommonStatesとCheckStatesのVisualStateGroupがあり、これらがToggleButtonと継承元のButtonBaseとさらに元のControlのChangeVisualStateの重ね掛けで変化するというややこしいものですが、一番下に加えたVisualStateMonitorの添付プロパティは以下のように出力します。
// 初期状態
Element: TestToggleButton -> Group: CommonStates -> State: Normal
Element: TestToggleButton -> Group: CheckStates -> State: Unchecked
// カーソルを上に移動
Element: TestToggleButton -> Group: CommonStates -> State: MouseOver
Element: TestToggleButton -> Group: CheckStates -> State: Unchecked
// クリック
Element: TestToggleButton -> Group: CommonStates -> State: MouseOver
Element: TestToggleButton -> Group: CheckStates -> State: Checked
// カーソルを上から外す
Element: TestToggleButton -> Group: CommonStates -> State: Normal
Element: TestToggleButton -> Group: CheckStates -> State: Checked
// 再度クリックしてカーソルを外す
Element: TestToggleButton -> Group: CommonStates -> State: Normal
Element: TestToggleButton -> Group: CheckStates -> State: Unchecked
これが分かれば簡単になるというものでもないですが、直接確認しながら作業できるのは随分違うと思います。実はもっと簡単にできる方法があるのかもしれませんが、とりあえず。

[追記] 添付プロパティの作成について

添付プロパティの構文の話ですが、依存関係プロパティの場合、PropertyMetadataのPropertyChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)のdはそのプロパティが定義されたクラスのインスタンスなので、これをキャストして処理をインスタンスメンバーに繋げることができます。一方、添付プロパティの場合、dはそのプロパティが添付されたFrameworkElementのインスタンスになるので、添付プロパティが定義されたクラスのインスタンスメンバーにそのままアクセスはできません。

したがって、FrameworkElementのインスタンスの処理はおおよそ以下に分かれます。
  1. 静的メソッドの中で処理を完結させる。デリゲートを駆使すればある程度複雑なこともできるが、状態をフィールドなどに保存できないので、行き届いた状態管理は難しい。

  2. 静的メソッドの中で、状態を静的フィールドなどに保存しながら処理する。アプリ中の一カ所でしか使われない場合や、静的な値が共有されても構わない場合に限られる。

  3. 添付プロパティの型をそのプロパティが定義されたクラスにすると、初期化時にe.NewValueにそのクラスのインスタンスが入ってくるので、それをキャストしてインスタンスメンバーに繋げる。WindowChromeで使われている方法で、複雑な処理も行いやすい。
あくまで構文上のテクニックですが、結構悩むところなので。

0 コメント :