WisdomSoft - for your serial experiences.

2.8 依存プロパティ

.NET Framework の機能として備わっているプロパティとは別に、WPF では実行時にインスタンスに対して任意に追加できる「依存関係プロパティ」を提供しています。なお、本書執筆時点では英語 Dependency Property を直訳した「依存プロパティ」と表記していますが、MSDN によれば「依存関係プロパティ」が正式な日本語訳となります。

2.8.1 動的なプロパティ

プロパティの概念は .NET の機能として提供されていることはご存知の通りです。プロパティは、オブジェクトが内部で管理するフィールドにアクセスするハンドラの定義を自動化してくれます。プロパティによる内部データの隠蔽では、もはやオブジェクト指向プログラミングでは一般的な設計です。データの入出力にアクセッサメソッドを必ず通すため、データ表現の仕様変更などに比較的容易に対応することができ、オブジェクト自身がデータ入出力の監視も行えます。

プロパティは、オブジェクトにデータ入出力字の柔軟性を与えますが、それでも設計によってはいくつかの問題を抱えることになります。例えば、プロパティによるデータ入出力の監視は、そのプロパティを定義している型の内部でしか行えません。外部のコードは、オブジェクトのプロパティにアクセスがあったことを知る手段がありません。そのため、外部のコードにプロパティの変更を伝えるには、プロパティの変更を通知するイベントを別途用意する必要があります。

また、特定のオブジェクト関係にのみ発生する状況依存の情報をオブジェクトに保有させたい場合も問題になります。例えば、何らかの子コンポーネントが、特定の型の親コンポーネントに何らかのメッセージを伝えたい場合です。この情報は子コンポーネントに関連付けられるべきですが、プロパティが利用されるのは親コンポーネントに子コンポーネントが追加されたときのみだと仮定します。この場合、子コンポーネントにプロパティを定義するのは冗長で奇妙な設計になります。

代表的な問題は、子コントロールが子を管理する親コンテナにレイアウト情報を伝えたい場合です。一般的なオブジェクト指向設計であれば、親コンテナは抽象化され、レイアウトの種類によってサブクラスが実装されます。コンテナが上下左右中央にコントロールを配置するフレームタイプであれば、子コントロールは親コンテナに「左側」「右側」「中央」といった配置情報を伝えたいかもしれません。しかし、親コンテナが表のようなグリッド状のレイアウトを提供する場合、こうした情報は意味を持ちません。子コントロールは、親コンテナの実装の型に応じて異なる種類のレイアウト情報を提供しなければなりません。この情報は、実行時レベルに依存してしまうため、コントロールのプロパティとして宣言することは冗長です。

このような、実行時や状況、オブジェクトの関係に依存するプロパティは、.NET が提供しているプロパティの機能では解決することができません。そこで、WPF では、より柔軟にオブジェクトと情報を関連付けるために依存プロパティと呼ばれる仕組みを導入しています。

依存プロパティは、.NET や CLR レベルの概念ではなく、WPF のクラスライブラリで提供される機能です。依存プロパティを利用することができるのは、System.Windows.DependencyObject クラス、またはそのサブクラスのみです。

System.Windows.DependencyObject クラス
System.Object 
   System.Windows.Threading.DispatcherObject 
    System.Windows.DependencyObject
[TypeDescriptionProviderAttribute(typeof(DependencyObjectProvider))] 
public class DependencyObject : DispatcherObject

WPF の主要なコントロールが継承している UIElement クラスが DependencyObject を継承しているため、WPF のオブジェクトの多くが依存プロパティをサポートしています。

依存プロパティは、コンパイル時ではなく、実行時にプロパティ情報を生成し、値をオブジェクトと関連付けることができるようになります。プロパティ情報と値の設定・取得は、すべて DependencyObject クラスで宣言されている SetValue() メソッドGetValue() メソッドを用いて行うことができます。

DependencyProperty クラス SetValue() メソッド
public void SetValue (DependencyProperty dp, Object value)
DependencyProperty クラス GetValue() メソッド
public Object GetValue (DependencyProperty dp)

SetValue() メソッドから、DependencyObject オブジェクトに対して新しくプロパティ値を設定することができ、GetValue() メソッドから、プロパティ値を取得することができます。それぞれ、第1パラメータの dp には、プロパティの種類を識別するための System.Windows.DependencyProperty クラスのオブジェクトを設定します。

SetValue() メソッドの value パラメータから、dp で指定したプロパティに関連付ける値を設定することができます。また、GetValue() メソッドの戻り値から、設定した値を取得することができます。

