85

Is there any way to listen to changes of a DependencyProperty? I want to be notified and perform some actions when the value changes but I cannot use binding. It is a DependencyProperty of another class.

Dave Clemmer
  • 3,741
  • 12
  • 49
  • 72
Rasto
  • 17,204
  • 47
  • 154
  • 245

6 Answers6

162

This method is definitely missing here:

DependencyPropertyDescriptor
    .FromProperty(RadioButton.IsCheckedProperty, typeof(RadioButton))
    .AddValueChanged(radioButton, (s,e) => { /* ... */ });

Caution: Because DependencyPropertyDescriptor has a static list of all handlers in application every object referenced in those handlers will leak if the handler is not eventually removed. (It does not work like common events on instance objects.)

Always remove a handler again using descriptor.RemoveValueChanged(...).

StayOnTarget
  • 11,743
  • 10
  • 52
  • 81
H.B.
  • 166,899
  • 29
  • 327
  • 400
  • 71
    Be very carefull with this since it can easliy introduce memory leaks! Always remove a handler again using `descriptor.RemoveValueChanged(...)` – CodeMonkey Jan 21 '13 at 13:06
  • 7
    see details and an alternative approach (define new dependency property + binding) at http://agsmith.wordpress.com/2008/04/07/propertydescriptor-addvaluechanged-alternative/ – Ilya Serbis Jul 22 '13 at 11:32
  • 2
    This works for WPF (which is what this question is for). If you land here looking for a windows store solution, you need to use the binding trick. Found this blog post that might help: http://blogs.msdn.com/b/flaviencharlon/archive/2012/12/07/getting-change-notifications-from-any-dependency-property-in-windows-store-apps.aspx Probably also works with WPF (as mentioned in the above answer). – Gordon Jan 30 '15 at 23:37
  • @Lu55 If not to worry about disposing `PropertyChangeNotifier` objects described in Smith's article, will it cause memory leaks because of uncleared bindings? – Sam Mar 07 '16 at 14:41
  • 1
    I did RemoveValueChanged in the Dispose method of the view, to ensure it doesn't leak. Is that enough? – Kind Contributor Nov 08 '16 at 05:13
  • 3
    @Todd: I think the leak is the other way around, the view might keep your view-model alive due to the reference to the handler. When the view is disposing the subscription should disappear as well anyway. People are a bit too paranoid about leaks from event handlers i think, usually it is not an issue. – H.B. Nov 09 '16 at 16:17
  • 6
    @H.B. In this case `DependencyPropertyDescriptor` has static list of all handlers in application, so every object referenced in handler will leak. It does not work like common event. – ghord Aug 23 '17 at 10:15
64

If it's a DependencyProperty of a separate class, the easiest way is to bind a value to it, and listen to changes on that value.

If the DP is one you're implementing in your own class, then you can register a PropertyChangedCallback when you create the DependencyProperty. You can use this to listen to changes of the property.

If you're working with a subclass, you can use OverrideMetadata to add your own PropertyChangedCallback to the DP that will get called instead of any original one.

Clonkex
  • 3,373
  • 7
  • 38
  • 55
