It seems there is bug in the WPF animation architecture that can cause memory not to be released and causing performance and stability problems. The document describes when the problem occurs and why.
During the development of a WPF application, we noticed that a huge amount of memory was used after a while. After profiling with the excellent .NET Memory Profiler v3.1 we found out that a lot of objects were not collected by the garbage collector.
After some heavy investigation I found out that a FrameworkTemplate was holding references to a lot of my textboxes that should have been collected already. These textboxes held references to other objects, so the result was that a lot of memory was wasted. When the event triggers with actions were removed from the textbox’s control template, then the memory was released properly.
When does the problem occur?
The problem occurs in the following situation:
- The control has a control template defined.
- The control template uses triggers with enter/leave actions.
- The control has the ‘Collapsed’ visibility.
Why does the problem occur?
Triggers are active during the initialization of a control. If a property triggers an enter/leave action, then the action is started. The action is started via the internal “StyleHelper.InvokeActions” method. This method checks if the visual tree of the object is already constructed and if it is, then the actions are invoked immediately.
If the visual tree hasn’t been created yet, then the action is queued by the “StyleHelper.DeferActions”, so it can be processed later. To my surprise, the deferred actions are stored with the template and not the control itself (IMHO: This is a fundamental design flaw). If the visual tree is created, then the deferred actions will be invoked.
The visual tree is created during the public “FrameworkElement.ApplyTemplate” method. The framework only calls this method when measuring the control. So if the control is never measured, then the visual tree is not created and the deferred actions will remain queued forever.
The MSDN documentation for “FrameworkElement.MeasureOverride” clearly states:
Elements should call Measure on each child during this process, otherwise the child elements will not be correctly sized or arranged.
You might expect that all elements call “FrameworkElement.Measure” on their children, so during this process the “FrameworkElement.ApplyTemplate” is called. However, WPF doesn’t respect this rule, because “UIElement.Measure” doesn’t start measuring when the element has a collapsed visibility.
Collapsed UI elements are never measured and if they never will get the Visible or Hidden visibility, then the object is never measured and the template isn’t applied. This seems very logical, but it also causes the deferred actions never to be removed from the template.
Because the template keeps the reference to the control, the control is never garbage collected. WPF controls know their parents and children. They also reference other objects via databinding. The result is that a lot of objects are referenced by the control (direct or indirect) and are not garbage collected.
I have created a sample application that illustrates the problem. When the application is started, then you can see the deferred actions for the control template of the MySampleControl.
When you press the “Create Collapsed Expander”, the application creates a window that contains a collapsed expander that contains a MySampleControl. Because the MySampleControl is collapsed it isn’t constructed and you can see the control is added to the deferred actions list (after refreshing the list).
When you expand the expander on the window and refresh the deferred actions, then you can see that the control is removed from the list. If you close the window, without expanding the expander, then the control will stay on the list forever. Because the item is still on the list it is never collected.
You can force a garbage collection and in the debug window (when running in Visual Studio) you can see which objects are finalized. Windows with expanders that have never been opened will never be finalized.
I guess Microsoft made a design error by storing the deferred actions with the template. They should have been stored in the control itself. The deferred actions are only used when applying the template, which is an instance method of the control. So it would have had full access. Now a dictionary is used that connects objects that shouldn’t be connected.
Response from Microsoft
I have received a response from Microsoft and they confirm it is a bug. It is on the list to be fixed, but there is no releasedate for .NET 3.5 SP2 or .NET 4. So I guess we’ll have to wait.
Microsoft .NET Framework 4
Beta 1 of the Microsoft .NET Framework 4 has been released and it now uses a ConditionalWeakTable instead of a HybridDictionary. The keys are kept as weak references and when the key is collected by the GC, then the associated value is also released. This also solves this issue in our situation, because the dictionary doesn’t keep the objects alive anymore.
Download the sample application.