カスタム依存関係プロパティ (WPF .NET)

Windows Presentation Foundation (WPF) のアプリケーション開発者とコンポーネント作成者は、カスタム依存関係プロパティを作成して、プロパティの機能を拡張することができます。 共通言語ランタイム (CLR) のプロパティとは異なり、依存関係プロパティでは、スタイル設定、データ バインディング、継承、アニメーション、および既定値のサポートを追加します。 BackgroundWidth、および Text は、WPF クラスの既存の依存関係プロパティの例です。 この記事では、カスタム依存関係プロパティを実装する方法について説明し、パフォーマンス、使いやすさ、および汎用性を向上させるためのオプションを示します。

重要

.NET 7 と .NET 6 用のデスクトップ ガイド ドキュメントは作成中です。

必須コンポーネント

この記事では、依存関係プロパティの基本的な知識と、依存関係プロパティの概要に関する記事を参照済みであることを前提としています。 この記事の例について理解するには、Extensible Application Markup Language (XAML) を使い慣れていて、WPF アプリケーションの記述方法を理解していると役に立ちます。

依存関係プロパティ識別子

依存関係プロパティは、Register または RegisterReadOnly を呼び出すことによって、WPF プロパティ システムに登録されるプロパティです。 Register メソッドから、依存関係プロパティの登録済みの名前と特性を保持する DependencyProperty インスタンスが返されます。 DependencyProperty インスタンスを "依存関係プロパティ識別子" と呼ばれる静的な読み取り専用フィールドに割り当てます。名前は、慣例により <property name>Property です。 たとえば、Background プロパティの識別子フィールドは常に BackgroundProperty です。

依存関係プロパティ識別子は、プライベート フィールドを持つプロパティをバッキングする標準パターンではなく、プロパティ値を取得または設定するためのバッキング フィールドとして使用されます。 識別子は、プロパティ システムから使用されるだけでなく、XAML プロセッサから使用されることもあり、コード (および場合によっては外部コード) で、識別子を使用して依存関係プロパティにアクセスすることができます。

依存関係プロパティは、DependencyObject 型から派生したクラスにのみ適用できます。 DependencyObject は WPF クラス階層のルートに近いため、ほとんどの WPF クラスで依存関係プロパティをサポートしています。 依存関係プロパティと、それらを説明するために使用される用語と規則の詳細については、「依存関係プロパティの概要」を参照してください。

依存関係プロパティ ラッパー

アタッチされたプロパティではない WPF 依存関係プロパティは、get および set アクセサーを実装する CLR ラッパーによって公開されます。 プロパティ ラッパーを使用すると、依存関係プロパティのコンシューマーは、他の CLR プロパティと同様に、依存関係プロパティの値を取得または設定できます。 get および set アクセサーは、DependencyObject.GetValueDependencyObject.SetValue の呼び出しを介して基になるプロパティ システムとやりとりし、依存関係プロパティ識別子をパラメーターとして渡します。 通常、依存関係プロパティのコンシューマーは、GetValue または SetValue を直接呼び出しませんが、カスタム依存関係プロパティを実装する場合は、ラッパーでこれらのメソッドを使用します。

依存関係プロパティを実装するタイミング

