Receiving notifications for dependency property changes on an existing object is a very common scenario. The way to do it properly is not very obvious. So much so that while reviewing some code, I found the following snippet.
// Believe it or not, this seems to be the only way to get change
// notifications for DPs unless you derive from the relevant
// class and override OnPropertyChanged.
PropertyDescriptor prop = TypeDescriptor.GetProperties(obj)["Prop"];
prop.AddValueChanged(obj, delegate { viewModel.RaisePropertyChanged("Prop"); });
There's a few issues with this code. The first one is that you reflect on the CLR property anchoring the dependency property, and not the dependency property itself. For example, the following code wouldn't work.
public Dock Dock { get { return DockPanel.GetDock(this); } }
public void TestDockProperty()
{
PropertyDescriptor descriptor = TypeDescriptor.GetProperties(this)["Dock"];
descriptor.AddValueChanged(this, delegate(object sender, EventArgs args) { MessageBox.Show("ValueChanged!"); });
DockPanel.SetDock(this, Dock.Top);
}
The reason is that the PropertyDescriptor points to the CLR property, not to the dependency property.
The second issue is a problem of performance. TypeDescriptor.GetProperties reflects on every call and doesn't cache the result, so its cost is O(n). Here's the result of iterating several times on the code using TypeDescriptor.
- 1,000 iterations : 00:00:00.2811402
- 10,000 iterations : 00:00:01.4369388
- 100,000 iterations : 00:00:14.1194856
So what is the correct way to do it? Say hi to DependencyTypeDescriptor. Here's the code rewritten to use DependencyProperties.
DependencyPropertyDescriptor prop = DependencyPropertyDescriptor.FromProperty(ParentObject.PropProperty, obj.GetType());
prop.AddValueChanged(obj, delegate { viewModel.RaisePropertyChanged("Prop"); });
If you execute the code in the small benchmark application we used previously, the results are completely different.
- 1,000 iterations : 00:00:00.00
- 10,000 iterations : 00:00:00.00
- 100,000 iterations : 00:00:00.0312378
As you can see in the source code, I simply use a DateTime before and after the call, and everything runs on the UI thread.
And the source is stored on box.net for those that want a peek. Be aware it's a visual Studio 2008 solution and project.
Download the source.