dragan10

The Silverlight Geek - カスタムコントロールの深層に迫る

2008-10-25 18:58:49

昨夜のポストは前振りのたぐいでしたが、本格的に始めることにしましょう。

[詳細な説明や、コード・行間に書かれたような内容やがどうしても知りたい方は、このテーマに関するビデオのシリーズをご覧ください。このシリーズは、VBとC#のコード付きで、数週間以内にリリースされます。(訳注:すでにこちらでリリースされています)]

昨晩に話を始めたとおり、Silverlightでカスタムコントロールを書くのと、他のGUI環境で書く場合との違いは、パーツ・ステートモデルで実現されたロジックとビジュアルの厳格な分離の有無です。

余談ですが、ここでいつも私たちが指摘させてもらっているのは、Silverlightはプログラマがカスタムコントロールを書く際に、パーツ・ステートモデルの使用を強制しているわけではないということです。とはいえ、このモデルはMicrosoftが推奨しているモデルであり、Expression Blendが理解してサポートしてくれるモデルでもあります。実際のところ、パーツ・ステートモデルに準拠せずにカスタムコントロールを書くケースは、私が想像できる限りでは、ただ単にそれが可能であるということを示したい場合くらいです。

パーツ・ステートモデルの概要

パーツ・ステートモデルの背景にある中心的なコンセプトは、コントロールのロジックをビジュアルから厳密に分離するということで、ビジュアルはVisual State Managerによって管理されることになります。Visual State Managerは、(a)コントロールがどのステートにあるか(ステートの定義はすぐ後でします)(b)どのパーツがVSMのコントロール下にあるか を知る必要があります。

テンプレートを使ったことがある方には、ステートはおなじみのものでしょう。実際のところ、もしテンプレートを使ったことがないならここでちょっと寄り道していただいて、そちらを先に体験していただいた方がいいかと思います。スタイルとテンプレートに関するビデオを3本ポストしてありますので、そこから始めていただければよいと思いますし、Blogのエントリにも有用なものがいくつもあります。

パーツ・ステートモデルの観点から見ると、コントロールはあるステートか、あるいはステート間の移行中にいることになります。Visual State Managerは、その時点でのコントロールのステート(たとえばMouseOver)に関連づけられたストーリーボードを実行させます。

既存のコントロールからテンプレートを作成する場合、ステートはすでに列挙済みなので、新しいステートを追加するにはカスタムコントロールを作らなければなりません。これについては、少し後で説明します。

パーツ

コントロールはたくさんの部品から構成されていますが、パーツ・ステートモデルに照らし合わせてみた場合、パーツと見なされるのは、コントロール自身のメソッドから呼び出される部品だけです。

例としてSilverlightのツールボックスからScrollBarを取り上げてみましょう。パーツ・ステートモデルとしては、このコントロールは四つのパーツからできています。

   1. · Down Repeat Button

   2. · Up Repeat Button

   3. · Scroll Bar

   4. · Thumb

ScrollBarには他の要素もありますが、パーツはこれだけです。これはつまり、ScrollBarの他の要素から利用される要素はこれらだけということです。

パーツ・ステートモデルをサポートしている多くのコントロールには、たとえばButtonもそうですが、単なる部品は一つもありません(!)

コントラクトの作成

Silverlightでカスタムコントロールを書く場合、「このパーツはVSMの管理下にあります」というコントラクトを作成することになります。それ以外のすべてはロジックと見なされ、「壁の向こう側にある」わけです。

属性は、.NETのプログラムにおいてメタデータを保存するための仕組みです。例として、Ratingコントロールからの抜粋をご覧ください。Ratingコントロールは、ユーザーの操作に応じて'lit'になったり、通常の状態になったりします(属性に関してもっと知りたい場合は、どれかC#やVBのよい参考書をご覧ください)。

[TemplatePart( Name = "Core", Type = typeof( FrameworkElement ) )]

 

[TemplateVisualState( Name = "Normal", GroupName = "CommonStates" )]

[TemplateVisualState( Name = "MouseOver", GroupName = "CommonStates" )]

[TemplateVisualState( Name = "Pressed", GroupName = "CommonStates" )]

 

[TemplateVisualState( Name = "Lit", GroupName = "RatingStates" )]

[TemplateVisualState( Name = "Norm", GroupName = "RatingStates" )]

 

public class RatingControl : Control

 

このコートでは、六つの属性が新しいカスタムコントロールに追加されています。最初の属性はCoreと名付けられたパーツです(Karen Corbyの例から直接いただいてきました)。その次の三つは、新しいコントロールがサポートする三つの"common states"です。これらが同じ"CommonStates"というGroupNameを持っていることに注意してください。そして最後の7行目と8行目は、RatingStatesの二つのステート、LitとNormです。

コントラクトによるロジックとビジュアルの分離

この数行は、強力なコントラクトがどんなものかを示しています。コントラクトは、Expression Blendと同じように、開発者とデザイナによって頼りになるものです。この数行は、"Core"オブジェクト(これはXamlで書かれることになります)がパーツとして内部メソッドの管理下に置かれることと、新しいコントロールが二つのステートグループを持つことをはっきりと示しており、それぞれのグループ内のステートが列挙されています。

そしてクラス定義からは、新しいコントロールがControlをベースクラスとして導出されることがわかります。

コントラクトの実装

コントラクトは実装しなければなりません。ここでは次のような手順で実装します。

   1. Silverlightのアプリケーションを作成し、プロジェクトの種類としてWeb Siteを選択します。

   2. ソリューションを右クリックし、プロジェクトの追加を選択し、Silverlight Class Libraryを追加します。

   3. クラスライブラリに新しいクラス(Ratingと名付けました)を追加します。これでRating.csが作成されます。

   4. 自動的に作成されていたClass1.csを削除します。

   5. クラスライブラリのプロジェクトを右クリックし、追加→新しい項目を選択します。Silverlight User Controlテンプレートを選択し、generic.xamlという名前をつけます。これはこの名前でなければなりません(!)

   6. generic.xaml.csを削除します。

現状の確認

    * Rating.csにはカスタムコントロールのコードを、パーツ・ステートモデルのコントラクトを生成するためのメタデータと共に書いていきます。

    * generic.xamlにはコントロールのデフォルトのビジュアルを書いていきます(Xamlで)。

    * コントロールが書けたら、ステップ1で作成したプロジェクトのPage.xamlにそのインスタンスを作ります。

   1: <UserControl x:Class="BookRater1.Page"

   2:     xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

   3:     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

   4:     xmlns:Controls="clr-namespace:ClassLibrary;assembly=ClassLibrary"

   5:     xmlns:vsm="clr-namespace:System.Windows;assembly=System.Windows"

   6:     Width="600" Height="400">

   7:  

   8:     <Grid x:Name="LayoutRoot" Background="White">

   9:         <Grid.RowDefinitions>

  10:             <RowDefinition Height=".5*" />

  11:             <RowDefinition Height=".5*" />

  12:         </Grid.RowDefinitions>

  13:         <Grid.ColumnDefinitions>

  14:             <ColumnDefinition Width=".5*" />

  15:             <ColumnDefinition Width=".5*" />

  16:         </Grid.ColumnDefinitions>

  17:         

  18:         <Controls:RatingControl x:Name="Rating1"  Grid.Row="0" Grid.Column="0" />

  19:         <Controls:RatingControl x:Name="Rating2"  Grid.Row="1" Grid.Column="0" Template="{StaticResource RatingControlControlTemplate1}"   />

  20:     </Grid>

  21: </UserControl>

要注意事項をいくつか

上のリストはPage.xamlだということを忘れないでください - これは、カスタムコントロールを使っているページです。

    * 4行目では、このクラスライブラリの名前空間を設定しています。

    * 18行目から19行目では、カスタムコントロールのインスタンスを二つ生成しています。二つ目のインスタンスにはテンプレートを使って、デフォルトのビジュアルをオーバーライドしています。これは、他のコントロールでやるのと同じ全く同じやり方です(まだその種のテンプレートの作り方については話をしていませんが)。

Rating.xsとgeneric.xamlの中身

generic.xaml

   1: <ResourceDictionary

   2:    xmlns=  -- Many of these -->

   3:   <Style TargetType="controls:RatingControl">

   4:     <Setter Property="Template">

   5:       <Setter.Value>

   6:         <ControlTemplate TargetType="controls:RatingControl">

   7:           <Grid x:Name="LayoutRoot">

   8:             <Grid.Resources>

   9:               <Storyboard x:Key="UnLight" >

  10:                 <DoubleAnimation

  11:                    Storyboard.TargetName="Core"

  12:                    Storyboard.TargetProperty="(UIElement.Opacity)"

  13:                    Duration="0:0:0.01" From="1" To=".5"/>

  14:               </Storyboard>

  15:               <Storyboard x:Key="Light" >

  16:                 <!-- -->

  17:               </Storyboard>

  18:               <Storyboard x:Key="Bounce" RepeatBehavior="forever" >

  19:                 <DoubleAnimationUsingKeyFrames

  20:                     BeginTime="00:00:00"

  21:                     Duration="00:00:01"

  22:                     Storyboard.TargetName="Core"

  23:                     Storyboard.TargetProperty="(UIElement.RenderTransform).

  24:                     (TransformGroup.Children)[3].(TranslateTransform.Y)">

  25:                         <SplineDoubleKeyFrame KeyTime="00:00:00" Value="0"/>

  26:                         <SplineDoubleKeyFrame KeyTime="00:00:00.25" Value="25"/>

  27:                         <SplineDoubleKeyFrame KeyTime="00:00:00.5" Value="0"/>

  28:                         <SplineDoubleKeyFrame KeyTime="00:00:00.75" Value="50"/>

  29:                         <SplineDoubleKeyFrame KeyTime="00:00:01" Value="0"/>

  30:                 </DoubleAnimationUsingKeyFrames>

  31:               </Storyboard>

  32:               <Storyboard x:Key="Dip" >

  33:                 <!-- -->

  34:               </Storyboard>

  35:             </Grid.Resources>

  36:             <vsm:VisualStateManager.VisualStateGroups>

  37:               <vsm:VisualStateGroup x:Name="CommonStates">

  38:                 <vsm:VisualState x:Name="Normal" />

  39:                 <vsm:VisualState x:Name="MouseOver"

  40:                     Storyboard="{StaticResource Bounce}"/>

  41:                 <vsm:VisualState x:Name="Pressed"

  42:                     Storyboard="{StaticResource Dip}"/>

  43:               </vsm:VisualStateGroup>

  44:               <vsm:VisualStateGroup x:Name="RatingStates">

  45:                 <vsm:VisualState  x:Name="Norm"

  46:                     Storyboard="{StaticResource UnLight}" />

  47:                 <vsm:VisualState x:Name="Lit"

  48:                     Storyboard="{StaticResource Light}" />

  49:               </vsm:VisualStateGroup>

  50:             </vsm:VisualStateManager.VisualStateGroups>

  51:             <Ellipse

  52:                 x:Name="Core"

  53:                 Width="200"

  54:                 Height="200" RenderTransformOrigin="0.5,0.5" >

  55:               <Ellipse.RenderTransform>

  56:                 <TransformGroup>

  57:                   <ScaleTransform/>

  58:                   <SkewTransform/>

  59:                   <RotateTransform/>

  60:                   <TranslateTransform/>

  61:                 </TransformGroup>

  62:               </Ellipse.RenderTransform>

  63:               <Ellipse.Fill>

  64:                 <RadialGradientBrush>

  65:                   <GradientStop Color="#FFFFD954" Offset="0.004"/>

  66:                   <GradientStop Color="#FFE9F515" Offset="1"/>

  67:                   <GradientStop Color="#FFF1F712" Offset="0.911"/>

  68:                 </RadialGradientBrush>

  69:               </Ellipse.Fill>

  70:             </Ellipse>

  71:           </Grid>

  72:         </ControlTemplate>

  73:       </Setter.Value>

  74:     </Setter>

  75:   </Style>

  76: </ResourceDictionary>

このファイルは間引いてありますが、普通のテンプレートファイルのように読めるでしょう。実質的な内容は8行目からで、ここからResourcesセクションが始まります。ここには、ステートに対して割り当てたい振る舞いをストーリーボードとして作成ます。つまり、たとえばカスタムコントロールの上にマウスを持ってきたときに、カスタムコントロールが上下に活発にバウンドするようにしたいとすれば、そのためのストーリーボードをこのリソースセクションに書けばよいということです(18-31行目がそうです)。

Recourcesセクション(35行目)の次では、Visual State Groupおよび各グループ内のビジュアルステートを定義します(36から50行目)。ここでやっているのは、適切なストーリーボードの各ステートへの割り当てです。

最後に、51行目でカスタムコントロールのデフォルトのビジュアルを生成しています。これには、"Core"という名前になったパーツも含まれていて、その楕円が51から70行目で定義されています。この例は単純化されているので、コントロールが持っているオブジェクトは一つだけですが、もっと複雑なコントロールの場合は無名の要素をたくさん持つこともあります。

Rating.cs

作成するクラスのコードのファイルには、ロジックと、CLRのイベントをVSMが認識できるステートに変換するためのプライベートなコードが定義されています。コントロールをデフォルトの見かけ(generic.xaml)にするのか、コントロールがpage.xamlでインスタンス化された時点で要求されるテンプレート化された見かけにするのかを決めているのもここです。

ここでは、名前がつけられたパーツをXamlから取り出してメンバー変数で保持しています。これは、このパーツを後で頻繁に使うからです。そしてこのコントロールにプライベートのヘルパーメソッドのGotoStateを定義します。これは、その他のメンバー変数をチェックして、Visual State ManagerのstaticなGoToStateメソッドの呼び出し方を決定します。

   1: public class RatingControl : Control

   2: {

   3:    private FrameworkElement corePart;

   4:    private bool isMouseOver;

   5:    private bool isPressed;

   6:    public event RoutedEventHandler Click;

   7:      public RatingControl()

   8:      {  DefaultStyleKey = typeof(RatingControl);  }

   9:  

  10:      public override void OnApplyTemplate()

  11:      {

  12:          base.OnApplyTemplate();

  13:          CorePart = (FrameworkElement)GetTemplateChild("Core");

  14:          GoToState(false);

  15:      }

  16:  

  17:      private void GoToState(bool useTransitions)

  18:      {

  19:          if (isPressed)

  20:          { VisualStateManager.GoToState(this, "Pressed", useTransitions); }

  21:          else if (isMouseOver)

  22:          { VisualStateManager.GoToState(this, "MouseOver", useTransitions); }

  23:        //...

  24:      }

  25:      //...

  26:    }

まだ二つ大きな要素が欠けています。一つはCLRイベントをVSMのイベントに変換する処理で、もう一つはプライベートのメンバー変数CorePartのsetterメソッドが、単にCorePartを設定するだけでなく、古いイベントハンドラの登録を解除し、MouseENterやMouseLeave、MouseLeftButtonDown、MouseButtonUpなどの新しいイベントハンドラを登録しなければならないということです。後者のステップをこんなふうにこなせば、前者のステップは自然とできてしまいます:

   1: void corePart_MouseEnter(object sender, MouseEventArgs e)

   2: {

   3:     isMouseOver = true;

   4:     GoToState(true);

   5: }

   6:  

   7: void corePart_MouseLeave(object sender, MouseEventArgs e)

   8: {

   9:     isMouseOver = false;

  10:     GoToState(true);

  11: }

十分注意して見ていったとしても、混乱しがちかも知れません。かなり複雑な事項がたくさんあります。ですので、さらにややこしいことになるよりは、いったんここでストップしてまとめてみましょう。そして、それぞれのピースがどのように協調して動くのかがわかる最初のビデオを待ってから、この先へ進むことにしましょう。

一つにまとめあげる

簡単にいうと、すべてができあがった時点では5つのファイルが一緒に働くことになります。

1. クラスライブラリ内のRating.cs。これはロジックとメソッドだけでなく、カスタムコントロールの属性も定義します。属性はコントラクトを定義し、Blendのようなツールの助けを借りて、コントロールをスキン対応にしてくれます。

2. 同じくクラスライブラリ内に、generic.xamlという名前でなければならないファイルがあり、カスタムコントロールのデフォルトの見かけがここで定義されます(Xamlが使われます)。

3. アプリケーションには三つのファイルがあり(これは普段通りです)、重要な役目を果たします:Page.xamlとPage.xaml.csとApp.xamlです。これらは普段通りに働きます。つまり、Page.xamlはコントロールのインスタンスを生成します。もしかしたらPage.xamlにはTemplate文があり、デフォルトのルック・アンド・フィールを変更するために、Page.xamlか(こちらの方が普通でしょう)App.xamlからテンプレートを取ってくるかも知れません。

4. Page.xaml.csには、アプリケーションのロジックがあります(コントロールのではなく)。これはいつもの通り。

5. App.xamlには、ボタンやチェックボックスの場合でもそうするように、新しく作成したカスタムコントロールのテンプレートを置くことになるかも知れません。

続きはすぐ。

※このエントリは ブロガーにより投稿されたものです。朝日インタラクティブ および ZDNet Japan編集部の見解・意向を示すものではありません。
  • 新着記事
  • 特集
  • ブログ