DependencyObject から派生するクラスでプロパティを実装する場合は、プロパティを DependencyProperty 識別子を使用してバッキングすることで、依存関係プロパティにします。 依存関係プロパティの作成が有益かどうかは、シナリオによって異なります。 シナリオによってはプライベート フィールドを使用してプロパティをバッキングすれば十分なこともありますが、プロパティで次の WPF 機能を 1 つ以上サポートしたい場合は、依存関係プロパティを実装することを検討してください。

  • スタイル内で設定可能なプロパティ。 詳細については、スタイルとテンプレートに関するページをご覧ください。

  • データ バインディングをサポートするプロパティ。 データ バインディングの依存関係プロパティの詳細については、「2 つのコントロールのプロパティをバインドする」を参照してください。

  • 動的リソース参照を通じて設定できるプロパティ。 詳細については、「XAML リソース」を参照してください。

  • 要素ツリー内の親要素から値を自動的に継承するプロパティ。 この場合、CLR アクセスのプロパティ ラッパーも作成する場合でも、RegisterAttached を使用して登録する必要があります。 詳細については、「プロパティ値の継承」を参照してください。

  • アニメーション可能なプロパティ。 詳しくは、「アニメーションの概要」をご覧ください。

  • プロパティ値が変更されたときの WPF プロパティ システムによる通知。 変更は、プロパティ システム、環境、ユーザー、またはスタイルによるアクションが原因である可能性があります。 プロパティで、プロパティの値が変更されたことがプロパティ システムによって判断されるたびに呼び出されるコールバック メソッドをプロパティ メタデータに指定できます。 関連する概念は、プロパティ値の強制型変換です。 詳細については、「依存関係プロパティのコールバックと検証」を参照してください。

  • 依存関係プロパティ メタデータへのアクセス。これは、WPF プロセスによって読み取られます。 たとえば、プロパティ メタデータを使用して次のことを行うことができます。

    • 依存関係プロパティの値が変更されたときに、レイアウト システムで要素のビジュアルを再構成するかどうかを指定する。

    • 派生クラスのメタデータをオーバーライドして、依存関係プロパティの既定値を設定する。

  • [プロパティ] ウィンドウでカスタム コントロールのプロパティを編集するなど、Visual Studio の WPF デザイナーのサポート。 詳細については、「コントロールの作成の概要」を参照してください。

シナリオによっては、既存の依存関係プロパティのメタデータをオーバーライドする方が、新しい依存関係プロパティを実装するよりも優れた選択肢となります。 メタデータのオーバーライドが実用的かどうかはシナリオ次第であり、そのシナリオが既存の WPF 依存関係プロパティとクラスの実装にどの程度似ているかによります。 既存の依存関係プロパティでメタデータをオーバーライドする方法の詳細については、「依存関係プロパティのメタデータ」を参照してください。

依存関係プロパティを作成するためのチェックリスト

依存関係プロパティを作成するには、次の手順に従います。 手順の一部を組み合わせて、1 行のコードで実装できます。

  1. (省略可能) 依存関係プロパティのメタデータを作成します。

  2. プロパティ名、所有者型、プロパティ値の型、および必要に応じてプロパティ メタデータを指定して、依存関係プロパティをプロパティ システムに登録します。

  3. 所有者型で DependencyProperty 識別子を public static readonly フィールドとして定義します。 識別子フィールドの名前は、プロパティ名にサフィックス Property を付加したものです。

  4. 依存関係プロパティ名と同じ名前の CLR ラッパー プロパティを定義します。 CLR ラッパーで、ラッパーをバッキングする依存関係プロパティと接続する get および set アクセサーを実装します。

プロパティの登録

プロパティを依存関係プロパティにするには、プロパティ システムに登録する必要があります。 プロパティを登録するには、クラスの本文内 (ただし、すべてのメンバーの定義の外) から Register メソッドを呼び出します。 Register メソッドから一意の依存関係プロパティ識別子が返されるので、これを、プロパティ システム API を呼び出すときに使用します。 Register 呼び出しがメンバー定義の外で行う理由は、DependencyProperty 型の public static readonly フィールドに戻り値を代入するからです。 このフィールドは、クラス内で作成し、依存性プロパティの識別子です。 次の例では、Register の第 1 引数に依存関係プロパティ AquariumGraphic の名前を指定しています。

// Register a dependency property with the specified property name,
// property type, owner type, and property metadata. Store the dependency
// property identifier as a public static readonly member of the class.
public static readonly DependencyProperty AquariumGraphicProperty =
    DependencyProperty.Register(
      name: "AquariumGraphic",
      propertyType: typeof(Uri),
      ownerType: typeof(Aquarium),
      typeMetadata: new FrameworkPropertyMetadata(
          defaultValue: new Uri("http://www.contoso.com/aquarium-graphic.jpg"),
          flags: FrameworkPropertyMetadataOptions.AffectsRender,
          propertyChangedCallback: new PropertyChangedCallback(OnUriChanged))
    );
