Silverlightの流儀 その2

今回はユーザコントロールを作るときの流儀について。

といっても、WinFormのカスタムコントロールなんかとそんなに違いはなくて「依存プロパティ」の使い方だけに注意しておけばいい。

「依存プロパティ」の話の前にカスタムコントロールの基本的な作り方について。

ユーザコントロールの基本

例えば「Hello, World」と表示するラベルのあるカスタムコントロールを作る場合、VS2008であれば「Silverlight User Control」ファイルを追加して以下のように変更する。

HelloWorldControl.xaml
<UserControl x:Class="SilverlightApplication2.Controls.HelloWorldControl"
  xmlns="http://schemas.microsoft.com/client/2007"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    
  <Grid>
       <TextBlock x:Name="textLabel" Text="Hello, World" />
  </Grid>
    
</UserControl>

Gridの下にTextBlockを追加している。

ちなみにルート要素は必ずしもUserControlである必要はなく、継承できるコントロールであればなんでもいい。といっても今のところUserControlとGrid、Canvas、StackPanelの四つに限られる。これはそれ以外のコントロールが今のところ*1継承不可(sealedクラス)なためである。

コードビハインドには特に変更を加える必要は無い。

HelloWorldControl.xaml.cs
using System;
using System.Windows.Controls;

namespace SilverlightApplication2.Controls {
  public partial class HelloWorldControl : UserControl {
      public HelloWorldControl() {
          InitializeComponent();
      }
  }
}

では、このユーザコントロールを使ってみる。ユーザコントロールを使うには、まずそれを使うXAML上にそのユーザコントロールの属する名前空間を宣言(インポート)する必要がある。

ここでは「my」という名前空間プレフィックスで、先程のユーザコントロール名前空間を宣言している。

Page.xaml
<UserControl x:Class="SilverlightApplication2.Page"
  xmlns="http://schemas.microsoft.com/client/2007"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  xmlns:my="clr-namespace:SilverlightApplication2.Controls">
    
  <Grid>
      <my:HelloWorldControl />
  </Grid>
    
</UserControl>

あとは名前空間プレフィックスを指定してユーザコントロールを宣言すれば、他のコントロールと同様に使用することができる。

これをビルドして実行すると以下のように表示される。

では、この「Hello, World」という文字列を外部から設定できるようにするにはどうすればいいか?

まともに考えれば以下のようにTextBlockのTextプロパティに値を設定するプロパティを定義して、

HelloWorldControl.xaml.cs
/// <summary>
/// テキストを取得、設定します。
/// </summary>
public string Text {
   get { return textLabel.Text; }
   set { textLabel.Text = value; }
}

このプロパティをXAML側から設定すればいい。

Page.xaml
<UserControl x:Class="SilverlightApplication2.Page"
   xmlns="http://schemas.microsoft.com/client/2007"
   xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
   xmlns:my="clr-namespace:SilverlightApplication2.Controls">
    
   <Grid>
       <my:HelloWorldControl Text="hoge"/>
   </Grid>
    
</UserControl>

これだけでも正常に動作するが、Silverlightの流儀には反する(このエントリを書く前はこれでは正常に動作しないと思っていたので、以降の説明には説得力がない)。

そこで登場するのが冒頭で出てきた「依存プロパティ」である。先程のTextプロパティを依存プロパティで書き直すと以下のようになる。

HelloWorldControl.xaml.cs

using System;
using System.Windows;
using System.Windows.Controls;

namespace SilverlightApplication2.Controls {
   public partial class HelloWorldControl : UserControl {
       /// <summary>
       /// Text
       /// </summary>
       public static readonly DependencyProperty TextProperty = DependencyProperty.Register(
           "Text", typeof(string), typeof(HelloWorldControl),
           (d, e) => ((HelloWorldControl)d).textLabel.Text = (string)e.NewValue
       );
       /// <summary>
       /// テキストを取得、設定します。
       /// </summary>
       public string Text {
           get { return (string)GetValue(TextProperty); }
           set { SetValue(TextProperty, value); }
       }

       public HelloWorldControl() {
           InitializeComponent();
       }
   }
}

TextPropertyというパブリックなフィールドを「DependencyProperty」という型で定義している。第一引数でプロパティ名、第二引数でプロパティの型、第三引数でこのプロパティを持つ型、最後にこのプロパティの値が変更された時に呼び出されるコールバックメソッドを指定する。

そしてこのフィールドへのヘルパーメソッドとしてのTextプロパティを定義している。

そもそも依存プロパティがその効果を発揮するのは、自分自身以外に拡張プロパティを提供する時である。

どういう事かCanvasを例にとって説明する。CanvasではCanvasの子コントロールに「Canvas.Left」「Canvas.Top」「Canvas.ZIndex」というプロパティを設定することができる。このプロパティを設定することによって子コントロールCanvas上の位置を決めることができる。

<UserControl x:Class="SilverlightApplication2.Page"
   xmlns="http://schemas.microsoft.com/client/2007"
   xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    
   <Canvas>
       <TextBlock x:Name="textLabel" Canvas.Left="10" Canvas.Top="10" Text="hoge" />
   </Canvas>
    
</UserControl>

何故このような事ができるかというと、これらのプロパティが依存プロパティとして実装されているから。「Canvas.Left」などのプロパティ設定が実際にどのようなコードに変換されるかというと、

Canvas.SetLeft(textLabel, 10);

となる。

これは以下のコードとも等価である。

textLabel.SetValue(Canvas.LeftProperty, 10);

ここからわかることは依存プロパティとはUI要素と値からなるマップということ。UI要素をキーとして、これに関連付く値を格納しているだけ。

これはWinFormのIExtenderProviderインターフェースを利用した拡張プロパティをより洗練させたものだと言える。
IExtenderProviderについては以下の記事を参考のこと


なので、今回のような自分自身で完結する場合は依存プロパティにする利点は何も無いと言えるけど、標準で用意されているコントロールのプロパティは全て依存プロパティで提供されているので、これに合わせるという意味で依存プロパティで実装しておこう。

*1:正式リリースされたら継承できるようになるかどうかは知らないけど