かずきのBlog@hatena

すきな言語は C# + XAML の組み合わせ。Azure Functions も好き。最近は Go 言語勉強中。日本マイクロソフトで働いていますが、ここに書いていることは個人的なメモなので会社の公式見解ではありません。

WPF4.5入門 その42 「WPFのプロパティシステム」

DispatcherObjectの1段下に継承階層をおりると、DependencyObjectというクラスになります。DependencyObjectは、WPFで使われた独自のプロパティシステムを実装しています。この独自のプロパティシステムのことを、依存関係プロパティと添付プロパティといいます。

存関係プロパティ

依存関係プロパティは、通常のCLRのプロパティと比べて、以下の機能を追加で提供します。 - リソースからの値の取得 - データバインディングへの対応 - スタイルによる値の設定 - アニメーション - オーバーライド可能なメタデータ - 親子関係にあるインスタンスでのプロパティ値の継承

依存関係プロパティの定義方法

依存関係プロパティは、DependencyObjectを直接、または間接的に継承したクラスで定義可能です。定義方法は、DependencyPropertyクラスのRegisterメソッドを使用します。DependencyObjectを継承したPersonクラスにNameという依存関係プロパティを定義する方法を以下に示します。

public class Person : DependencyObject
{

    public static readonly DependencyProperty NameProperty =
        DependencyProperty.Register(
            "Name", // プロパティ名を指定
            typeof(string), // プロパティの型を指定
            typeof(Person), // プロパティを所有する型を指定
            new PropertyMetadata("default name")); // メタデータを指定。ここではデフォルト値を設定してる
}

Registerメソッドを使いDependencyPropertyクラスのインスタンスを作成します。作成したインスタンスはpublic static readonlyのフィールドに「プロパティ名Property」の命名規約で格納します。DependencyPropertyの値は、DependencyObjectクラスに定義されているGetValue、SetValueメソッドで取得と設定が可能です。上記クラスを使ってName依存関係プロパティの値の取得と設定をするコード例を以下に示します。

// GetValue, SetValueの使用例
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のプロパティのラッパーを作成します。

public class Person : DependencyObject
{

    public static readonly DependencyProperty NameProperty =
        DependencyProperty.Register(
            "Name", // プロパティ名を指定
            typeof(string), // プロパティの型を指定
            typeof(Person), // プロパティを所有する型を指定
            new PropertyMetadata("default name")); // メタデータを指定。ここではデフォルト値を設定してる

    // 依存関係プロパティのCLRのプロパティのラッパー
    public string 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型のインスタンスを指定したとします。

public class Person : DependencyObject
{
    // Nameプロパティは省略

    public static readonly 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プロパティの値が共有されて不都合がおきてしまいます。

// 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>();
}

これで問題は起きなくなります。

値の変更の検出

依存関係プロパティのメタデータには、第二引数にプロパティの値に変更があったときに呼ばれるコールバックメソッドを指定することが出来ます。以下のように設定をします。

public static readonly DependencyProperty NameProperty =
    DependencyProperty.Register(
        "Name", // プロパティ名を指定
        typeof(string), // プロパティの型を指定
        typeof(Person), // プロパティを所有する型を指定
        new PropertyMetadata(
            "default name", // デフォルト値の設定
            NamePropertyChanged)); // プロパティの変更時に呼ばれるコールバックの設定

private static void NamePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
    Console.WriteLine("Nameプロパティが{0}から{1}に変わりました", e.OldValue, e.NewValue);
}

DependencyPropertyChangedEventArgsのOldValueプロパティとNewValueプロパティで変更前、変更後の値の取得が可能です。プロパティの値が変わった時に何か処理をしたいときに使用します。注意点としては、staticメソッドで、値が変更されたインスタンスはメソッドの引数にDependencyObjectの形で渡されるという点です。値が変更されたインスタンスに何か操作をしたい場合は、引数で渡されたものをキャストして使用します。

値の矯正

依存関係プロパティには、値が有効範囲にあるかどうかを指定する方法があります。メタデータの第三引数にcoerceValueCallbackという引数を指定することで、値がプロパティにとって正しい範囲にあるかを検証する処理を追加することができます。以下にPersonクラスにAgeというプロパティを追加して、値の範囲が0~120であるように矯正する処理を設定している例を示します。

public static readonly DependencyProperty AgeProperty =
    DependencyProperty.Register(
        "Age", 
        typeof(int), 
        typeof(Person), 
        new PropertyMetadata(
            0,
            AgeChanged,
            CoerceAgeValue));

private static object CoerceAgeValue(DependencyObject d, object baseValue)
{
    // 年齢は0-120の間
    var value = (int)baseValue;
    if (value < 0)
    {
        return 0;
    }
    if (value > 120)
    {
        return 120;
    }
    return value;
}

private static void AgeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
    Console.WriteLine("Ageプロパティが{0}から{1}に変わりました。", e.OldValue, e.NewValue);
}


public int 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の場合に不正な値とするコード例を以下に示します。

public static readonly DependencyProperty AgeProperty =
    DependencyProperty.Register(
        "Age", 
        typeof(int), 
        typeof(Person), 
        new PropertyMetadata(
            0,
            AgeChanged,
            CoerceAgeValue),
        ValidateAgeValue);

private static bool ValidateAgeValue(object value)
{
    // MinValueとMaxValueはやりすぎだろ
    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);
}

過去記事