I’ve been meaning to write about this for a while since I’ve seen this approach mentioned lots of times on the newsgroups but have seen no mentions about the caveats. Just today I saw the same problem in some code from one of the disciples. The scenario is that you want to know when the value of a dependency property changes but you don’t have a one to one relationship with the object. For example, you have a list of items and you want to know when the IsSelected property of an item has changed.
The solution that I have seen given for this is to get to the PropertyDescriptor and use its AddValueChanged method to provide an EventHandler to receive a notification when the property has changed. Sometimes, the reply will mention/use DependencyPropertyDescriptor directly but its the same thing since that is just a derived PropertyDescriptor that provides additional information about the underlying DependencyProperty it represents. You can get to this property descriptor in a few ways but the most common are to get it from the TypeDescriptor.GetProperties method or using the DependencyPropertyDescriptor.FromProperty.
The issue with this approach is that it will root your object so it will never get collected by the GC. There have been plenty of discussions about how hooking events (particularly static events) can root your object so I won’t go into great detail there. While it does not seem that you are hooking a static event in this case, in essence you are. When you add a handler to a property descriptor, that property descriptor stores the delegate in a hashtable keyed by the object whose property you are hooking. A delegate/handler is basically a pointer to a method on an object (or no object if its for a static method) so that means the property descriptor has a reference to your object as well as the object whose value you are watching (since that is the key into the hashtable). The property descriptors themselves are cached statically so the hashtable is kept around and therefore your object and the one you are watching are as well.
I personally like to use the Scitech memory profiler (or you can use the Microsoft CLR Profiler) when debugging memory leak issues but you can see the issue manifest itself pretty easily in this case. First we’ll do a benchmark to make sure that we can check whether the object was collected.
If you run this code and check isAlive, you will see that it returns false indicating that the ListBoxItem was not referenced and was able to be collected. Now let’s try using the AddValueChanged method.
This time isAlive returns true because the property descriptor is maintaining a reference to the ListBoxItem. Now, let’s try an alternative approach that involves creating a helper class to listen for the property change.
In this case isAlive is false indicating that the object can be collected even though we’re still maintaining an explicit reference to the notifier. The implementation for the class – PropertyChangeNotifier – is listed below. The class is basically a simple DependencyObject that exposes 2 properties – Value returns the value of the property of the object that it is watching and PropertySource returns the object whose property it is watching. The constructor for the object takes the object whose property is to be watched for changes and the property that should be watched. This class takes advantage of the fact that bindings use weak references to manage associations so the class will not root the object who property changes it is watching. It also uses a WeakReference to maintain a reference to the object whose property it is watching without rooting that object. In this way, you can maintain a collection of these objects so that you can unhook the property change later without worrying about that collection rooting the object whose values you are watching.
Another possible approach would be to implement your own derived WeakEventManager but I’ll leave that as an exercise for the reader. For those that choose to go this route, there is such a class – except its internal – in the PresentationFramework assembly.