Reed Copsey
  • 554,122
  • 78
  • 1,158
  • 1,373
  • 11
    According to [MSDN](http://msdn.microsoft.com/en-us/library/ms597491.aspx) and my experience, _Some characteristics (of the supplied metadata ) ...Others, such as PropertyChangedCallback, are combined._ So your own PropertyChangedCallback will get called *in addition* to the existing callbacks, not *instead of*. – Marcel Gosselin Jul 21 '11 at 01:41
  • 1
    dead link? is it https://msdn.microsoft.com/library/ms745795%28v=vs.100%29.aspx now? – Simon K. Dec 08 '15 at 16:44
  • Would it be possible to add this clarification to the answer? I thought that the OverrideMetadata would replace the callbacks of the parent, and this was holding me back from using it. – username Oct 28 '19 at 00:54
  • 1
    I agree, this is not very clear: "the easiest way is to bind a value to it, and listen to changes on that value". An example would be very helpful – StayOnTarget Oct 31 '19 at 18:31
19

I wrote this utility class:

  • It gives DependencyPropertyChangedEventArgs with old & new value.
  • The source is stored in a weak reference in the binding.
  • Not sure if exposing Binding & BindingExpression is a good idea.
  • No leaks.
using System;
using System.Collections.Concurrent;
using System.Windows;
using System.Windows.Data;

public sealed class DependencyPropertyListener : DependencyObject, IDisposable
{
    private static readonly ConcurrentDictionary<DependencyProperty, PropertyPath> Cache = new ConcurrentDictionary<DependencyProperty, PropertyPath>();

    private static readonly DependencyProperty ProxyProperty = DependencyProperty.Register(
        "Proxy",
        typeof(object),
        typeof(DependencyPropertyListener),
        new PropertyMetadata(null, OnSourceChanged));

    private readonly Action<DependencyPropertyChangedEventArgs> onChanged;
    private bool disposed;

    public DependencyPropertyListener(
        DependencyObject source, 
        DependencyProperty property, 
        Action<DependencyPropertyChangedEventArgs> onChanged = null)
        : this(source, Cache.GetOrAdd(property, x => new PropertyPath(x)), onChanged)
    {
    }

    public DependencyPropertyListener(
        DependencyObject source, 
        PropertyPath property,
        Action<DependencyPropertyChangedEventArgs> onChanged)
    {
        this.Binding = new Binding
        {
            Source = source,
            Path = property,
            Mode = BindingMode.OneWay,
        };
        this.BindingExpression = (BindingExpression)BindingOperations.SetBinding(this, ProxyProperty, this.Binding);
        this.onChanged = onChanged;
    }

    public event EventHandler<DependencyPropertyChangedEventArgs> Changed;

    public BindingExpression BindingExpression { get; }

    public Binding Binding { get; }

    public DependencyObject Source => (DependencyObject)this.Binding.Source;

    public void Dispose()
    {
        if (this.disposed)
        {
            return;
        }

        this.disposed = true;
        BindingOperations.ClearBinding(this, ProxyProperty);
    }

    private static void OnSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var listener = (DependencyPropertyListener)d;
        if (listener.disposed)
        {
            return;
        }

        listener.onChanged?.Invoke(e);
        listener.OnChanged(e);
    }

    private void OnChanged(DependencyPropertyChangedEventArgs e)
    {
        this.Changed?.Invoke(this, e);
    }
}

using System;
using System.Windows;

public static class Observe
{
    public static IDisposable PropertyChanged(
        this DependencyObject source,
        DependencyProperty property,
        Action<DependencyPropertyChangedEventArgs> onChanged = null)
    {
        return new DependencyPropertyListener(source, property, onChanged);
    }
}
Johan Larsson
  • 17,112
  • 9
  • 74
  • 88
6

You could inherit the Control you're trying to listen, and then have direct access to:

protected void OnPropertyChanged(string name)

No risk of memory leak.

Don't be afraid of standard OO techniques.

Kind Contributor
  • 17,547
  • 6
  • 53
  • 70
6

There are multiple ways to achieve this. Here is a way to convert a dependent property to an observable, such that it can be subscribed to using System.Reactive:

public static class DependencyObjectExtensions
{
    public static IObservable<EventArgs> Observe<T>(this T component, DependencyProperty dependencyProperty)
        where T:DependencyObject
    {
        return Observable.Create<EventArgs>(observer =>
        {
            EventHandler update = (sender, args) => observer.OnNext(args);
            var property = DependencyPropertyDescriptor.FromProperty(dependencyProperty, typeof(T));
            property.AddValueChanged(component, update);
            return Disposable.Create(() => property.RemoveValueChanged(component, update));
        });
    }
}

Usage

Remember to dispose the subscriptions to prevent memory leaks:

public partial sealed class MyControl : UserControl, IDisposable 
{
    public MyControl()
    {
        InitializeComponent();

        // this is the interesting part 
        var subscription = this.Observe(MyProperty)
                               .Subscribe(args => { /* ... */}));

        // the rest of the class is infrastructure for proper disposing
        Subscriptions.Add(subscription);
        Dispatcher.ShutdownStarted += DispatcherOnShutdownStarted; 
    }

    private IList<IDisposable> Subscriptions { get; } = new List<IDisposable>();

    private void DispatcherOnShutdownStarted(object sender, EventArgs eventArgs)
    {
        Dispose();
    }

    Dispose(){
        Dispose(true);
    }

    ~MyClass(){
        Dispose(false);
    }

    bool _isDisposed;
    void Dispose(bool isDisposing)
    {
        if(_disposed) return;

        foreach(var subscription in Subscriptions)
        {
            subscription?.Dispose();
        }

        _isDisposed = true;
        if(isDisposing) GC.SupressFinalize(this);
    }
}
Andy Brown
  • 11,766
  • 2
  • 42
  • 61
MovGP0
  • 7,267
  • 3
  • 49
  • 42
1

If that is the case, One hack. You could introduce a Static class with a DependencyProperty. You source class also binds to that dp and your destination class also binds to the DP.

Dave Clemmer
  • 3,741
  • 12
  • 49
  • 72
Prince Ashitaka
  • 8,623
  • 12
  • 48
  • 71