[WPF] WPF入門 ~依存関係プロパティ ③~

投稿日 : 2009年3月20日 13:17
依存関係プロパティの3回目です。
前回では「依存関係プロパティで何が行えるのか?」について取り上げました。
基本的な依存関係プロパティの使用方法・実装方法については前回で取り上げましたが、依存関係プロパティでは下記にあげる依存関係プロパティの実装も行えます。
  • 読み取り専用の依存関係プロパティの実装
  • コレクション型依存関係プロパティの実装
ではそれぞれの実装方法を見ていきましょう。

読み取り専用の依存関係プロパティ
読み取り専用の依存関係プロパティは、外部による値の設定により不都合がある場合に使用します。
読取専用の依存関係プロパティは通常の依存関係プロパティとは以下の点が異なります。
  • WPFプロパティシステムに登録する場合、DependencyPropertyクラスのRegisterメソッドでは無く、RegisterReadOnlyメソッドを使用し登録する
  • CLRプロパティでラッパーを実装する場合、Set実装をしない(又はスコープレベルを下げる)ようにして、読み取り専用状態の矛盾がないようにする
  • WPFプロパティシステムに登録し返されるオブジェクトはDependencyPropertyクラスでは無く、DependencyPropertyKeyクラスであり、外部に公開をしない(スコープレベルを下げる)必要がある
このようなルールに基づき読み取り専用の依存関係プロパティを実装します。
ではコードの例を見てみましょう。

- VB -
Public Class ReadonlyDependencyObject
    Inherits DependencyObject
    Friend Shared ReadOnly CaptionPropertyKey As DependencyPropertyKey
    Public Shared ReadOnly CaptionProperty As DependencyProperty
 
    Shared Sub New()
        CaptionPropertyKey = DependencyProperty.RegisterReadOnly( _
        "Caption", GetType(String), GetType(ReadonlyDependencyObject), New FrameworkPropertyMetadata())
 
        CaptionProperty = CaptionPropertyKey.DependencyProperty
    End Sub
 
    Public Property Caption() As String
        Get
            Return CStr(GetValue(CaptionProperty))
        End Get
        Protected Set(ByVal value As String)
            SetValue(CaptionPropertyKey, value)
        End Set
    End Property
End Class
- C# -
public class ReadonlyDependencyObject : DependencyObject
{
    internal static readonly DependencyPropertyKey CaptionPropertyKey;
    public static readonly DependencyProperty CaptionProperty;
 
    static ReadonlyDependencyObject()
    {
        CaptionPropertyKey = DependencyProperty.RegisterReadOnly(
            "Caption", typeof(string), typeof(ReadonlyDependencyObject), new FrameworkPropertyMetadata());
 
        CaptionProperty = CaptionPropertyKey.DependencyProperty;
    }
 
    public string Caption
    {
        get { return GetValue(CaptionProperty) as string;}
        protected set { SetValue(CaptionPropertyKey,value); }
    }
}

ここで注目して頂きたい部分は以下の通りになります。
  1. 読取専用の依存関係プロパティの値の取得は、DependencyPropertyKeyクラスのDependencyPropertyプロパティ経由にて返却する
  2. CLRプロパティより有効なスコープより値の設定を行う場合は、DependencyObjectクラスのSetValueメソッドの引数にDependencyPropertyKeyクラスを使用し値を設定する
1.については、有効な依存関係プロパティが返却されるようにする為です。また、DependencyObjectクラスのGetValueメソッドはDependencyPropertyクラスを引数に取る為でもあります。
2.については、読取専用の依存関係プロパティを管理しているのはDependencyPropertyKeyクラスである為です。

このようにして読取専用の依存関係プロパティの実装は行えます。

コレクション型の依存関係プロパティ
依存関係プロパティではコレクション型の依存関係プロパティを実装する事も出来ます。
今回取り上げるのは、コレクション型の依存関係プロパティの実装方法では無く、「コレクション型の依存関係プロパティの実装する上での注意点」になります。

まずはコレクション型の依存関係プロパティを実装する例を見てみましょう。

- VB -
Public Class CollectionDependencyPropertySample
    Inherits DependencyObject
    Private Shared ReadOnly CaptionsPropertyKey As DependencyPropertyKey
    Public Shared ReadOnly CaptionsProperty As DependencyProperty
 
    Shared Sub New()
        Dim metaData As New FrameworkPropertyMetadata()
        metaData.DefaultValue = New List(Of String)()
 
        CaptionsPropertyKey = DependencyProperty.RegisterReadOnly( _
        "Captions", GetType(List(Of String)), GetType(CollectionDependencyPropertySample), metaData)
 
        CaptionsProperty = CaptionsPropertyKey.DependencyProperty
    End Sub
 
    Public ReadOnly Property Captions() As List(Of String)
        Get
            Return DirectCast(GetValue(CaptionsProperty), List(Of String))
        End Get
    End Property
End Class
- C# -
public class CollectionDependencyPropertySample : DependencyObject
{
    private static readonly DependencyPropertyKey CaptionsPropertyKey;
    public static readonly DependencyProperty CaptionsProperty;
 
