MSDN マガジン > Home > 発行物 > 2008 > September >  基礎 : 依存関係プロパティと通知
基礎
依存関係プロパティと通知
Charles Petzold

コードのダウンロード : Foundations2008_09a.exe (158 KB)
オンラインでのコードの参照
オブジェクトには、数多くの役目が負わされるようになってきました。さまざまな作業がオブジェクトに指定されます。Windows® Presentation Foundation (WPF) アプリケーションでは、一般的なオブジェクトに対して多種多様な要求が同時に殺到することがあります。たとえば、このデータにバインドし、このようなスタイルを設定し、ビジュアル ツリーの親からビジュアルを継承し、さらにアニメーションを表示するといったことが要求されます。
どのように範囲を区切り、優先順位を決めればよいでしょうか。この問題を解決するために、WPF では依存関係プロパティという機能を利用します。WPF クラスで依存関係プロパティを使用することで、データのバインド、スタイル、継承、さらに他のソースによって生じた変更に構造的に対応できます。依存関係プロパティは、初期の .NET プログラマにとってのイベントやイベント処理と同じくらい重要な機能です。
ただし、従来からある一部の作業では、依存関係プロパティでも完全には対応できない場合があります。たとえば、依存関係プロパティを持つ特定のオブジェクトにアクセスできるとき、そのプロパティの 1 つが変更されたときに通知を受けるように設定するとします。イベント ハンドラを使えば、この処理は簡単に実現できるように思えます。しかし、必要な通知イベントが単に存在しないとしたら、どうでしょうか。
これは、オブジェクトのコレクションを扱う場合に重大な問題となります。コレクションに含まれるオブジェクトの、特定の依存関係プロパティに加えられた変更を通知する場合を考えてみましょう。従来の方法では、FreezableCollection<T> を使用するしかありません。これによって、何らかの変更が発生したときに通知が行われます。しかし、変更の内容までは通知されません。
つまり、依存関係プロパティが他の機能とうまく連携できない場合があるのです。このコラムでは、依存関係プロパティに通知イベントが存在しないという問題を補う方法について解説します。

依存関係プロパティの基礎
PopArt という名前の WPF クラスを作成するとします。ここでは、Brush 型のプロパティ、SwirlyBrush を定義します。PopArt が DependencyObject から継承される場合、SwirlyBrush を DependencyProperty として定義できます。まず、パブリックの静的な読み取り専用フィールドを定義します。
public static readonly DependencyProperty SwirlyBrushProperty;
依存関係プロパティは、元のプロパティの名前に "Property" が付加された名前になります。次に、フィールド宣言または静的コンストラクタの中で、依存関係プロパティを登録します。
SwirlyBrushProperty = DependencyProperty.Register("SwirlyBrush",
  typeof(Brush), typeof(PopArt),
  new PropertyMetadata(OnSwirlyBrushChanged));
さらに、このプロパティに対する通常のアクセスに使用される通常のプロパティ (CLR プロパティとも呼ばれる) を定義する必要があります。
public Brush SwirlyBrush {
  set { SetValue(SwirlyBrushProperty, value); }
  get { return (Brush) GetValue(SwirlyBrushProperty); }
}
SetValue メソッドと GetValue メソッドは DependencyObject によって定義されます。そのため、依存関係プロパティを定義するすべてのクラスが DependencyObject クラスから派生していることが必要です。CLR プロパティには、この 2 つのメソッドへの呼び出し以外のコードを含めないでください。CLR プロパティは、しばしば依存関係プロパティによって "サポート" されると言われます。
依存関係プロパティの登録で参照されている OnSwirlyBrushChanged メソッドは、SwirlyBrush プロパティが変更されるたびに呼び出されるコールバック メソッドです。このメソッドは静的フィールドに関連付けられているため、静的である必要があります。
static void OnSwirlyBrushChanged(DependencyObject obj,
  DependencyPropertyChangedEventArgs args) {
  ...
}
最初の引数は、プロパティが変更されるクラスの特定のインスタンスです。PopArt という名前のクラスでこの依存関係プロパティを定義した場合、最初の引数は必ず PopArt 型のオブジェクトになります。ここでは、静的メソッドと同じ名前でインスタンス メソッドを定義します。静的メソッドでは、次のようにインスタンス メソッドを呼び出します。
static void OnSwirlyBrushChanged(DependencyObject obj,
  DependencyPropertyChangedEventArgs args) {
  (obj as PopArt).OnSwirlyBrushChanged(args);
}

