Blend Behaviors + DependencyPropertyDescriptor = Memory Leak

March 14, 2013

no comments

In the past couple of weeks, I was taking part in a focused effort to find and fix memory-related issues in one of my client’s applications. In this post I want to write about a specific type of memory leak that I found more than once, which is related to Blend-behaviors.

As an example, consider the following behavior:

   1: public class MyMarginDependentBehavior : Behavior<FrameworkElement>

   2: {

   3:     private readonly DependencyPropertyDescriptor _marginPropertyDescriptor;

   4:  

   5:     public MyMarginDependentBehavior()

   6:     {

   7:         _marginPropertyDescriptor = DependencyPropertyDescriptor.FromProperty(FrameworkElement.MarginProperty, typeof(FrameworkElement));

   8:     }

   9:  

  10:     protected override void OnAttached()

  11:     {

  12:         base.OnAttached();

  13:         _marginPropertyDescriptor.AddValueChanged(AssociatedObject, OnMarginChanged);

  14:     }

  15:  

  16:     protected override void OnDetaching()

  17:     {

  18:         _marginPropertyDescriptor.RemoveValueChanged(AssociatedObject, OnMarginChanged);

  19:         base.OnDetaching();

  20:     }

  21:  

  22:     private void OnMarginChanged(object sender, EventArgs eventArgs)

  23:     {

  24:         // Do Something

  25:     }

  26: }

It is not important what the behavior is actually doing, only that in order for it to work, it needs to be notified when the Margin property of the AssociatedObject changes. It uses the DependencyPropertyDescriptor class to add an event handler to be called when the property’s value changes. The event handler is removed in the OnDetaching method, which is supposed to be called when the behavior is not used anymore.

At first glance, the code seems to be perfectly fine, but in fact, it hides a memory leak which is caused by the following two reasons:

  1. The AddValueChanged method adds a strong reference to the AssociatedObject in a static dictionary. This reference is removed only when the RemoveValueChanged method is called.
  2. The behavior’s OnDetaching method is never being called, which means that the RemoveValueChanged method is never being called as well.

While debugging the issue in WinDbg, You won’t see the behavior itself in the reference chain, because it is not the one that holds the reference to the AssociatedObject.

   1: 0:009> !gcroot 00000040800c7de0 

   2: HandleTable:

   3:     00000040fc3617c8 (pinned handle)

   4:     -> 0000004090011f10 System.Object[]

   5:     -> 00000040800fc318 System.Collections.Generic.Dictionary`2[[System.ComponentModel.PropertyDescriptor, System],[MS.Internal.ComponentModel.DependencyObjectPropertyDescriptor, WindowsBase]]

   6:     -> 000000408012b820 System.Collections.Generic.Dictionary`2+Entry[[System.ComponentModel.PropertyDescriptor, System],[MS.Internal.ComponentModel.DependencyObjectPropertyDescriptor, WindowsBase]][]

   7:     -> 000000408012af08 MS.Internal.ComponentModel.DependencyObjectPropertyDescriptor

   8:     -> 000000408012dfd8 System.Collections.Generic.Dictionary`2[[System.Windows.DependencyObject, WindowsBase],[MS.Internal.ComponentModel.PropertyChangeTracker, WindowsBase]]

   9:     -> 0000004080134b90 System.Collections.Generic.Dictionary`2+Entry[[System.Windows.DependencyObject, WindowsBase],[MS.Internal.ComponentModel.PropertyChangeTracker, WindowsBase]][]

  10:     -> 000000408013a7c8 System.Windows.Controls.Button

  11:     -> 00000040801bcad8 System.Windows.EffectiveValueEntry[]

  12:     -> 000000408013b408 System.Windows.ModifiedValue

  13:     -> 00000040800c7de0 BehaviorsMemoryLeak.Item

As you can see, the DependencyObjectPropertyDescriptor holds a reference to the AssociatedObject (a Button control in that case), which holds a reference to the view model (the Item class), probably via the DataContext property. The DependencyObjectPropertyDescriptor class itself is being referenced by a static dictionary. Because of that, the button, the view model, and the behavior, will never be garbage-collected.

Note that the usage of the DependencyPropertyDescriptor class is just one example of a memory leak that can be caused by a behavior. As a general rule, the behavior’s reference and the AssociatedObject’s reference should never be held by objects with longer lifetimes than the AssociatedObject itself (which is not the case with the static dictionary in the above example). You should never rely on the OnDetaching method to remove those references, because it is never being called.

To fix the above memory leak, you can use a wonderful solution that was posted online, in 2008, by Andrew Smith. It leverages the fact that the binding mechanism holds a WeakReference to the source object.

A second solution is to implement and use the WeakEventManager class (as described here), but in my experience, its purging semantics does not always call the StopListening method in the above scenario (Based on a quick code inspection, it seems that the WeakEventManager class calls the StopListening method for unreferenced handlers when a handler is added or removed, or when the event is raised, none of which happens in our scenario).

Another solution, which is not always possible, is to add a handler to direct events of the AssociatedObject. That way, the behavior’s reference will be held by the AssociatedObject itself, and not by the static dictionary. In the above example we cannot do it, because the FrameworkElement class does not have a MarginChanged event or something similar.

I hope that I’ve saved you some headache by pointing you in the right direction.

cross-posted from http://www.programmingtidbits.com/post/2013/03/13/Blend-Behaviors-And-DependencyPropertyDescriptor-Equals-Memory-Leak.aspx

Add comment
facebook linkedin twitter email

Leave a Reply

Your email address will not be published.

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

*