既存のDependencyPropertyの変更を検知する

自前でDependencyPropertyを実装する場合は、Register時にPropertyMetadataを渡すことで、値の変更時にコールバックを呼び出すことが可能となります。

しかし既存のDependencyPropertyの場合はすでにRegisterされているため、変更を検知するには別のアプローチが必要となります。

今回は2つのアプローチを実装してみたいと思います。

なお実用上は、既存のコントロールには対応するChangedイベントが用意されているため(ActualWidthならSizeChangedなど)、DependencyPropertyの変更の検知が必要なケースはほとんどないと思います。

DependencyPropertyDescriptor

RectangleとTextBlockがあり、RectangleのActualWidthがTextBlockに表示される例を考えます。
f:id:any-programming:20170710002225p:plain

FromPropertyでDependencyPropertyDescriptorを取得し、コールバックをAddValueChangedで登録しています。

var desc = DependencyPropertyDescriptor.FromProperty(Rectangle.ActualWidthProperty, typeof(Rectangle));
desc.AddValueChanged(rectangle, (s, e) => textblock.Text = rectangle.ActualHeight.ToString());

この方法の場合、RemoveValueChangedを呼び出さない限りコントロールの参照が持ち続けられるので、コントロールを動的に削除したりする時には注意が必要です。

DependencyObjectにBindingして検知

ターゲットのDependencyPropertyにBindingして変更を検知する仲介クラス、DependencyPropertyChangedを定義してみました。

仲介用のValueプロパティを持っており、値が変化したときに登録したコールバックを呼ぶようになっています。

またConditionalWeakTableにターゲットのコントロールと共に保持することにより、ターゲットと同じ寿命になるようにしてあります。

public class DependencyPropertyChanged<T> : DependencyObject
{
    public T Value
    {
        get { return (T)GetValue(ValueProperty); }
        set { SetValue(ValueProperty, value); }
    }
    public static readonly DependencyProperty ValueProperty =
        DependencyProperty.Register("Value", typeof(T), typeof(DependencyPropertyChanged<T>), 
            new PropertyMetadata(default(T), (obj, e) => (obj as DependencyPropertyChanged<T>).OnValueChanged()));

    static ConditionalWeakTable<DependencyObject, List<DependencyObject>> _instances = new ConditionalWeakTable<DependencyObject, List<DependencyObject>>();
    Action<T> _changed;
    public DependencyPropertyChanged(DependencyObject target, string path, Action<T> changed)
    {
        _changed = changed;
        _instances.GetOrCreateValue(target).Add(this);
        BindingOperations.SetBinding(this, DependencyPropertyChanged<double>.ValueProperty, 
            new Binding(path) { Source = target, Mode = BindingMode.OneWay });
    }
    void OnValueChanged() =>
        _changed(Value);
}
new DependencyPropertyChanged<double>(rectangle, nameof(rectangle.ActualWidth), x => textboxWidth.Text = x.ToString());

この方法であれば、コントロールを動的に削除しても参照が残り続けることはなくなります。