When I first started out with MonoTouch and started wiring up event handlers it was fantastic. I could do a lot of things without having to go to much trouble or setting up of UIxxx delegates. Grand stuff. Then I started noticing that objects weren’t cleaned up and garbage collected when I thought they would be. What was going on?
It turns out that there were two issues I was running into, one of which has to do with event handlers and user defined types. The other is a bug with UINavigationControllers that I can’t do a lot about except be aware.
Allow me to outline the issue with the event handlers (click here for the gist). What happens in this sample is that when you click the test button you are navigated to a page with a UITableView with two rows. In one of the rows is a UITableViewCell with an embedded UITextField and the other has a custom UIControl embedded in it. Both have an event handler on their embedded controls.
Navigating back to the initial view and the back to the test view again (see that bug with the navigation controller for why) and in the debug output we can see that the cell with the UITextField is disposed but not the cell with the custom control.
The reason for this is that there is some special handling for the “built-in” types that wrap Objective-C objects. The UITextField object that we get to a reference to is purely a wrapper for the real underlying Objective-C / CocoaTouch (native) object. The garbage collector can collect the c# object whenever it needs to and the MonoTouch runtime can create a new one representing that same native object at anytime and not loose data.
The same is not true for any class that the we define. For our classes the GC cannot clean them up until the native object can be released, which it normally can do.
In the sample above however, we create a cycle between the UITableViewCell and our custom UIControl class (the cell owns the custom control, the custom control has a reference to the cell via the event handler and the custom control is added as a subview to the cell) and the GC cannot detect when to collect our cell and custom control and they stay in memory.
What we need is a lightweight way to break the cycle that is caused by the event handler, the custom control mustn’t have a direct reference to its owning control. To achieve this I use a WeakReference and wire the event handler up to a static method.
The class below takes a weak reference to the delegate of the event handler and an action to be called when the event is raised. You should use either a static method on the delegate or a lambda that does not reference “this” - the action that is called will pass the delegate instance back to the action (as long as it is still alive).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
You use the event proxy with a static method
1 2 3 4 5 6 7 8 9 10 11
or a lambda, just don’t reference “this” in the expression.
1 2 3 4 5
It is possible to retain a reference to the proxy object and call dispose on it if the event should no longer be handled. The proxy itself won’t be GC’d until the object that it is the delegate of is collected, but that is the only real issue. An action to call to unhook the event handler could be added but I find that I never call it as I want the event raised as long as the source is alive anyway.
You could use this proxy on any event handler, or just in those cases where there is likely to be an issue - which is any user defined class with an event handler that is retained by a native Objective-C object. I’ve toyed around with the idea of using them everywhere and injecting the ability to have events raised in asynchronously but haven’t had a chance to pursue it much further.
UPDATE: I meant to add last night when I wrote this that I had asked about this issue with Xamarin support and they were really helpful in answering my questions. I really appreciate the effort they go to. They also pointed out that only types defined in monotouch.dll are treated as a built-in types - if you were to create a binding project and bind a third party library and wire up events then you might find this issue.