SetValue() メソッドと GetValue() メソッドの実質的な効果は辞書オブジェクトの振る舞いに似ています。dp パラメータに指定する DependencyProperty クラスのオブジェクトは、辞書におけるキーに相当します。

System.Windows.DependencyProperty クラス
[TypeConverterAttribute("System.Windows.Markup.DependencyPropertyConverter, PresentationFramework, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, Custom=null")] 
public sealed class DependencyProperty

DependencyProperty クラスは、コンストラクタを公開していないため static な Register() メソッドからオブジェクトを取得します。

DependencyProperty クラス Register() メソッド
public static DependencyProperty Register (
    string name,
    Type propertyType, Type ownerType
)

name には、このプロパティを識別するための名前を文字列で指定します。propertyType には、このプロパティに設定される値の型情報を表す Type オブジェクトを、ownerType には、この依存プロパティを利用する型を表す Type オブジェクトを指定します。

当然、依存プロパティの種類によって設定される値の型は異なるはずなので、SetValue() や GetValue() メソッドは Object 型として値を入出力します。しかし、SetValue() メソッドであらゆる型の設定を許してしまうと、仕様の制約上 String 型の依存プロパティを想定しているにも関わらず int 型の整数など、異なる型の値を渡してしまうことができます。

そこで、SetValue() メソッドは、value パラメータに渡された値の型が DependencyProperty の propertyType に設定した型を実装するかどうかを調べ、方が一致しない場合は実行時例外が発生するように仕組まれています。

依存プロパティを利用することで前述した状況依存の情報交換を解決することができます。例えば、特定の型の親コンテナに子コントロールがレイアウト情報を伝えたい場合、親コンテナが対応することができる依存プロパティを公開し、子コントロールはコンテナが提供する DependencyProperty オブジェクトを使ってレイアウト情報を依存プロパティに設定すればよいのです。

コード1
using System;
using System.Windows;

class Test {
	public static DependencyProperty LactProperty;
	static Test() {
		LactProperty = DependencyProperty.Register(
			"Lact", typeof(String), typeof(Test)
		);
	}

	[STAThread]
	public static void Main() {
		Window wnd = new Window();
		wnd.SetValue(LactProperty, "乳酸菌とってるぅ?");
		wnd.Content = wnd.GetValue(LactProperty);
		wnd.FontSize = 40;

		Application app = new Application();
		app.Run(wnd);
	}
}
実行結果
コード1 実行結果

コード1は、Window オブジェクトに対して Test クラスで作成した依存プロパティの値を設定・取得するプログラムです。Window クラスは DependencyObject から派生しているので、依存プロパティを利用することができます。

通常、依存プロパティを提供するクラスは static で readonly なフィールドとして DependencyProperty オブジェクトを公開します。前述した例で考えると、子コントロールが親コンテナにレイアウト情報を提供しなければならない場合、親コンテナのクラスがレイアウト情報を保存する依存プロパティへのアクセス手段を提供し、子コントロールのオブジェクトが依存プロパティを利用して値を設定するという関係になります。

このプログラムでは、Test クラスが公開する DependencyProperty を利用して、Window オブジェクトの SetValue() メソッドから文字列を設定し、その後 GetValue() から設定した文字列を取得しています。SetValue() メソッドと GetValue() メソッドで設定する DependencyProperty オブジェクトによって、値を適切に出し入れすることができています。

DependencyProperty オブジェクトは、辞書におけるキーの役割を果たすため、同一のキーを設定すれば値を共有することができますが、異なるキーを設定することで、任意の数の依存プロパティを自由に設定することができます。

コード2
using System;
using System.Windows;

class Test {
	public static readonly DependencyProperty StringProperty;
	public static readonly DependencyProperty IntegerProperty;

	static Test() {
		StringProperty = DependencyProperty.Register(
			"String", typeof(String), typeof(Test)
		);

		IntegerProperty = DependencyProperty.Register(
			"Integer", typeof(int), typeof(Test)
		);
	}

	[STAThread]
	public static void Main() {
		Window wnd = new Window();
		wnd.SetValue(StringProperty, "Call me!");
		wnd.SetValue(IntegerProperty, 39108);

		String text = "StringProperty=\"" + wnd.GetValue(StringProperty) + "\"\n";
		text += "IntegerProperty=" + wnd.GetValue(IntegerProperty);

		wnd.Content = text;
		wnd.FontSize = 30;

		Application app = new Application();
		app.Run(wnd);
	}
}
実行結果
コード2 実行結果

