ネコのために鐘は鳴る

寺院に住み着くパソコ〇好き

DependencyPropertyとBindingについて

DependencyPropertyとは何か

WPFにおけるBindingの根幹をなす依存関係プロパティ(DependencyProperty)について書いておこうと思います。

まず、DependencyPropertyとは何かというと、一言で言うとBindingができるプロパティのことです。 より正確に言うと、Bindingのtargetとなることができるプロパティです。

まずはその定義から見ていきましょう。

public class SampleClass : DependencyObject
{
    public static readonly DependencyProperty MyNameProperty =
        DependencyProperty.Register("MyName",
                                    typeof(string),
                                    typeof(SampleClass),
                                  new PropertyMetadata(null));
}

説明の為にSampleClassというクラスを定義しました。DependencyObjectを継承しています。 そして、一見何やら奇怪なstaticフィールドMyNamePropertyが定義されていますが、これがDependencyPropertyです。

そして、そのDependencyPropertyに値を与えているRegisterメソッドの4つの引数についてです。

1つ目は、プロパティ名。依存関係プロパティは〇〇Propertyというフィールド名にするのが慣習であるため、〇〇の部分がこれに当たります。

2つ目はプロパティの型。

3つ目はこの依存関係プロパティを持つクラス。

4つ目はこの依存関係プロパティのメタデータであり、その引数(上記の場合null)はプロパティの初期値です。

Propertyというからには、何らかの値を保持したり取り出したり出来るわけです。

var sample = new SampleClass();
// MyNamePropertyに "ねこ" とセットする
sample.SetValue(SampleClass.MyNameProperty, "ねこ");

// MyNamePropertyから値を取り出す
var n = (string)sample.GetValue(SampleClass.MyNameProperty);

このようにして値をセットしたり取り出したり出来るのですが、これでは使いにくすぎます。 そこで、普通のプロパティと同じように使えるように、ラッパープロパティを定義してあげます。

public class SampleClass : DependencyObject
{
    // 依存関係プロパティのラッパープロパティ
    public string MyName
    {
        get { return (string)GetValue(MyNameProperty); }
        set { SetValue(MyNameProperty, value); }
    }

    public static readonly DependencyProperty MyNameProperty =
        DependencyProperty.Register("MyName",
                                    typeof(string),
                                    typeof(SampleClass),
                                  new PropertyMetadata(null));
}

MyNameという普通のプロパティを新たに定義し、先ほどの依存関係プロパティをラップしていることがわかります。

ちなみに、この依存関係プロパティとラッパープロパティの記述は、Visual Studioの場合propdpと打ち、Tabキー2回押してスニペットを使用すれば簡単に書くことができます。楽チンは人生の花。

これで、普通のプロパティと同様の使い方ができます。

var sample = new SampleClass();
// MyNamePropertyに "ねこ" とセットする
sample.MyName = "ねこ";

// MyNamePropertyから値を取り出す
var name = sample.MyName;

ここまでしてようやく見慣れた普通のプロパティですね。

さて、本題はここからです。

BindingとDependencyProperty

まず始めに、認識を正しくしておきましょう。Bindingとはプロパティとプロパティを同期させるものではありません。 プロパティと依存関係プロパティを同期させるものです。

xamlでBindingを書くと、いまいち曖昧な理解でも書けてしまうのでC#でBindingを書くとしましょう。

クラス定義側

public class SampleClass : DependencyObject
{
    public string MyName
    {
        get { return (string)GetValue(MyNameProperty); }
        set { SetValue(MyNameProperty, value); }
    }

    public static readonly DependencyProperty MyNameProperty =
        DependencyProperty.Register("MyName",
                                    typeof(string),
                                    typeof(SampleClass),
                                  new PropertyMetadata(null));

    public override string ToString()
    {
        return MyName;
    }
}

public class SourceObject
{
    public string Name { get; set; }
}

Bindingの実行

