DispatcherObjectの1段下に継承階層をおりると、DependencyObjectというクラスになります。DependencyObjectは、WPFで使われた独自のプロパティシステムを実装しています。この独自のプロパティシステムのことを、依存関係プロパティと添付プロパティといいます。
存関係プロパティ
依存関係プロパティは、通常のCLRのプロパティと比べて、以下の機能を追加で提供します。
- リソースからの値の取得
- データバインディングへの対応
- スタイルによる値の設定
- アニメーション
- オーバーライド可能なメタデータ
- 親子関係にあるインスタンスでのプロパティ値の継承
依存関係プロパティの定義方法
依存関係プロパティは、DependencyObjectを直接、または間接的に継承したクラスで定義可能です。定義方法は、DependencyPropertyクラスのRegisterメソッドを使用します。DependencyObjectを継承したPersonクラスにNameという依存関係プロパティを定義する方法を以下に示します。
publicclass Person : DependencyObject
{
publicstaticreadonly DependencyProperty NameProperty =
DependencyProperty.Register(
"Name", typeof(string), typeof(Person), new PropertyMetadata("default name"));
}
Registerメソッドを使いDependencyPropertyクラスのインスタンスを作成します。作成したインスタンスはpublic static readonlyのフィールドに「プロパティ名Property」の命名規約で格納します。DependencyPropertyの値は、DependencyObjectクラスに定義されているGetValue、SetValueメソッドで取得と設定が可能です。上記クラスを使ってName依存関係プロパティの値の取得と設定をするコード例を以下に示します。
var p = new Person();
Console.WriteLine(p.GetValue(Person.NameProperty));
p.SetValue(Person.NameProperty, "おおた");
Console.WriteLine(p.GetValue(Person.NameProperty));
実行すると、以下のような出力になります。
default name
おおた
GetValueメソッドとSetValueメソッドを使って依存関係プロパティの値の取得と設定が出来ますが、通常のプロパティの使用方法とかけ離れているため、通常は、以下のようなCLRのプロパティのラッパーを作成します。
publicclass Person : DependencyObject
{
publicstaticreadonly DependencyProperty NameProperty =
DependencyProperty.Register(
"Name", typeof(string), typeof(Person), new PropertyMetadata("default name")); publicstring Name
{
get { return (string)GetValue(NameProperty); }
set { SetValue(NameProperty, value); }
}
}
上記のプロパティを使うと使用する側のコードは自然なC#によるクラスを利用したコードになります。
var p = new Person();
Console.WriteLine(p.Name);
p.Name = "おおた";
Console.WriteLine(p.Name);
デフォルト値の設定
Personクラスの例で示したように、依存関係プロパティは、メタデータを使ってでデフォルト値の設定が出来ます。デフォルト値は、全てのクラスで同じインスタンスが使われます。このようにして、大量のインスタンスが生成されたときにメモリをデフォルト値によって無駄に使うことがないようになっています。その反面、List型などのような参照型の値の場合同じインスタンスを使うと不都合があるケースがあります。
例えば先ほどのPersonクラスにChildrenというList型の依存関係プロパティを追加してデフォルト値にList型のインスタンスを指定したとします。
publicclass Person : DependencyObject
{
publicstaticreadonly DependencyProperty ChildrenProperty =
DependencyProperty.Register(
"Children",
typeof(List<Person>),
typeof(Person),
new PropertyMetadata(new List<Person>())); public List<Person> Children
{
get { return (List<Person>)GetValue(ChildrenProperty); }
set { SetValue(ChildrenProperty, value); }
}
}
このようにすると、2つのPersonクラスのインスタンスを作った時に、Childrenプロパティの値が共有されて不都合がおきてしまいます。
var p1 = new Person();
var p2 = new Person();
p1.Children.Add(new Person());
p2.Children.Add(new Person());
Console.WriteLine("p1.Children.Count = {0}", p1.Children.Count);
Console.WriteLine("p2.Children.Count = {0}", p2.Children.Count);
このプログラムの実行結果はどちらも2が表示されてしまいます。このような問題を避けるためには、通常のプロパティと同じように、デフォルト値をコンストラクタで行う必要があります。
public Person()
{
this.Children = new List<Person>();
}
これで問題は起きなくなります。
値の変更の検出
依存関係プロパティのメタデータには、第二引数にプロパティの値に変更があったときに呼ばれるコールバックメソッドを指定することが出来ます。以下のように設定をします。
publicstaticreadonly DependencyProperty NameProperty =
DependencyProperty.Register(
"Name", typeof(string), typeof(Person), new PropertyMetadata(
"default name",
NamePropertyChanged)); privatestaticvoid NamePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
Console.WriteLine("Nameプロパティが{0}から{1}に変わりました", e.OldValue, e.NewValue);
}
DependencyPropertyChangedEventArgsのOldValueプロパティとNewValueプロパティで変更前、変更後の値の取得が可能です。プロパティの値が変わった時に何か処理をしたいときに使用します。注意点としては、staticメソッドで、値が変更されたインスタンスはメソッドの引数にDependencyObjectの形で渡されるという点です。値が変更されたインスタンスに何か操作をしたい場合は、引数で渡されたものをキャストして使用します。
値の矯正
依存関係プロパティには、値が有効範囲にあるかどうかを指定する方法があります。メタデータの第三引数にcoerceValueCallbackという引数を指定することで、値がプロパティにとって正しい範囲にあるかを検証する処理を追加することができます。以下にPersonクラスにAgeというプロパティを追加して、値の範囲が0~120であるように矯正する処理を設定している例を示します。
publicstaticreadonly DependencyProperty AgeProperty =
DependencyProperty.Register(
"Age",
typeof(int),
typeof(Person),
new PropertyMetadata(
0,
AgeChanged,
CoerceAgeValue));
privatestaticobject CoerceAgeValue(DependencyObject d, object baseValue)
{
var value = (int)baseValue;
if (value< 0)
{
return0;
}
if (value> 120)
{
return120;
}
returnvalue;
}
privatestaticvoid AgeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
Console.WriteLine("Ageプロパティが{0}から{1}に変わりました。", e.OldValue, e.NewValue);
}
publicint Age
{
get { return (int)GetValue(AgeProperty); }
set { SetValue(AgeProperty, value); }
}
CoerceAgeValueメソッドが値を矯正している処理になります。範囲外の値が設定された場合は、範囲内の値を返しています。この処理がどのように動くか示すためのコードを以下に示します。
var p = new Person();
p.Age = 10;
p.Age = -10;
p.Age = 150;
実行結果は以下のようになります。
Ageプロパティが0から10に変わりました。
Ageプロパティが10から0に変わりました。
Ageプロパティが0から120に変わりました。
-10を設定したのに0が設定されていることと、150を設定したのに120が設定されていることが確認できます。この値の矯正処理は、プロパティの変更時だけではなくDependencyObjectのCoerceValueメソッドに依存関係プロパティを渡すことでも呼び出すことができます。よく使われる例として、最大値(Maximum)と最小値(Minimum)を指定できるクラスで、このプロパティの値が変わった時にthis.CoerceValue(ValuePeoperty);のように値のプロパティを最大値と最小値の範囲内に矯正する処理を呼び出すといったケースがあります。
プロパティの妥当性検証
プロパティの値の矯正の他に、不正な値が設定されたときに例外をスローする検証処理を記述する方法も提供されています。これはメタデータではなく、Registerメソッドの第5引数として指定します。値を受け取り、その値が妥当な値の場合はtrueを返し、不正な値の場合はfalseを返すようにします。
AgeプロパティがMinValue、MaxValueの場合に不正な値とするコード例を以下に示します。
publicstaticreadonly DependencyProperty AgeProperty =
DependencyProperty.Register(
"Age",
typeof(int),
typeof(Person),
new PropertyMetadata(
0,
AgeChanged,
CoerceAgeValue),
ValidateAgeValue);
privatestaticbool ValidateAgeValue(objectvalue)
{
int age = (int)value;
return age != int.MaxValue && age != int.MinValue;
}
このようにすると、以下のようにMaxValueやMinValueを設定するとArgumentExceptionの例外がスローされます。
var p = new Person();
try
{
p.Age = int.MinValue;
}
catch (ArgumentException ex)
{
Console.WriteLine(ex);
}
過去記事