今回は添付プロパティの再登場です。
この間取り上げた
[WPF] WPF入門 ~レイアウトシステム [添付プロパティ]~ではXAMLにおける添付プロパティの簡単な使用方法のみ取り上げました。
ここ数回に分けて依存関係プロパティについて取り上げた事と、添付プロパティが依存関係プロパティととても関わりの深い物になっているので再度ここで取り上げたいと思います。
それでは以前も書きましたが、添付プロパティのMSDNの説明を見てみましょう。
添付プロパティとは、Extensible Application Markup Language (XAML) によって定義される概念です。添付プロパティの目的は、任意のオブジェクトに対して設定可能な一種のグローバル プロパティとして使用することです。Windows Presentation Foundation (WPF) では一般に、添付プロパティは、従来のプロパティ "ラッパー" を持たない特殊な形式の依存関係プロパティとして定義されます。
また、添付プロパティを使用する理由としてこのように説明されています。
添付プロパティの目的の 1 つは、実際には親要素で定義されるプロパティについて、子要素がそれぞれ別の値を指定できるようにすることです。このシナリオは、たとえば、子要素をユーザー インターフェイス (UI) にどのように表示するかを子要素から親要素に通知させるという状況に応用されます。DockPanel.Dock プロパティがその一例です。DockPanel.Dock プロパティが添付プロパティとして作成されるのは、このプロパティが DockPanel 自体ではなく DockPanel に含まれる要素に対して設定するように設計されているためです。DockPanel クラスでは、DockProperty という名前の静的 DependencyProperty フィールドが定義されており、添付プロパティのパブリック アクセサとして GetDock メソッドと SetDock メソッドが定義されています。
説明からも解るとおりに、添付プロパティとは子要素から親要素に値の設定を通知させる為に使用されます。
また、添付プロパティは依存関係プロパティとして定義しなくても大丈夫ですが、一般的には添付プロパティは依存関係プロパティとして定義されます。
それではまず、添付プロパティの実装例を見てみましょう。
- VB -
Public Class AttachedPropertyObject
Inherits DependencyObject
Public Shared ReadOnly IsAttachedProperty As DependencyProperty
Shared Sub New()
IsAttachedProperty = DependencyProperty.RegisterAttached( _
"IsAttached", GetType(Boolean), GetType(AttachedPropertyObject), New FrameworkPropertyMetadata(False))
End Sub
Public Shared Function GetIsAttached(ByVal target As DependencyObject) As Boolean
Return CBool(target.GetValue(IsAttachedProperty))
End Function
Public Shared Sub SetIsAttached(ByVal target As DependencyObject, ByVal value As Boolean)
target.SetValue(IsAttachedProperty, value)
End Sub
End Class
- C# -
public class AttachedPropertyObject : DependencyObject
{
public static readonly DependencyProperty IsAttachedProperty;
static AttachedPropertyObject()
{
IsAttachedProperty = DependencyProperty.RegisterAttached(
"IsAttached", typeof(bool), typeof(AttachedPropertyObject), new FrameworkPropertyMetadata(false));
}
public static bool GetIsAttached(DependencyObject target)
{
return (bool)target.GetValue(IsAttachedProperty);
}
public static void SetIsAttached(DependencyObject target, bool value)
{
target.SetValue(IsAttachedProperty, value);
}
}
添付プロパティは依存関係プロパティの実装と何となく似ています。添付プロパティの実装において幾つか決まり事があります。
- Publicでかつ静的なDependencyProperty型の変数を使用する
- 添付プロパティ名のサフィックス(接頭辞)に "Property" を付ける
- 変数宣言時に初期化するか、静的コンストラクタにて初期化する(今回は静的コンストラクタを使用)
- DependencyPropertyクラスの静的メソッドのRegisterAttachedメソッドを使用し、添付プロパティを登録する
- CLRプロパティラッパーを使用せず、添付プロパティのアクセサとしてGetとSetを先頭に付けた静的メソッドを実装する
以上のように添付プロパティの実装を行う事が出来ます。
ここで少し余談ですが、添付プロパティ登録時に使用しているRegisterAttachedメソッドは添付プロパティの登録以外でも依存関係プロパティの登録時にも使用されます。
依存関係プロパティでは「プロパティ値の継承」を行う必要のある依存関係プロパティの登録に使用します。MSDNでもこのように説明されています。
Register の代わりに RegisterAttached を使用して依存関係プロパティを登録する 1 つのシナリオは、プロパティ値の継承をサポートすることです。依存関係プロパティを公開するプロパティ ラッパー アクセサをクラスが定義する場合や、Get* および Set* 静的メソッドを公開して、実際の添付プロパティのサポート アクセサを提供しない場合でも、RegisterAttached を使用して、値を継承する依存関係プロパティを登録する必要があります。プロパティ値の継承は、非添付依存関係プロパティのために機能しているように見えることがありますが、ランタイム ツリーでの特定の要素の境界を介する非添付プロパティの継承動作は定義されていません。プロパティを添付として登録すると、添付プロパティは事実上プロパティ システムに対するグローバル プロパティとなり、プロパティ値の継承が確実に要素ツリーのすべての境界を介して機能するようになります。メタデータで Inherits を指定した場合は、必ず RegisterAttached を使用してプロパティを登録します。
少し道がそれましたが、添付プロパティについて戻ります。
XAMLを使用した添付プロパティではどのように添付プロパティが使用され、親子間の連携が行われているか良くわかりません。
添付プロパティの説明では子要素が親要素にプロパティ変更の通知を行うとなっていますが、実際コード例を見て頂くと解かるとおりに添付プロパティを設定したオブジェクトに対し添付プロパティ値の設定を行っています。
「子要素が親要素に通知をする」の言葉では矛盾しているように感じますが、その理由は以下の通りになります。
- 子要素が親要素に通知をするといった表現は、添付プロパティの使用されるケースが多いレイアウトをサポートしたコントロール作成についての表現である
- 添付プロパティは、呼んで文字の如く「ある要素(クラス等)に対してグローバルなプロパティを添付する」である
- 添付プロパティの実装は依存関係プロパティと変わらず、WPFプロパティシステムを利用する事で初めて有効なサービスとなる
上記の理由を詳しく見て行きたいと思います。
レイアウトをサポートしているコントロールの添付プロパティの使用方法が、あたかも「子要素が親要素に通知を行っている」ように見えるだけであり、実際は添付プロパティを設定した要素自身がWPFプロパティシステムによりグローバルなプロパティを添付され、そのプロパティ値が添付プロパティを実装している要素によって取得され、レンダリングに関わる計算に使用されているのです。よって親子関係がない物であっても添付プロパティを使用する事は可能なのです。
上記コード例でもそうであるように、添付プロパティのアクセサメソッド内ではDependencyObjectクラスのGetValueメソッドとSetValueメソッドが使用されています。それはWPFプロパティシステムを使用する為に、DependencyPropertyクラスのRegisterAttachedメソッドによって添付プロパティが登録され、グローバルなプロパティとして使用される必要がある為です。
添付プロパティは実装ルールでの「GetとSetを先頭に付けた静的メソッドを実装」を行うだけで、VisualStudioのWPFデザイナでは「添付プロパティもどき」的にインテリセンスで機能しますが、それだけでは添付プロパティとしての機能がありません。
よって「添付プロパティはDependencyPropertyクラスを使用し依存関係プロパティのような実装をするべき」になります。
まずは親子間が無くてもプロパティが添付される例を見てみましょう。
- VB -
Public Class Program
Public Shared Sub Main()
Dim button1 As New Button()
AttachedPropertyObject.SetIsAttached(button1, True)
Console.WriteLine("button1に添付されたプロパティ値 = {0}", AttachedPropertyObject.GetIsAttached(button1))
AttachedPropertyObject.SetIsAttached(button1, False)
Console.WriteLine("button1に添付されたプロパティ値 = {0}", AttachedPropertyObject.GetIsAttached(button1))
Console.WriteLine()
Dim button2 As New Button()
AttachedPropertyObject.SetIsAttached(button2, True)
Console.WriteLine("button2に添付されたプロパティ値 = {0}", AttachedPropertyObject.GetIsAttached(button2))
AttachedPropertyObject.SetIsAttached(button2, False)
Console.WriteLine("button2に添付されたプロパティ値 = {0}", AttachedPropertyObject.GetIsAttached(button2))
End Sub
End Class
- C# -
public class Program
{
[STAThread]
public static void Main()
{
Button button1 = new Button();
AttachedPropertyObject.SetIsAttached(button1, true);
Console.WriteLine("button1に添付されたプロパティ値 = {0}", AttachedPropertyObject.GetIsAttached(button1));
AttachedPropertyObject.SetIsAttached(button1, false);
Console.WriteLine("button1に添付されたプロパティ値 = {0}", AttachedPropertyObject.GetIsAttached(button1));
Console.WriteLine();
Button button2 = new Button();
AttachedPropertyObject.SetIsAttached(button2, true);
Console.WriteLine("button2に添付されたプロパティ値 = {0}", AttachedPropertyObject.GetIsAttached(button2));
AttachedPropertyObject.SetIsAttached(button2, false);
Console.WriteLine("button2に添付されたプロパティ値 = {0}", AttachedPropertyObject.GetIsAttached(button2));
}
}
上記例を実行すると下記のような出力結果が出ます。
button1に添付されたプロパティ値 = True
button1に添付されたプロパティ値 = False
button2に添付されたプロパティ値 = True
button2に添付されたプロパティ値 = False
上記例では添付プロパティに値を設定しても、設定したオブジェクトに対しては特に何も行っていませんが、このように、親子間の関係がなくとも「グローバルなプロパティ値」として各オブジェクトにプロパティ値が添付されている事がご理解頂けたかと思います。
ではどのようにして「子要素が親要素にプロパティ値設定を通知しているように見せれるのか?」を、取り上げたクラスを少し変更してみます。
- VB -
Public Class AttachedPropertyObject
Inherits DependencyObject
Public Shared ReadOnly IsAttachedProperty As DependencyProperty
Shared Sub New()
Dim metaData As New FrameworkPropertyMetadata()
metaData.DefaultValue = False
metaData.PropertyChangedCallback = New PropertyChangedCallback(AddressOf OnPropertyChangedCallback)
IsAttachedProperty = DependencyProperty.RegisterAttached( _
"IsAttached", GetType(Boolean), GetType(AttachedPropertyObject), metaData)
End Sub
Private Shared Sub OnPropertyChangedCallback(ByVal d As DependencyObject, ByVal e As DependencyPropertyChangedEventArgs)
Dim target As Button = TryCast(d, Button)
If target Is Nothing Then Return
If CBool(e.NewValue) Then
target.Tag = "Attached"
Else
target.Tag = "Unattached"
End If
End Sub
Public Shared Function GetIsAttached(ByVal target As Button) As Boolean
Return CBool(target.GetValue(IsAttachedProperty))
End Function
Public Shared Sub SetIsAttached(ByVal target As Button, ByVal value As Boolean)
target.SetValue(IsAttachedProperty, value)
End Sub
End Class
- C# -
public class AttachedPropertyObject : DependencyObject
{
public static readonly DependencyProperty IsAttachedProperty;
static AttachedPropertyObject()
{
FrameworkPropertyMetadata metaData = new FrameworkPropertyMetadata();
metaData.DefaultValue = false;
metaData.PropertyChangedCallback = new PropertyChangedCallback(OnPropertyChangedCallback);
IsAttachedProperty = DependencyProperty.RegisterAttached(
"IsAttached", typeof(bool), typeof(AttachedPropertyObject), metaData);
}
private static void OnPropertyChangedCallback(DependencyObject d,DependencyPropertyChangedEventArgs e)
{
Button target = d as Button;
if (target == null) return;
if ((bool)e.NewValue)
{
target.Tag = "Attached";
}
else
{
target.Tag = "Unattached";
}
}
public static bool GetIsAttached(Button target)
{
return (bool)target.GetValue(IsAttachedProperty);
}
public static void SetIsAttached(Button target, bool value)
{
target.SetValue(IsAttachedProperty, value);
}
}
この変更した添付プロパティ実装クラスを使用した簡単な使用例を見てみましょう。
- VB -
Public Class Program
Public Shared Sub Main()
Dim button1 As New Button()
Console.WriteLine("Button.Tag = {0}", If(button1.Tag Is Nothing, "添付プロパティが設定されていません。", button1.Tag.ToString()))
AttachedPropertyObject.SetIsAttached(button1, True)
Console.WriteLine("Button.Tag = {0}", If(button1.Tag Is Nothing, "添付プロパティが設定されていません。", button1.Tag.ToString()))
AttachedPropertyObject.SetIsAttached(button1, False)
Console.WriteLine("Button.Tag = {0}", If(button1.Tag Is Nothing, "添付プロパティが設定されていません。", button1.Tag.ToString()))
End Sub
End Class
- C# -
public class Program
{
[STAThread]
public static void Main()
{
Button button1 = new Button();
Console.WriteLine("Button.Tag = {0}", (button1.Tag == null ? "添付プロパティが設定されていません。" : button1.Tag.ToString()));
AttachedPropertyObject.SetIsAttached(button1, true);
Console.WriteLine("Button.Tag = {0}", (button1.Tag == null ? "添付プロパティが設定されていません。" : button1.Tag.ToString()));
AttachedPropertyObject.SetIsAttached(button1, false);
Console.WriteLine("Button.Tag = {0}", (button1.Tag == null ? "添付プロパティが設定されていません。" : button1.Tag.ToString()));
}
}
これを実行するとこのように出力されます。
Button.Tag = 添付プロパティが設定されていません。
Button.Tag = Attached
Button.Tag = Unattached
今回はレイアウト変更を行うような例ではありませんが、Buttonオブジェクトが添付プロパティを設定した事で、添付プロパティ実装クラスがButtonクラスに何かしらの影響を与えている事がご理解頂けたかと思います。
このような影響を与える添付プロパティを実装する事で、「DockPanelクラスのDockプロパティ」や「CanvasクラスのTopやLeftプロパティ」のような他のレイアウトを変更するクラスの作成が行えます。
レイアウトの変更を行うカスタムクラスについては、また別の機会に取り上げたいと思います。
今回取り上げた添付プロパティは、依存関係プロパティと同様にとても重要なプロパティシステムでありますので、是非この機会に添付プロパティの理解を深めて頂けたらと思います。
to be contine・・・