// BindingのTargetになる依存関係オブジェクト
var sample = new SampleClass();
var bind = new Binding();
// BindingのSourceとなるプロパティを持つオブジェクト
bind.Source = new SourceObject() 
{ 
    Name = "ねこちゃん" 
};
// Bindingの実行
bind.Path = new PropertyPath("Name");
BindingOperations.SetBinding(sample,
                             SampleClass.MyNameProperty,
                             bind);

上記のソースコードを見てもらえれば分かるように、BindingにはSourceとTargetがあり、Sourceになれるのはプロパティ、Targetになれるのは依存関係プロパティです。

(本筋の話とは逸れますが、SampleClassがToString()をoverrideしてMyNameを返しているのは意味があってやっています。後で説明します)

これを、xamlで書くとどうなるのでしょうか?

ただのDependencyObjectをUIElementで構成されるViewのLogicalTreeの中に書くことはできないので、今回はLabelのコンテンツとして配置することにしましょう。 (LabelのContentはobject型なのでなんでも入れられる)

ちなみに、先ほどSampleClassのToString()をoverrideしていた理由はこのためで、Labelの中身というのは実はToString()された文字列が表示される仕組みになっていて、 この場合MyNameが画面に表示されるという仕掛けです。

<Label>
    <Label.Resources>
        <local:SourceObject x:Key="SourceObj" Name="こねこ"/>
    </Label.Resources>
    <local:SampleClass MyName="{Binding Name, Source={StaticResource SourceObj}}"/>
</Label>

本筋の話とはやや逸れますが、今回のSampleClassはFrameworkElementですらないDependencyObjectなので、FrameworkElementが持つDataContextの恩恵を受けられません。 したがって、Resourcesの中にSourceObjectを持ち、StaticResourceとしてBindingのSourceに指定しています。

一見すると、MyNameプロパティとNameプロパティをBindingしているように見えますが違います。 Bindingはプロパティと依存関係プロパティの間しか繋げません。

これは、xamlが解釈される時に、自動的にその背後にある依存関係プロパティがTargetになるよう解釈されているだけです。

実際、依存関係プロパティのラッパープロパティでない普通のプロパティに対しxamlでBinding構文を書くと、Visual Studioに怒られます。

もちろん、Bindingではなく、ただ値を与えるだけならば、普通のプロパティでもxaml上で書けます。

public class Hoge
{
    // 普通のプロパティ
    public string Name { get; set; }
}
<!-- エラーは出ない -->
<Hoge Name="ねこ"/>

DependencyProperty同士のBinding

さて、今、チェックボックスが2つあり、そのチェックを同期させたいということを考えてみましょう。 つまり、片方をチェックすると、もう片方も自動でチェックがつくということです。

これはxaml上で次のようにBindingを書けば特に何かC#でコードを書くことなく実現できます。

<CheckBox x:Name="source"/>
<CheckBox IsChecked="{Binding IsChecked, ElementName=source, Mode=TwoWay}"/>

前提として、CheckBoxIsCheckedプロパティ等、主要なコントロールのプロパティはほぼ全て依存関係プロパティとして実装されています。 つまり、CheckBoxIsCheckedプロパティはCheckBox.IsCheckedPropertyという依存関係プロパティのラッパープロパティであるということです。

上記のxamlは一見、依存関係プロパティ同士のBindingに見えますが、何度も言うようにBindingはプロパティと依存関係プロパティを繋ぎます。

すなわち、BindingのTargetはCheckBox.IsCheckedProperty依存関係プロパティですが、SourceはラッパープロパティであるIsCheckedプロパティなわけです。

勿論、IsCheckedプロパティの実態はCheckBox.IsCheckedProperty依存関係プロパティなので、当然事実上2つのチェックボックスの依存関係プロパティが連動するわけですね。

さいごに

Bindingには方向(Mode)と更新タイミング(UpdateSourceTrigger)とかがあって、もう少し奥が深いのですがその辺はまた別の記事で書こうかなと思います。

書いたらこの辺にリンクを貼っておこうと思います。。。