' Register a dependency property with the specified property name,
' property type, owner type, and property metadata. Store the dependency
' property identifier as a public static readonly member of the class.
Public Shared ReadOnly AquariumGraphicProperty As DependencyProperty =
    DependencyProperty.Register(
        name:="AquariumGraphic",
        propertyType:=GetType(Uri),
        ownerType:=GetType(Aquarium),
        typeMetadata:=New FrameworkPropertyMetadata(
            defaultValue:=New Uri("http://www.contoso.com/aquarium-graphic.jpg"),
            flags:=FrameworkPropertyMetadataOptions.AffectsRender,
            propertyChangedCallback:=New PropertyChangedCallback(AddressOf OnUriChanged)))

注意

クラスの本文内に依存関係プロパティを定義するのは一般的な実装ですが、クラスの静的コンストラクター内に依存関係プロパティを定義することもできます。 依存関係プロパティを初期化するために複数行のコードが必要な場合には、このアプローチが適している場合があります。

依存関係プロパティの名前付け

プロパティ システムが正常に動作するためには、依存関係プロパティに対して名前付け規則を確立することが必須です。 作成する識別子フィールドの名前は、プロパティの登録済みの名前にサフィックス Property を付ける必要があります。

依存関係プロパティ名は、登録するクラス内で一意である必要があります。 基本データ型を介して継承される依存関係プロパティは既に登録されており、派生型で登録することはできません。 ただし、依存関係プロパティの所有者としてクラスを追加することで、クラスが継承していない型であっても、別の型によって登録された依存関係プロパティを使用できます。 クラスを所有者として追加する方法の詳細については、「依存関係プロパティのメタデータ」を参照してください。

プロパティ ラッパーの実装

慣例により、ラッパー プロパティの名前は、Register 呼び出しの第 1 パラメーター (依存関係プロパティ名) と同じである必要があります。 ラッパーの実装で、get アクセサーで GetValueset アクセサーで SetValue を呼び出します (読み取り/書き込みプロパティの場合)。 次の例は、登録呼び出しと識別子フィールドの宣言の後にあるラッパーを示しています。 WPF クラスのすべてのパブリック依存関係プロパティで、同様のラッパーモデルを使用します。

// Register a dependency property with the specified property name,
// property type, owner type, and property metadata. Store the dependency
// property identifier as a public static readonly member of the class.
public static readonly DependencyProperty AquariumGraphicProperty =
    DependencyProperty.Register(
      name: "AquariumGraphic",
      propertyType: typeof(Uri),
      ownerType: typeof(Aquarium),
      typeMetadata: new FrameworkPropertyMetadata(
          defaultValue: new Uri("http://www.contoso.com/aquarium-graphic.jpg"),
          flags: FrameworkPropertyMetadataOptions.AffectsRender,
          propertyChangedCallback: new PropertyChangedCallback(OnUriChanged))
    );

// Declare a read-write property wrapper.
public Uri AquariumGraphic
{
    get => (Uri)GetValue(AquariumGraphicProperty);
    set => SetValue(AquariumGraphicProperty, value);
}
' Register a dependency property with the specified property name,
' property type, owner type, and property metadata. Store the dependency
' property identifier as a public static readonly member of the class.
Public Shared ReadOnly AquariumGraphicProperty As DependencyProperty =
    DependencyProperty.Register(
        name:="AquariumGraphic",
        propertyType:=GetType(Uri),
        ownerType:=GetType(Aquarium),
        typeMetadata:=New FrameworkPropertyMetadata(
            defaultValue:=New Uri("http://www.contoso.com/aquarium-graphic.jpg"),
            flags:=FrameworkPropertyMetadataOptions.AffectsRender,
            propertyChangedCallback:=New PropertyChangedCallback(AddressOf OnUriChanged)))

' Declare a read-write property wrapper.
Public Property AquariumGraphic As Uri
    Get
        Return CType(GetValue(AquariumGraphicProperty), Uri)
    End Get
    Set
        SetValue(AquariumGraphicProperty, Value)
    End Set
End Property

まれなケースを除き、ラッパーの実装には、GetValueSetValue のコードのみを含める必要があります。 この理由については、「カスタム依存関係プロパティに対する影響」を参照してください。