    static CollectionDependencyPropertySample()
    {
        FrameworkPropertyMetadata metaData = new FrameworkPropertyMetadata();
        metaData.DefaultValue = new List<string>();
 
        CaptionsPropertyKey = DependencyProperty.RegisterReadOnly(
            "Captions", typeof(List<string>), typeof(CollectionDependencyPropertySample), metaData);
 
        CaptionsProperty = CaptionsPropertyKey.DependencyProperty;
    }
 
    public List<string> Captions
    {
        get { return (List<string>)GetValue(CaptionsProperty); }
    }
}

一般的にコレクション型のプロパティは、コレクションオブジェクト自体の設定を目的としている訳ではなく、コレクションに格納されている要素群を操作可能にする為に実装するので、読取専用な依存関係プロパティとして定義します。
また、コレクション型の依存関係プロパティの既定値は、要素が格納されていないコレクションを設定する為、既定値にインスタンス化したコレクションオブジェクトを設定します。
ここまではなんら変哲も無く実装出来ていますが、上記クラスを使用した簡単な使用例を見てみましょう。今回解り易くする為に、コンソールアプリケーションにて行っています。

- VB -
Public Class Program
    Shared Sub Main()
        Dim obj1 As New CollectionDependencyPropertySample()
        Dim obj2 As New CollectionDependencyPropertySample()
 
        Console.WriteLine("obj1の要素数 = {0}、 obj1の1番目の要素 = {1}", obj1.Captions.Count, If(obj1.Captions.Count = 0, "要素なし", obj1.Captions(0)))
        Console.WriteLine("obj2の要素数 = {0}、 obj2の1番目の要素 = {1}", obj2.Captions.Count, If(obj2.Captions.Count = 0, "要素なし", obj2.Captions(0)))
        Console.WriteLine()
 
        obj1.Captions.Add("First Value")
        Console.WriteLine("obj1の要素追加")
 
        Console.WriteLine("obj1の要素数 = {0}、 obj1の1番目の要素 = {1}", obj1.Captions.Count, If(obj1.Captions.Count = 0, "要素なし", obj1.Captions(0)))
        Console.WriteLine("obj2の要素数 = {0}、 obj2の1番目の要素 = {1}", obj2.Captions.Count, If(obj2.Captions.Count = 0, "要素なし", obj2.Captions(0)))
        Console.WriteLine()
 
        obj2.Captions.Add("Second Value")
        Console.WriteLine("obj2の要素追加")
 
        Console.WriteLine("obj1の要素数 = {0}、 obj1の1番目の要素 = {1}", obj1.Captions.Count, If(obj1.Captions.Count = 0, "要素なし", obj1.Captions(0)))
        Console.WriteLine("obj2の要素数 = {0}、 obj2の1番目の要素 = {1}", obj2.Captions.Count, If(obj2.Captions.Count = 0, "要素なし", obj2.Captions(0)))
    End Sub
End Class
- C# -
public class Program
{
    public static void Main()
    {
        CollectionDependencyPropertySample obj1 = new CollectionDependencyPropertySample();
        CollectionDependencyPropertySample obj2 = new CollectionDependencyPropertySample();
 
        Console.WriteLine("obj1の要素数 = {0}、 obj1の1番目の要素 = {1}", obj1.Captions.Count, (obj1.Captions.Count == 0 ? "要素なし" : obj1.Captions[0]));
        Console.WriteLine("obj2の要素数 = {0}、 obj2の1番目の要素 = {1}", obj2.Captions.Count, (obj2.Captions.Count == 0 ? "要素なし" : obj2.Captions[0]));
        Console.WriteLine();
 
        obj1.Captions.Add("First Value");
        Console.WriteLine("obj1の要素追加");
 
        Console.WriteLine("obj1の要素数 = {0}、 obj1の1番目の要素 = {1}", obj1.Captions.Count, (obj1.Captions.Count == 0 ? "要素なし" : obj1.Captions[0]));
        Console.WriteLine("obj2の要素数 = {0}、 obj2の1番目の要素 = {1}", obj2.Captions.Count, (obj2.Captions.Count == 0 ? "要素なし" : obj2.Captions[0]));
        Console.WriteLine();
 
        obj2.Captions.Add("Second Value");
        Console.WriteLine("obj2の要素追加");
 
        Console.WriteLine("obj1の要素数 = {0}、 obj1の1番目の要素 = {1}", obj1.Captions.Count, (obj1.Captions.Count == 0 ? "要素なし" : obj1.Captions[0]));
        Console.WriteLine("obj2の要素数 = {0}、 obj2の1番目の要素 = {1}", obj2.Captions.Count, (obj2.Captions.Count == 0 ? "要素なし" : obj2.Captions[0]));
    }
}

実行すると以下の結果が出力されます。

obj1の要素数 = 0、 obj1の1番目の要素 = 要素なし
obj2の要素数 = 0、 obj2の1番目の要素 = 要素なし

obj1の要素追加
obj1の要素数 = 1、 obj1の1番目の要素 = First Value
obj2の要素数 = 1、 obj2の1番目の要素 = First Value

