Silverlight 2 Beta 2で追加されたVisualStateManager

Silverlight 2のBeta 2がリリースされて、色々と新しい機能が追加された。その中で一番おもしろそうなのがこれ「Visual State Manager」という仕組み。

簡単に言えば、コードを書かなくてもUIコンポーネントの状態(イベント)に合わせて、UIの表示を変えられますよという機能。EventTriggerがSilverlightでは使えないので、その代替機能という感じらしい(WPFにもこれが実装される予定)。

VSのサポートが不十分なのとExpression Blendの使い方がわからないのとで(あとドキュメントが不親切すぎる)、理解するのに苦労したけどやっとわかった。

例えばマウスを上に移動させるとちょっと透明にするボタンを作る場合、今までだとボタンのMouseOverイベントでOpacityを変更するアニメーションをBeginして、MouseLeaveイベントでアニメーションをStopとかする必要があったけど、VisualStateManagerを使うとこれらがコードレスでできる。

以下がそのXAMLコード

Page.xaml

<UserControl x:Class="SilverlightApplication1.Page"
   xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
   xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
   Width="500" Height="500">

   <Grid x:Name="LayoutRoot" Background="White" >
       <Button Width="80" Height="40" Content="OK" Foreground="White">
           <Button.Template>
               <ControlTemplate TargetType="Button">
                   <Grid>
                       <Border x:Name="frame" BorderBrush="Black" BorderThickness="1" CornerRadius="20">
                           <Border.Background>
                               <LinearGradientBrush StartPoint="0.5,0" EndPoint="0.5,1">
                                   <GradientStop Color="White" Offset="0" />
                                   <GradientStop Color="Black" Offset="0.6" />
                                   <GradientStop Color="White" Offset="1" />
                               </LinearGradientBrush>
                           </Border.Background>
                            
                           <Grid HorizontalAlignment="Center" VerticalAlignment="Center">
                               <ContentPresenter Content="{TemplateBinding Content}" Foreground="{TemplateBinding Foreground}" />
                           </Grid>
                       </Border>

                       <VisualStateManager.VisualStateGroups>
                           <VisualStateGroup x:Name="FocusStates">
                               <VisualState x:Name="Unfocused" />
                               <VisualState x:Name="Focused" />
                           </VisualStateGroup>
                           <VisualStateGroup x:Name="CommonStates">
                               <VisualState x:Name="Normal" />
                               <VisualState x:Name="MouseOver">
                                   <Storyboard>
                                       <DoubleAnimation To="0.5" Duration="0:0:0"
                                                Storyboard.TargetName="frame" Storyboard.TargetProperty="Opacity" />
                                   </Storyboard>
                               </VisualState>
                               <VisualState x:Name="Pressed" />
                               <VisualState x:Name="Disabled" />
                           </VisualStateGroup>
                       </VisualStateManager.VisualStateGroups>
                   </Grid>
               </ControlTemplate>
           </Button.Template>
       </Button>
   </Grid>
    
</UserControl>

解説

ButtonのTemplateの中で「VisualStateManager.VisualStateGroups」というのを宣言している。その中には「VisualStateGroup」を宣言して、さらにその下に「VisualState」を宣言する。

VisualStateはUIコンポーネントのステートとそれに関連付けるStoryboardを定義する。VisualStateGroupはVisualStateをグループ化するためのもの。

定義できるVisualStateはUIコンポーネント毎に決まっていて、それぞれのUIコンポーネントに「TemplateVisualState」というカスタム属性を使って定義されている。

例えばButtonの場合、

  • TemplateVisualState(Name="Unfocused", GroupName="FocusStates")
  • TemplateVisualState(Name="MouseOver", GroupName="CommonStates")-
  • TemplateVisualState(Name="Pressed", GroupName="CommonStates")
  • TemplateVisualState(Name="Focused", GroupName="FocusStates")
  • TemplateVisualState(Name="Disabled", GroupName="CommonStates")
  • TemplateVisualState(Name="Normal", GroupName="CommonStates")]

というのが定義されている。

これはildasm.exeやReflector.netなんかの逆アセンブラツールを使えば簡単に調べる事ができるので、この情報を元にしてVisualStateGroupsを宣言すればいい(グループ名は別に同じじゃなくても動くみたい)。

あとはVisualStateの下にStoryboardを宣言して、そのステートの時に実行したいアニメーションを定義すればいい。

<VisualState x:Name="MouseOver">
   <Storyboard>
       <DoubleAnimation To="0.5" Duration="0:0:0"
                        Storyboard.TargetName="frame" Storyboard.TargetProperty="Opacity" />
   </Storyboard>
</VisualState>

ちなみに、TemplateVisualState属性で宣言されているVisualStateを全て宣言しないといけないわけではなくて、使いたいものだけ宣言するだけでいい(最低限Normalだけは宣言したほうがいいけどね)。

今回ならこんだけでいい

<VisualStateManager.VisualStateGroups>
   <VisualStateGroup x:Name="CommonStates">
       <VisualState x:Name="Normal" />
       <VisualState x:Name="MouseOver">
           <Storyboard>
               <DoubleAnimation To="0.5" Duration="0:0:0"
                                Storyboard.TargetName="frame" Storyboard.TargetProperty="Opacity" />
           </Storyboard>
       </VisualState>
   </VisualStateGroup>
</VisualStateManager.VisualStateGroups>

あとクラスライブラリ的にはVisualStateManagerのVisualStateGroupsPropertyという依存添付プロパティはinternalなのでコードから使う事はできないし、インテリセンスも効かないので注意。さらにこのXAMLコードのコンパイルも一回目は失敗するけど二回目は何故か成功するので、二回ビルドする事を忘れずに!!

このへんは正式版では直っていると思いたい。

これでUIのカスタマイズが結構楽になる。でもこれControlTemplateでしか(たぶん)使えないから、気楽に使うというわけにはいかないんだけどね。そこだけが残念。

[追記]
あとこの機能を使うとVisualStudioのデザイナが使えなくなるので注意!まぁ、元々プレビューだけの機能しか無いのであまり困らないけどね。