プロパティが、確立された名前付け規則に従わない場合、次の問題が発生する可能性があります。

  • スタイルとテンプレートの一部が機能しません。

  • ほとんどのツールとデザイナーは、XAML を適切にシリアル化するために名前付け規則に依存し、プロパティごとのレベルでデザイナー環境のサポートを提供しています。

  • WPF XAML ローダーの現在の実装では、ラッパーを完全にバイパスし、名前付け規則に依存して属性値を処理しています。 詳細については、「XAML 読み込みと依存関係プロパティ」を参照してください。

依存関係プロパティのメタデータ

依存関係プロパティを登録すると、プロパティ システムによって、プロパティの特性を格納するメタデータ オブジェクトが作成されます。 Register メソッドのオーバーロードにより、Register(String, Type, Type, PropertyMetadata) のように登録時にプロパティ メタデータを指定できます。 プロパティ メタデータの一般的な使用方法は、依存関係プロパティを使用する新しいインスタンスに対してカスタムの既定値を適用することです。 プロパティ メタデータを提供しない場合、プロパティ システムによって、依存関係プロパティの特性の多くに既定値が割り当てられます。

FrameworkElement から派生したクラスで依存関係プロパティを作成する場合は、その基底クラス PropertyMetadata ではなく、より専門的なメタデータ クラス FrameworkPropertyMetadata を使用できます。 いくつかの FrameworkPropertyMetadata コンストラクター シグネチャを使用して、メタデータ特性のさまざまな組み合わせを指定できます。 既定値を指定するだけの場合は、FrameworkPropertyMetadata(Object) を使用し、Object パラメーターに既定値を渡します。 値の型が、確実に Register 呼び出しで指定した propertyType と一致するようにします。

一部の FrameworkPropertyMetadata オーバーロードでは、プロパティのメタデータ オプション フラグを指定できます。 これらのフラグは、プロパティ システムによって個別のプロパティに変換され、フラグの値は、レイアウト エンジンなどの WPF プロセスで使用されます。

メタデータ フラグの設定

メタデータ フラグを設定するときは、次の点を考慮してください。

  • プロパティ値 (またはそれに対する変更) を使用して、レイアウト システムによる UI 要素のレンダリング方法に影響を与える場合は、次のフラグを 1 つ以上設定します。

    • AffectsMeasure。これは、プロパティ値の変更によって UI レンダリング、特に、その親内のオブジェクトによって占有される領域の変更が必要であることを示します。 たとえば、このメタデータ フラグを Width プロパティに設定します。

    • AffectsArrange。これは、プロパティ値の変更によって UI レンダリング、特に、その親内のオブジェクトの位置の変更が必要であることを示します。 通常、オブジェクトのサイズも変更されません。 たとえば、このメタデータ フラグを Alignment プロパティに設定します。

    • AffectsRender。これは、レイアウトとメジャーに影響を与えないが、まだ別のレンダリングが必要な変更が発生したことを示します。 たとえば、このフラグを Background プロパティまたは、要素の色に影響を与えるその他のプロパティに設定します。

    これらのフラグは、プロパティ システム (またはレイアウト) コールバックのオーバーライド実装への入力としても使用できます。 たとえば、インスタンスのプロパティで値の変更が報告され、メタデータに AffectsArrange が設定されている場合、OnPropertyChanged コールバックを使用して InvalidateArrange を呼び出すことができます。

  • 一部のプロパティは、他の方法で親要素のレンダリング特性に影響を与えます。 たとえば、MinOrphanLines プロパティを変更すると、フロー ドキュメントの全体的なレンダリングが変更される可能性があります。 AffectsParentArrange または AffectsParentMeasure を使用して、独自のプロパティで親アクションを通知します。

  • 既定では、依存関係プロパティはデータ バインディングをサポートします。 ただし、IsDataBindingAllowed を使用すると、現実的なシナリオがない場合や、大きなオブジェクトなど、データ バインディングのパフォーマンスが問題になる場合にデータ バインディングを無効にすることができます。

  • 依存関係プロパティの既定のデータ バインディング モードOneWay ですが、特定のバインディングのバインディング モードを TwoWay に変更できます。 詳細については、「バインディングの方向」を参照してください。 依存関係プロパティの作成者は、双方向のバインディングを既定のモードにするように選択することもできます。 双方向のデータ バインディングを使用する既存の依存関係プロパティの例は MenuItem.IsSubmenuOpen です。これは、他のプロパティとメソッド呼び出しに基づく状態を持っています。 IsSubmenuOpen のシナリオでは、その設定ロジックと、MenuItem の複合が既定のテーマ スタイルと相互作用します。 TextBox.Text は、既定で双方向のバインディングを使用する別の WPF 依存関係プロパティです。

  • Inherits フラグを設定することで、依存関係プロパティのプロパティの継承を有効にできます。 プロパティの継承は、親と子の要素に共通のプロパティがあり、子要素が共通プロパティの親値を継承することが理にかなっているシナリオで役立ちます。 継承可能なプロパティの例は、DataContext です。これは、データ表示にマスター詳細シナリオを使用するバインディング操作をサポートします。 プロパティ値の継承により、ページまたはアプリケーション ルートでデータ コンテキストを指定できるため、子要素のバインドに指定する必要がなくなります。 継承されたプロパティ値によって既定値がオーバーライドされますが、プロパティ値はどの子要素にもローカルに設定できます。 パフォーマンス コストがかかるため、プロパティ値の継承は慎重に使用してください。 詳細については、「プロパティ値の継承」を参照してください。

  • Journal フラグを設定して、依存関係プロパティをナビゲーション ジャーナリング サービスで検出または使用する必要があることを示します。 たとえば、SelectedIndex プロパティで Journal フラグを設定して、選択した項目のジャーナリング履歴をアプリケーションで保持することをお勧めします。