obj2の要素追加
obj1の要素数 = 2、 obj1の1番目の要素 = First Value
obj2の要素数 = 2、 obj2の1番目の要素 = First Value

出力結果がおかしいですね。
インスタンス化直後のコンソール出力ではobj1とobj2とも正しく既定値が働いているように見えます。
ですがobj1のCaptionsプロパティに要素を追加した後の2回目のコンソール出力では、異なるインスタンス間でコレクション要素値自体を共有してしまっています。
その次のobj2のCaptionsプロパティに要素を追加しても同様の結果がコンソールに出力されています。
なぜこのような結果になってしまったのでしょうか?

コレクション型以外の依存関係プロパティの場合では、WPFプロパティシステムに依存関係プロパティを登録する際に、既定値(今までは"DefalutValue"を指定していた)を設定していました。
既定値は依存関係プロパティに対するメタデータであるので、すべてのインスタンスで共通されるべき値であります。
あくまでも既定値が共有されるだけであり、実際の依存関係プロパティ値が共通される訳ではないので、コレクション型以外の型の値を意識する事はありませんでした。
ですが、コレクション型の依存関係プロパティの場合は、依存関係プロパティとして公開するのはあくまでも「コレクション内の要素群」である為、既定値として設定するべきは「コレクション要素群を格納する初期化された器」であります。これは言い換えると「要素が空のコレクションインスタンス」を既定値として設定すると受け取れます。
上記コレクション型の依存関係プロパティの実装例では確かにその様に定義されていますが、依存関係プロパティは「変数定義に登録」するか「静的コンストラクタ内で登録」するので、異なるインスタンス間であっても既定値は共通される物になります。
では異なるインスタンス間で、既定値が「要素が空のコレクションインスタンスである事」を実現するにはどのようにすれば良いのでしょうか?
ではその実装例を見てみましょう。

- VB -
Public Class CollectionDependencyPropertySample
    Inherits DependencyObject
    Private Shared ReadOnly CaptionsPropertyKey As DependencyPropertyKey
    Public Shared ReadOnly CaptionsProperty As DependencyProperty
 
    Public Sub New()
        MyBase.New()
        SetValue(CaptionsPropertyKey, New List(Of String)())
    End Sub
 
    Shared Sub New()
        Dim metaData As New FrameworkPropertyMetadata()
        metaData.DefaultValue = New List(Of String)()
 
        CaptionsPropertyKey = DependencyProperty.RegisterReadOnly( _
        "Captions", GetType(List(Of String)), GetType(CollectionDependencyPropertySample), metaData)
 
        CaptionsProperty = CaptionsPropertyKey.DependencyProperty
    End Sub
 
    Public ReadOnly Property Captions() As List(Of String)
        Get
            Return DirectCast(GetValue(CaptionsProperty), List(Of String))
        End Get
    End Property
End Class
- C# -
public class CollectionDependencyPropertySample : DependencyObject
{
    private static readonly DependencyPropertyKey CaptionsPropertyKey;
    public static readonly DependencyProperty CaptionsProperty;
 
    public CollectionDependencyPropertySample() : base()
    {
        SetValue(CaptionsPropertyKey, new List<string>());
    }
 
    static CollectionDependencyPropertySample()
    {
        FrameworkPropertyMetadata metaData = new FrameworkPropertyMetadata();
        metaData.DefaultValue = new List<string>();
 
        CaptionsPropertyKey = DependencyProperty.RegisterReadOnly(
            "Captions", typeof(List<string>), typeof(CollectionDependencyPropertySample), metaData);
 
        CaptionsProperty = CaptionsPropertyKey.DependencyProperty;
    }
 
    public List<string> Captions
    {
        get { return (List<string>)GetValue(CaptionsProperty); }
    }
}

上記例のように、クラスコンストラクタ内で依存関係プロパティの値を一意のインスタンスにする為に初期化します。
コレクション型の依存関係プロパティは読取専用の依存関係プロパティとして定義しますので、クラス内だけでアクセス出来るDependencyPropertyKeyオブジェクトを使用して、SetValueメソッドにてリセットします。
この実装を使用した実行結果を見てみましょう。

obj1の要素数 = 0、 obj1の1番目の要素 = 要素なし
obj2の要素数 = 0、 obj2の1番目の要素 = 要素なし

obj1の要素追加
obj1の要素数 = 1、 obj1の1番目の要素 = First Value
obj2の要素数 = 0、 obj2の1番目の要素 = 要素なし

obj2の要素追加
obj1の要素数 = 1、 obj1の1番目の要素 = First Value
obj2の要素数 = 1、 obj2の1番目の要素 = Second Value

このようにコレクション型の依存関係プロパティを実装する場合は、既定値の設定に注意しなければならない事があるのが理解頂けたかと思います。
カスタムコントロールなどでは子要素を格納する依存関係プロパティとしてコレクション型が多々使用されます。
コレクション型の依存関係プロパティを実装する場合は、「既定値の設定による依存関係プロパティ値の共有」には十分気をつけましょう。

今回は「読取専用の依存関係プロパティ」「コレクション型の依存関係プロパティ」を取り上げました。

to be continue・・・

コメントの入力
タイトル
名前
Url
コメント