コード2は、Window オブジェクトに文字列型の依存プロパティ StringProperty と、int 型の依存プロパティ IntegerProperty をそれぞれ設定しています。その後、GetValue() メソッドからそれぞれの依存プロパティを取得していますが、文字列と整数は個別に、適切に保存されていることが実行結果から確認できます。

このように、依存プロパティは、DependencyProperty プロパティをキーに用いて、特定のオブジェクトと値を関連付けます。これは、コンパイル時に用意される .NET のプロパティとは根本的に異なり、実行時レベルで動的に生成されるプロパティです。

2.8.2 依存プロパティのスコープ

依存プロパティは、DependencyProperty オブジェクトを、SetValue() メソッドと GetValue() メソッドでキーのように利用しましたが、これは、DependencyProperty オブジェクトが提供するプロパティ名を使って識別しています。依存プロパティの名前は Register() メソッドの第 1 パラメータ name に設定した文字列が使われます。

当然、DependencyProperty オブジェクトを提供するクラスの開発者は、プロパティの意味にちなんだ名前を付けようとしますが、Name や Text、Color などのありきたりな名前は、他のクラスの開発者も利用する可能性があります。同一の名前の DependencyProperty を登録してしまった場合、どうなるのでしょう。

幸いにも、DependencyProperty オブジェクトの名前はグローバルではありません。DependencyProperty オブジェクトに設定する名前は、Register() メソッドの第 3 パラメータに設定した型ごとに管理されます。よって、プロパティ名のスコープは ownerType パラメータに設定した型と関連付けられます。

Register() メソッドは、登録されている依存プロパティを内部で管理しているため、もし Register() メソッドに、すでに登録済みの DependencyProperty と衝突する  ownerType パラメータと name の組み合わせを渡した場合、実行時例外が発生します。

コード3
using System;
using System.Windows;

class A {
	public static readonly DependencyProperty NameProperty;
	static A() {
		NameProperty = DependencyProperty.Register(
			"Name", typeof(String), typeof(A)
		);
	}
}

class B {
	public static readonly DependencyProperty NameProperty;
	static B() {
		NameProperty = DependencyProperty.Register(
			"Name", typeof(String), typeof(B)
		);
	}
}

class Test {
	[STAThread]
	public static void Main() {
		Window wnd = new Window();
		wnd.SetValue(A.NameProperty, "Here we go");
		wnd.SetValue(B.NameProperty, "Go My Way");

		String text = "A Name=" + wnd.GetValue(A.NameProperty) + "\n";
		text += "B Name=" + wnd.GetValue(B.NameProperty);

		wnd.Content = text;
		wnd.FontSize = 30;

		Application app = new Application();
		app.Run(wnd);
	}
}
実行結果
コード3 実行結果

コード3は、A クラスと B クラスで、それぞれ同じ名前の Name という名前を設定した DependencyProperty を公開しています。これらは、プロパティ名が同一ですが、依存プロパティを所有する ownerType パラメータに設定している型が異なるため、衝突しません。それぞれの DependencyProperty オブジェクトを用いて、個別に値を設定することができます。

2.8.3 依存プロパティの隠蔽

依存プロパティの原理を理解していれば値を設定したり、取得することができますが、依存プロパティの存在は設計者にとって重要というだけで、クラスライブラリの利用者にとっては作業が複雑になるだけです。やはり、仕組みとしては .NET が提供するプロパティが適しています。

そこで、クラスの内部で依存プロパティを利用する場合は、通常のプロパティで隠蔽することが望まれます。つまり、依存プロパティを隠蔽するラッパープロパティを作成します。

public static readonly DependencyProperty TextProperty;

public String Text {
	set { SetValue(TextProperty, value); }
	get { return (String)GetValue(TextProperty); }
}

上記のように、クラスが保有している DependencyProperty オブジェクトに間接参照するプロパティを構築すれば、クラスの利用者にとっては依存プロパティと通常のプロパティの違いを意識する必要はなくなります。

実際に、この仕組みは WPF の多くのクラスで使われています。WPF では、依存プロパティを保有するクラスは、DependencyProperty オブジェクトを public static readonly で宣言されるフィールドとして公開しています。依存プロパティは、必ずプロパティ名に加えて Property という接尾辞で公開されています。そして、依存プロパティにアクセスする通常のプロパティは、依存プロパティの名前から Property という接尾辞を除いた名前になります。