読み取り専用の依存関係プロパティ

読み取り専用の依存関係プロパティを定義できます。 一般的なシナリオは、内部状態を格納する依存関係プロパティです。 たとえば、IsMouseOver は、状態がマウス入力によってのみ決定される必要があるため、読み取り専用です。 詳細については、「読み取り専用の依存関係プロパティ」を参照してください。

コレクション型の依存関係プロパティ

コレクション型依存関係プロパティには、参照型の既定値の設定やコレクション要素のデータ バインディング サポートなど、考慮すべき追加の実装の問題があります。 詳細については、「コレクション型依存関係プロパティ」を参照してください。

依存関係プロパティのセキュリティ

通常は、依存関係プロパティをパブリック プロパティとして、DependencyProperty 識別子フィールドを public static readonly フィールドとして宣言します。 protected など、制限のより厳しいアクセス レベルを指定する場合でも、依存関係プロパティは、プロパティ システム API と組み合わせて、その識別子を介してアクセスできます。 保護された識別子フィールドであっても、WPF メタデータ レポートまたは値決定 API (LocalValueEnumerator など) を使用してアクセスできる可能性があります。 詳細については、「依存関係プロパティのセキュリティ」を参照してください。

読み取り専用の依存関係プロパティの場合、RegisterReadOnly から返される値は DependencyPropertyKey であり、通常、DependencyPropertyKeypublic クラスのメンバーにすることはありません。 WPF プロパティ システムでは、DependencyPropertyKey はコードの外部に伝達されないため、読み取り専用の依存関係プロパティの方が、読み取り/書き込み依存関係プロパティよりも set セキュリティが優れています。

依存関係プロパティとクラス コンストラクター

マネージド コードのプログラミングには、クラス コンストラクターで仮想メソッドを呼び出さないという一般的な原則があり、多くの場合、コード分析ツールで強制されます。 これは、派生クラスのコンストラクターの初期化中に基本コンストラクターを呼び出すことができ、派生クラスの初期化が完了する前に基本コンストラクターによって呼び出される仮想メソッドが実行される可能性があるためです。 DependencyObject から既に派生しているクラスから派生する場合、プロパティ システム自体で仮想メソッドを呼び出し、内部的に公開します。 これらの仮想メソッドは、WPF プロパティ システム サービスの一部です。 メソッドをオーバーライドすることで、派生クラスが値の決定に参加できるようになります。 ランタイムの初期化の潜在的な問題を回避するには、特殊なコンストラクター パターンに従う場合を除き、依存関係プロパティの値をクラスのコンストラクター内で設定しないでください。 詳細については、「DependencyObject の安全なコンストラクター パターン」を参照してください。

関連項目