void OnSwirlyBrushChanged(DependencyPropertyChangedEventArgs args) {
  ...
}
このインスタンス メソッドには、SwirlyBrush の値の変更に対応するために必要な要素がすべて含まれます。
コード内では、PopArt のインスタンスの SwirlyBrush プロパティの値を通常の方法で設定できます。
popart.SwirlyBrush = new SolidColorBrush(Colors.AliceBlue);
ただし、PopArt クラスには PopArt.SwirlyBrushProperty という名前のパブリックな静的フィールドが存在することに注意してください。このフィールドは、PopArt 型のどのオブジェクトにも依存しない、DependencyProperty 型の有効なオブジェクトです。これを使用することで、そのプロパティを持つオブジェクトが作成される前にクラスの特定のプロパティを参照できます。
DependencyObject で定義される SetValue メソッドもパブリックであるため、SwirlyBrush プロパティは次のようにしても設定できます。
popart.SetValue(PopArt.SwirlyBrushProperty,
  new SolidColorBrush(Colors.AliceBlue));
さらに、図 1 に示した 3 つのオブジェクトがある場合、次のように完全に標準化されたコードを使ってこのプロパティを設定できます。
target.SetValue(property, value);
オブジェクト 説明
target DependencyObject から派生したクラスのインスタンス。
property DependencyProperty 型のオブジェクト。
value 依存関係プロパティの適切な型のオブジェクト。
値の型が、DependencyProperty に関連付けられている型 (この例では Brush) と一致しなければ、例外が発生します。ただし、DependencyProperty で定義される ValidType メソッドを利用することで、そのような問題を回避できます。
DependencyProperty オブジェクトを SetValue および GetValue と組み合わせて使用することでプロパティを参照し、設定する手法は、"SwirlyBrush" などの文字列を使ってプロパティを参照する従来の手法よりも簡潔です。従来の方法でプロパティを指定した場合、プロパティを実際に設定するためのリフレクションが必要でした。

ソースとターゲットをバインドする
XAML でデータ バインド、スタイル、アニメーションを設定する場合、依存関係プロパティを WPF 内で広範囲に使用する必然性はあまりありませんが、コード内でそのような設定をする場合、その必然性は高まります。BindingOperations および FrameworkElement によって定義された SetBinding メソッドには、バインドのターゲットとして、DependencyProperty 型のオブジェクトが必要です。WPF スタイルで使用される Setter クラスには、DependencyProperty オブジェクトが必要です。BeginAnimation メソッドにも、アニメーションのターゲットとして DependencyProperty オブジェクトが必要です。
WPF で優先順位付け規則が適切に適用されるためには、このようなターゲットがすべて DependencyProperty オブジェクトであることが必要です。たとえば、アニメーションによって設定されるプロパティ値は、スタイルによって設定される値よりも優先されます。特定のプロパティの値を決定した可能性があるソースを調べるには、DependencyPropertyHelper クラスを使用します。
データ バインドのターゲットは依存関係プロパティであることが必要ですが、バインドのソースは依存関係プロパティでなくてもかまいません。バインドによってソースの変化を正常に監視する必要がある場合、バインドのソースによって何らかの通知メカニズムを実装する必要があるのは明らかですが、実際には WPF では 3 種類のソース通知を実行できます。それぞれの種類のソース通知は単独で使用する必要があり、同時には使用できません。
1 つ目は、ソースとターゲットの両方に依存関係プロパティを使用する方法です (この方法が推奨されます)。同じプロパティが、さまざまな状況でバインドのソースとしてもターゲットとしても機能する場合、特に有効な方法です。
2 つ目は、プロパティ名に基づいてイベントを定義する方法です。たとえば、Flavor という名前のプロパティをクラスで定義する場合、FlavorChanged という名前のイベントも定義します。イベント名からわかるように、Flavor の値が変わるたびにこのイベントが発生します。このような通知は、Windows フォームで広く使用されていました (たとえば、Control により定義される Enabled プロパティには、対応する EnabledChanged イベントがありました)。ただし、これは WPF コードでは使用を避ける必要があるため、ここでは詳しく説明しません。
3 つ目は、INotifyPropertyChanged インターフェイスを実装する方法です。PropertyChangedEventHandler デリゲートに基づいて、このクラスで PropertyChanged という名前のイベントを定義する必要があります。関連付けられる PropertyChangedEventArgs オブジェクトには、PropertyName というプロパティが含まれます。このプロパティは、変更されたプロパティの名前を示すテキスト文字列です。
たとえば、クラスに Flavor というプロパティと、flavor という名前のプライベート フィールドが含まれる場合、プロパティの set アクセサは次のようになります。
flavor = value;

