Wednesday 14 July 2010

Microsoft .NET events and garbage collection

Scenario
We have created a fancy WPF (using XAML) desktop application which uses N-tier design philosophy (data tier, business logic tier, presentation tier) with data binding between the user interface and the business logic, utilising value converters.
The user interface is quite complex with lots of triggers and templates that change depending on whether the element has focus, or the mouse is hovering over it, or the data type it is bound to (for date types we swap to a calendar control, for boolean types we swap to check box, etc).
This works nicely and it all sits within an MDI (multiple document interface) style application. Each document is bound to one instance of our main business logic class. Of course this object has several properties that are collections of other objects, and these collections are bound to content presenters (a XAML term for something that can repeatedly apply the same template to each element in an arbitrary collection).
The properties of the business logic objects that are bound were "dependency" properties (a Microsoft implementation of an Apple Objective-C concept of bindable values dating back to the early nineties). Under the hood, this means the business objects implement the INotifyPropertyChanged interface which in turn means that they have a public event called PropertyChanged that is raised whenever one of the bindable properties are changed. This event has an argument containing the name of the property (i.e. a string!!! value) that has changed.
The user interface then subscribes to this event notification so that it knows when the displayed values need to be changed when the program logic updates these properties.
The problem
When the user closes one of these documents, you would expect to see a drop in the amount of allocated memory for the application. As it is, having one document open consumes about 200MB of memory, so when it is closed and we dispose of the business logic object instance, and remove the document from the visual tree, we expect the garbage collector to do its job and return that 200MB to the operating system. Can you guess why I'm blogging about this?
Not even a request to GC.Collect() has any effect.
It turns out (thanks Ants Profiler) that the data bindings are at fault. Specifically, the business layer objects are prevented from being disposed properly because the user interface bindings are still listening to the PropertyChanged events. The bindings themselves cannot be disposed because they are still referenced in the internal collection of PropertyChanged listeners (maintained in the business logic by auto-generated code). Catch 22.
The solution
Don't use Microsoft's dependency properties. I know it is so easy in Visual Studio to just type "propdp" then hit the Tab key and have an instant dependency property generated for you! Alas this is the last thing you should ever get in the habit of doing.
Instead, write your own light-weight bindable properties by implementing the INotifyPropertyChanged interface yourself. Declare your own PropertyChanged event. Manually handle the collection of event listeners in a List collection.
Implement the IDisposable interface (but you were doing this already, weren't you!) and in the Dispose() method, clear the list of event listeners. Problem solved; no more dangling references interfering with garbage collection!
It's a lot of work depending on the number of business layer classed that can be bound to the user interface, and in our case was over 100 classes and 3000 properties.
On the plus side, the performance of the application improves three-fold (lighter-weight binding takes far less time to set up) and the memory usage drops from 179MB to 9MB after a document is closed.

No comments:

Post a Comment