if (PropertyChanged != null)
  PropertyChanged(this, new PropertyChangedEventArgs("Flavor");
Flavor プロパティに加えられる変更を監視するクラスは、PropertyChanged イベントに単純にサブスクライブでき、このプロパティ (または、このクラス内の他のプロパティ) が変更されたときに通知することができます。
INotifyPropertyChanged インターフェイスも、絶対に間違いのない機能とは言えません。このインターフェイスの命令は、すべて PropertyChanged という名前のイベントです。クラスでこのイベントが必ず発生するという保証はありません。多くの場合、クラスでは一部のパブリック プロパティに対してのみイベントが発生します。ソース コードにアクセスできない場合、PropertyChanged イベントを発生させるプロパティと発生させないプロパティを事前に判別する適切な方法はありません。
それでも、WPF データ バインド、スタイル、またはアニメーションのターゲットとならず、なおかつ他のクラスに変更を通知する必要があるプロパティに対しては、INotifyPropertyChanged は単純で優れた解決策です。

カスタム コレクションと通知
オブジェクトのコレクションを使用する場合、いずれかのオブジェクトのプロパティに変更が加えられるたびに通知が必要になることがあります。最も望ましい通知は、コレクション内で変更された項目と、その項目で具体的にどのプロパティが変更されたかを正確に知らせる通知です。
コレクション内のオブジェクトに INotifyPropertyChanged インターフェイスが実装され、コレクション自体に INotifyCollectionChanged が実装されている場合、これは非常に簡単に実現できます。このインターフェイスを実装するコレクションでは、コレクションに新しい項目が追加されるか、既存の項目が削除されるたびに、CollectionChanged イベントが発生します。CollectionChanged イベントによって、追加された項目と削除された項目の一覧が得られます。よく使用されるコレクション オブジェクトで INotifyCollectionChanged を実装しているものとしては、ObservableCollection<T> ジェネリック クラスがあります。
ここでは、ObservableCollection<T> から派生したカスタム コレクション クラスを作成し、ItemPropertyChanged という名前の新しいイベントを実装します。このイベントは、コレクション内のいずれかの項目のプロパティが変更されるたびに発生し、その項目および変更されたプロパティを含むイベント引数を返します。図 2 に、PropertyChangedEventArgs クラスから派生した ItemPropertyChangedEventArgs クラスを示します。このクラスに、型オブジェクトの新しいプロパティである Item が含まれます。図 2 には、ItemPropertyChangedEventHandler デリゲートも示されています。
public class ItemPropertyChangedEventArgs : PropertyChangedEventArgs {
  object item;

  public ItemPropertyChangedEventArgs(object item, 
    string propertyName) : base(propertyName) {
    this.item = item;
  }

  public object Item {
    get { return item; }
  }
}

public delegate void ItemPropertyChangedEventHandler(object sender, 
  ItemPropertyChangedEventArgs args);
ObservableCollection<T> から派生し、ItemPropertyChanged イベントを定義する ObservableNotifiableCollection<T> を図 3 に示します。この新しいクラスでは、型パラメータが INotifyPropertyChanged 型のオブジェクトに限定されていることに注意してください。OnCollectionChanged のオーバーライド中に、コレクションに追加される各項目について PropertyChanged イベント ハンドラが登録されます。項目がコレクションから削除されると、PropertyChanged イベント ハンドラも削除されます。項目の PropertyChanged イベントの発生中、コレクションで ItemPropertyChanged イベントが発生します。PropertyChanged イベントの送信側オブジェクト (プロパティが変更された項目) が、ItemPropertyChangedEventArgs の Item プロパティになります。
class ObservableNotifiableCollection<T> : 
  ObservableCollection<T> where T : INotifyPropertyChanged {

  public ItemPropertyChangedEventHandler ItemPropertyChanged;

  protected override void OnCollectionChanged(
    NotifyCollectionChangedEventArgs args) {

    base.OnCollectionChanged(args);

    if (args.NewItems != null)
      foreach (INotifyPropertyChanged item in args.NewItems)
        item.PropertyChanged += OnItemPropertyChanged;

    if (args.OldItems != null)
      foreach (INotifyPropertyChanged item in args.OldItems)
        item.PropertyChanged -= OnItemPropertyChanged;
  }

  void OnItemPropertyChanged(object sender, 
    PropertyChangedEventArgs args) {

    if (ItemPropertyChanged != null)
      ItemPropertyChanged(this, 
        new ItemPropertyChangedEventArgs(sender, 
        args.PropertyName));
  }
}
図 2 および図 3 で示したコードは、このコラムでダウンロードできるコードに含まれている ObservableNotifiableCollectionDemo プロジェクトの一部分です。このプロジェクトには、簡単なデモンストレーション プログラムも含まれています。

依存関係プロパティとイベント
依存関係プロパティによって優れたバインドのソースが作成されますが、パブリック イベントの形での汎用の通知メカニズムは提供されません。そのようなイベントを探し回ったあげく、この面倒な事実に気付けば、少々ショックを受けることでしょう。
しかし、DependencyObject では保護された OnPropertyChanged メソッドが定義されます。このメソッドは、DependencyObject の動作に欠かせないものです。個人的な経験から判断すると、OnPropertyChanged は SetValue を呼び出した結果として呼び出されます。個々の依存関係プロパティに関連付けられているコールバック メソッドを呼び出すのは、OnPropertyChanged です。OnPropertyChanged の関連ドキュメントには、このメソッドをオーバーライドする危険性について数多くの警告が記載されています。
依存関係プロパティを実装するクラスを記述する際、特定の依存関係プロパティが変更されるたびにイベントを発生するように、そのクラスのユーザーが希望していると想定することがあるでしょう。このような場合は、そのイベントを明示的に指定します。イベントには、プロパティの名前に "Changed" を付加した名前を付けますが (Windows フォームでのイベントの命名法に似ています)、DependencyPropertyChangedEventHandler デリゲートを使用したイベントを定義します。サンプルでは、UIElement の中に、FocusableChanged や IsEnabledChanged などの依存関係プロパティに関連付けられているイベントが多数含まれています。
前に示した PopArt サンプルでは、次に示すようなコードで SwirlyBrushChanged イベントを定義できます。
public event DependencyPropertyChangedEventHandler 
  SwirlyBrushChanged;
前に示した OnSwirlyBrushChanged メソッドのインスタンス バージョンでは、このイベントを次のようなコードで発生させます。
if (SwirlyBrushChanged != null)
  SwirlyBrushChanged(this, args);
このコードはあまりにも平凡なため、これ以上の依存関係プロパティがイベントに関連付けられていないのは驚くべきことです。
使用するクラスの作成者が必要なイベントを用意していなかった場合、特定の依存関係プロパティが変更されたときに通知を受け取るには、別の方法を探る必要があります。幸運なことに、少なくとも 3 つの解決法があります。1 つ目は奇妙な方法、2 つ目は強引な方法、3 つ目は左側のフィールドを使用する方法です。

動的なバインド
依存関係プロパティの変更の通知を受け取るための奇妙な方法とは、依存関係プロパティとデータ バインドの両方を動的に作成することです。DependencyObject から継承されるクラスを記述する際、依存関係プロパティが含まれるオブジェクトにアクセスでき、そのいずれかの依存関係プロパティが変更されたときに通知を受ける必要がある場合、依存関係プロパティとバインドを実行時に作成して必要な情報を得ることができます。
たとえば、クラスで TextBlock 型のオブジェクト txtblk にアクセスでき、Text プロパティが変更されたときに通知を受ける必要があるとします。Text プロパティは TextProperty という名前の DependencyProperty によりサポートされますが、TextBlock では TextChanged イベントが定義されていません。この問題を解消するには、まず監視対象のプロパティと同じ型の DependencyProperty を登録します。
DependencyProperty MyTextProperty = 
  DependencyProperty.Register("MyText", typeof(string), GetType(),
  new PropertyMetadata(OnMyTextChanged));
この DependencyProperty オブジェクトは、パブリックな静的フィールドである必要はありません。この例に示すように、インスタンス メソッドの内部で登録することができます。OnMyTextChanged コールバックも、インスタンス メソッドにすることができます。
DependencyProperty を登録した後で、TextBlock オブジェクトとこのプロパティのバインドを定義できます。
Binding binding = new Binding("Text");
binding.Source = txtblk;
BindingOperations.SetBinding(this, MyTextProperty, binding);
これで、txtblk オブジェクトの Text プロパティに変更が加えられるたびに、OnMyTextChanged コールバックが呼び出されるようになります。
さらに、監視対象の DependencyObject と DependencyProperty の情報がまったくない状態でも、この DependencyProperty を登録し、コールバックを定義することができます。DependencyObject から派生した obj という名前のクラスで、property という名前の DependencyProperty 変数を追跡する必要があることのみ把握しているとします。コードは次のようになります。
DependencyProperty OnTheFlyProperty = 
  DependencyProperty.Register("OnTheFly", 
  property.PropertyType, 
  GetType(),
  new PropertyMetadata(OnTheFlyChanged));

Binding binding = new Binding(property.Name);
binding.Source = obj;
BindingOperations.SetBinding(this, OnTheFlyProperty, binding);
この解決方法は、1 つのオブジェクトに 1 つ以上のプロパティが含まれている場合に適しています。同じ型の複数のオブジェクト (コレクションなど) を扱う場合、各オブジェクトの各プロパティに対して一意の依存関係プロパティとバインドを実行中に作成する必要があります。このような方法は、すぐに手に負えなくなります。

Freezable の差異
Freezable クラスは、依存関係オブジェクトに変更が加えられたときに通知を受け取る "強引な" 手法で使用します。この方法でも確かに通知を受けることができますが、実際に変更が加えられた対象を判別することができません。
Freezable は、DependencyObject で定義された OnPropertyChanged メソッドをオーバーライドし、Changed という名前の新しいプロパティを定義します。このプロパティについては、"この Freezable 自体またはそれに含まれるいずれかのオブジェクトが変更されたとき" に実行されると規定されています。この記述は太字で強調されています。"それに含まれるいずれかのオブジェクト" が依存関係プロパティにサポートされた Freezable 型のサブプロパティであると認識できる限り、実際にこの記述のように機能します。
たとえば、A という Freezable クラスに、B 型のプロパティ B が含まれているとします。B クラスも Freezable から派生しており、C 型のプロパティ C を含んでいます。C クラスも Freezable から派生し、double 型のプロパティ D を含んでいます。これらのプロパティはすべて、依存関係プロパティでサポートされています。これらのすべての型について、それぞれ対応するオブジェクトを作成し、プロパティ D を変更すると、A、B、C すべてのクラスで Changed イベントが発生します。このコラムでダウンロードできるソース コードに含まれている NestedFreezableNotificationsDemo プロジェクトで、この動作を確認できます。
大きな問題点は、この Changed イベントが EventHandler デリゲートに基づいており、Freezable オブジェクトのどのプロパティ (またはサブプロパティ) が変更されたかに関する正確な情報がまったく得られないことです。さらに、この方法による通知ではオーバーヘッドが大きいという問題があります。そのため、変更不可能にすることができ、すべての通知を消失できる機能にちなんで Freezable クラスという名前が付けられています。
Freezable は、WPF のグラフィックス システムで広く使用されています。Brush、Pen、Geometry、Drawing の各クラスは、すべて Freezable から派生しています。DoubleCollection、PointCollection、VectorCollection、Int32Collection などのコレクションは、Freezable から派生しています。また、このようなコレクションにはコレクションが変更されたときに発生する Changed イベントも実装されています。その他の Freezable コレクションとしては、コレクション内の項目のいずれかのプロパティまたはサブプロパティが変更されるたびに Changed イベントを発生させる Freezable オブジェクト (GeometryCollection や TransformCollection など) のコレクションがあります。
たとえば、PathGeometry は Freezable から派生しています。これには PathFigureCollection 型の Figures プロパティが含まれていますが、このプロパティもまた Freezable から派生しています。PathFigureCollection には PathFigure 型のオブジェクトが含まれていますが、これもまた Freezable から派生しています。PathFigure には PathSegmentCollection 型の Segments というプロパティが含まれていますが、これも Freezable です。PathSegment は Freezable から派生しており、PolyLineSegment や PolyBezierSegment などのクラスの基本クラスです。これらのクラスには PointCollection 型の Points というプロパティが含まれていますが、このプロパティもまた Freezable から派生しています。
全体的な動作としては、個々のポイントが変更されたときに変更通知がオブジェクト階層をさかのぼって伝達され、グラフィカル オブジェクト全体が再描画されます。
PathGeometry では、変更されたポイントを正確に判別し、ジオメトリの該当部分だけを修正するのでしょうか。つまり、増分更新は実行されるのでしょうか。公開されている情報から判断する限り、それは不可能です。PathGeometry によって判別できることは、何らかの変更が加えられ、全部を最初から再描画する必要があることのみです。
入れ子になったサブプロパティのコレクションのどこかで何らかの変更が発生したという単純な通知が、目的に適合するのであれば、Freezable クラスは最適な解決策の 1 つと言えます。また、FreezableCollection<T> ジェネリック クラスも便利です。型パラメータは Freezable 型にする必要はありません。単純に、DependencyObject にするだけでかまいません。ただし、型パラメータが Freezable 型であれば、コレクションの項目のプロパティまたは Freezable サブプロパティに変更が加えられたときに Changed イベントが発生します。
Freezable クラスには特定の制限事項があることに注意してください。クラスを FreezableCollection<T> から派生させる場合、CreateInstanceCore をオーバーライドする必要があります (さらに、そのクラスのコンストラクタを単純に呼び出して、そのオブジェクトを返す必要があります)。そうしなければ、意味不明の奇妙なエラーが発生します。

DependencyPropertyDescriptor を使用する
3 つ目の方法は、DependencyPropertyDescriptor クラスを含む依存関係プロパティから通知イベントを取得する方法です。このクラスは "主にデザイナ アプリケーションで使用する" と規定されているため、決してわかりやすい解決策ではありませんが、実用的で重要な方法です。
ここでも、TextBlock 型のオブジェクトにアクセスでき、Text プロパティが変更されたときに通知を受ける必要があるとします。まず、DependencyPropertyDescriptor 型のオブジェクトを作成します。
DepedencyPropertyDescriptor descriptor =
  DependencyPropertyDescriptor.FromProperty(
  TextBlock.TextProperty, typeof(TextBlock));
1 つ目の引数は DependencyProperty オブジェクト、2 つ目の引数はその依存関係プロパティを含む、または継承するクラスです。DependencyPropertyDescriptor は特定の TextBlock オブジェクトに関連付けられていないことに注意してください。
次に、特定の TextBlock オブジェクト (この例では txtblk) の Text プロパティに加えられた変更を検出するためのハンドラを登録します。
descriptor.AddValueChanged(txtblk, OnTextChanged);
ハンドラを削除する RemoveValueChanged メソッドも存在することに注意してください。
OnTextChanged ハンドラは、単純な EventHandler デリゲート シグネチャに基づいており、次のようなコードで定義されます。
void OnTextChanged(object sender, EventArgs args) {
  ...
}
EventArgs 引数では情報が返されません。ただし、ハンドラの最初の引数が、Text の値が変更された TextBlock オブジェクトとなります。
これらのハンドラを複数の依存関係プロパティで共有せずに、それぞれの依存関係プロパティごとに別々のハンドラを用意しようとする場合もあるでしょう。しかし、オブジェクトはハンドラに対する 1 つ目の引数で判別できるため、複数のオブジェクトでハンドラを簡単に共有できます。
これで、最後の手順を実行するために必要な情報が揃いました。最後の手順では、コレクション内の項目の依存関係プロパティが変更されたときに通知を発生させるコレクションを構築します。

DependencyObjects のコレクション
理論的には、DependencyObject から派生したオブジェクトのコレクションがある場合、特定の依存関係プロパティに対応する DependencyPropertyDescriptor オブジェクトを作成できます。このオブジェクトを利用して、コレクション内の任意の項目でこれらのプロパティが変更されるたびに、通知を受けることができます。
DependencyPropertyDescriptor からの通知を処理するメソッドに関して、1 つ問題があります。どの依存関係プロパティが変更されたのかを判別できないため、1 つのメソッドを複数の依存関係プロパティで共有しないでください。それぞれの依存関係プロパティについて、別々のメソッドが必要です。一般的に、実行時になるまでは必要な数のメソッドを使用することはできません。メソッドは実行時にも動的に作成できますが、それには中間言語を生成する必要があります。通知処理専用のメソッドを含むクラスを定義した後、監視対象の依存関係プロパティそれぞれに対して、このクラスのインスタンスを作成する方が簡単です。
ここでは、それを実行するために ObservableDependencyObjectCollection<T> を作成し、これを ObservableCollection<T> から派生させています。このコレクションの項目は、DependencyObject から派生したクラスのインスタンスであることが必要です。コレクション クラスでは、その型パラメータにより定義または継承された各 DependencyProperty に対して、または選択された一連の DependencyProperty オブジェクトに対して、DependencyPropertyDescriptor オブジェクトが作成されます。
ObservableDependencyObjectCollection<T> により、ItemDependencyPropertyChanged という名前の新しいイベントが定義されます。図 4 に、イベント引数の定義とこのイベントのデリゲートを示します。ItemDependencyPropertyChangedEventArgs は、通常の DependencyPropertyChangedEventArgs と似ていますが、Item プロパティが含まれている点と、OldValue プロパティが含まれない点が異なります。
public struct ItemDependencyPropertyChangedEventArgs {
  DependencyObject item;
  DependencyProperty property;
  object newValue;

  public ItemDependencyPropertyChangedEventArgs(
    DependencyObject item,
    DependencyProperty property,
    object newValue) {

    this.item = item;
    this.property = property;
    this.newValue = newValue;
  }

  public DependencyObject Item {
    get { return item; }
  }

  public DependencyProperty Property {
    get { return property; }
  }

  public object NewValue {
    get { return newValue; }
  }
}

public delegate void ItemDependencyPropertyChangedEventHandler(
  Object sender,
  ItemDependencyPropertyChangedEventArgs args);
実際の ObservableDependencyObjectCollection<T> クラスの実装例を 3 つ紹介します。このクラスの最初の部分を図 5 に示します。このクラスは ObservableCollection<T> から派生していますが、型パラメータは DependencyObject に限定されます。ObservableCollection<T> には 2 つのコンストラクタが含まれます。1 つはパラメータを持たないコンストラクタ、もう 1 つはコンテンツ初期化用の List<T> ジェネリック オブジェクトを持つコンストラクタです。新しいクラスにはこの 2 つのコンストラクタが含まれますが、必要に応じて監視対象の DependencyProperty オブジェクトを指定できる 2 つのコンストラクタも追加されます。
たとえば、Button オブジェクトのコレクションが必要であるものの、フォント関連の 2 つのプロパティが変更されたときにのみ通知を受ける必要があるとします。コンストラクタは次のようになります。
ObservableDependencyObjectCollection<Button> buttons =
  new ObservableDependencyObjectCollection(
  Button.FontSize, Button.FontFamily);
次に、このコレクションのハンドラを実装します。
buttons.ItemDependencyPropertyChanged +=
  OnItemDependencyPropertyChanged;
図 5 のすべてのコンストラクタで、最後に GetExplicitDependencyProperties または GetAllDependencyProperties が呼び出されます。1 つ目のメソッドでは、コンストラクタ引数で指定された各 DependencyProperty オブジェクトごとに、単純に CreateDescriptor を呼び出します。2 つ目のメソッドでは、リフレクションを使用して、型パラメータおよびその先祖 (BindingFlags.FlattenHierarchy が表す内容) により定義された DependencyProperty 型のパブリック フィールドをすべて取得した後、それぞれについて CreateDescriptor を呼び出します。
public class ObservableDependencyObjectCollection<T> : 
  ObservableCollection<T> where T : DependencyObject {

  public event ItemDependencyPropertyChangedEventHandler 
    ItemDependencyPropertyChanged;

  List<DescriptorWrapper> descriptors = new List<DescriptorWrapper>();

  public ObservableDependencyObjectCollection() {
    GetAllDependencyProperties();
  }

  public ObservableDependencyObjectCollection(List<T> list) : base(list) {
    GetAllDependencyProperties();
  }

  public ObservableDependencyObjectCollection(
    params DependencyProperty[] properties) {

    GetExplicitDependencyProperties(properties);
  }

  public ObservableDependencyObjectCollection(List<T> list, 
    params DependencyProperty[] properties) : base(list) {

    GetExplicitDependencyProperties(properties);
  }

  void GetExplicitDependencyProperties(
    params DependencyProperty[] properties) {

    foreach (DependencyProperty property in properties)
      CreateDescriptor(property);
  }

  void GetAllDependencyProperties() {
    FieldInfo[] fieldInfos = 
      typeof(T).GetFields(BindingFlags.Public | 
                          BindingFlags.Static | 
                BindingFlags.FlattenHierarchy);

    foreach (FieldInfo fieldInfo in fieldInfos)
      if (fieldInfo.FieldType == typeof(DependencyProperty))
        CreateDescriptor(fieldInfo.GetValue(null) as DependencyProperty);
  }

  void CreateDescriptor(DependencyProperty property) {
    DescriptorWrapper descriptor = 
      new DescriptorWrapper(typeof(T), property, 
      OnItemDependencyPropertyChanged);
      descriptors.Add(descriptor);
  }
CreateDescriptor メソッドを呼び出すたびに、DescriptorWrapper 型の新しいオブジェクトが作成され、コレクション内の項目の型、監視対象の DependencyProperty、および OnItemDependencyPropertyChanged という名前のコールバック メソッドがそのオブジェクトに渡されます。
図 6 に示す DescriptorWrapper クラスは、ObservableDependencyObjectCollection<T> に包含され、本質的に DependencyPropertyDescriptor をラップします。コンストラクタにより、この DependencyPropertyDescriptor およびコールバック メソッドに関連付ける特定の DependencyProperty が不要になります。
class DescriptorWrapper {
  DependencyPropertyChangedEventHandler 
    OnItemDependencyPropertyChanged;
  DependencyPropertyDescriptor desc;
  DependencyProperty dependencyProperty;

  public DescriptorWrapper(Type targetType, 
    DependencyProperty dependencyProperty, 
    DependencyPropertyChangedEventHandler 
    OnItemDependencyPropertyChanged) {

    desc = DependencyPropertyDescriptor.FromProperty(
      dependencyProperty, targetType);
    this.dependencyProperty = dependencyProperty;
    this.OnItemDependencyPropertyChanged = 
      OnItemDependencyPropertyChanged;
  }

  public void AddValueChanged(DependencyObject dependencyObject) {
    desc.AddValueChanged(dependencyObject, OnValueChanged);
  }

  public void RemoveValueChanged(DependencyObject dependencyObject) {
    desc.RemoveValueChanged(dependencyObject, OnValueChanged);
  }

  void OnValueChanged(object sender, EventArgs args) {
    OnItemDependencyPropertyChanged(sender, 
      new DependencyPropertyChangedEventArgs(
      dependencyProperty, 
      null,
      (sender as DependencyObject).GetValue(
      dependencyProperty)));
  }
}
このクラスには、DependencyPropertyDescriptor オブジェクト内の対応するメソッドを呼び出す AddValueChanged メソッドと RemoveValueChanged メソッドも含まれています。OnValueChanged ハンドラによって、情報がコレクション クラスに返されます。
ObservableDependencyObjectCollection<T> クラスの最後のセクションを図 7 に示します。OnCollectionChanged メソッドのオーバーライドによって、コレクションに追加されるすべての項目 (および各 DescriptorWrapper) をループ処理し、AddValueChanged を呼び出します。コレクションから削除されたすべての項目に対応するハンドラが削除されます。
protected override void OnCollectionChanged(
  NotifyCollectionChangedEventArgs args) {

  base.OnCollectionChanged(args);

  if (args.NewItems != null)
    foreach (DependencyObject obj in args.NewItems)
      foreach (DescriptorWrapper descriptor in descriptors)
        descriptor.AddValueChanged(obj);

  if (args.OldItems != null)
    foreach (DependencyObject obj in args.OldItems)
      foreach (DescriptorWrapper descriptor in descriptors)
        descriptor.RemoveValueChanged(obj);
}

protected void OnItemDependencyPropertyChanged(object item, 
  DependencyPropertyChangedEventArgs args) {

  if (ItemDependencyPropertyChanged != null)
    ItemDependencyPropertyChanged(this, 
      new ItemDependencyPropertyChangedEventArgs(
      item as DependencyObject, 
      args.Property, args.NewValue));
}
ObservableDependencyObjectCollection<T> クラスの最後にある OnItemDependencyPropertyChanged メソッドが DescriptorWrapper から呼び出され、ItemDependencyPropertyChanged イベントが発生します。これで、この演習全体の目的が達成されます。
ObservableDependencyObjectCollection<T> クラスは、型パラメータに基づいてコレクションを構築する時点で必要な DescriptorWrapper オブジェクトがすべて作成されるように構造化されています。この型パラメータを DependencyObject として指定し、パラメータを持たないコンストラクタを使用する場合、DependencyObject では自分の依存関係プロパティが定義されないため、DescriptorWrapper オブジェクトが作成されません。ただし、DependencyObject として型パラメータを指定し、一連の DependencyProperty オブジェクトを明示的に指定することで、通知が機能します。
より現実的なシナリオとしては、ObservableDependencyObjectCollection<T> の型パラメータを FrameworkElement に設定し、FrameworkElement のすべての派生クラス (TextBlock や Button など) をこのコレクションに入力することが考えられます。このコレクションでは、UIElement と FrameworkElement により定義された依存関係プロパティの変更のみが報告され、派生クラスで定義された依存関係プロパティの変更は報告されません。
柔軟性を高めるためには、項目が追加されたときに、項目の型および型により定義される依存関係プロパティに基づいて、新しい DescriptorWrapper オブジェクトを作成するようにコレクションを作成する必要があります。DescriptorWrapper オブジェクトを重複して作成しないように、最初にそれぞれの依存関係プロパティについて DescriptorWrapper が既に作成されていないかどうかをチェックする必要があります。
コレクションから項目を削除するとき、不要になった DescriptorWrapper オブジェクトを削除する場合があるでしょう。それには、各 DescriptorWrapper に使用カウンタを付加しておき、使用カウンタが 0 になったときに ObservableDependencyObjectCollection<T> の記述子コレクションから特定の DescriptorWrapper を削除する必要があります。
また、完全に柔軟なコレクション クラスを作成し、すべての項目のプロパティを監視することもできます。このとき、項目で依存関係プロパティが定義されていても、INotifyPropertyChanged インターフェイスを実装していてもかまいません。
このように、依存関係プロパティでも適切に扱うことで、Microsoft® .NET Framework のプロパティと同様にイベントを生成するように指定することができます。

ご意見やご質問は mmnet30@microsoft.com まで英語でお送りください。

Charles Petzold は MSDN Magazine の寄稿編集者です。最新の著書には『The Annotated Turing: A Guided Tour through Alan Turing's Historic Paper on Computability and the Turing Machine』(John Wiley & Sons Inc、2008 年) があります。

コミュニティ コンテンツ   コミュニティ コンテンツとは
新しいコンテンツの追加 RSS  注釈
Processing
Page view tracker