Earlier this week we found ourselves looking into a strange databinding problem, where things were working as expected on devices but not in the simulator. Differences between simulators and devices is certainly not uncommon, but I'm not sure I can recall a time where it was on the device that things were binding correctly, and the simulator that was showing problems. The usual suspect of being a linker issue was off the table, so we had to dig a little deeper.
My suspicions then turned to differences in build configurations between the two - the garbage collector, debugging symbols, profiling support, etc - but no amount of option toggling smoked anything out there either. The strangest part? If we built in Visual Studio and deployed to the simulator, things worked. If we built with Xamarin Studio...no dice. Harumph.
At this point I was pretty confident it would end up being a garbage collection issue of some sort, and found that the binding was set up like this in a view model:
public void Init()
{
// ...other stuff...
option.Quantifiable.WeakSubscribe(
() => option.Quantifiable.Quantity,
(sender, args) => RaisePropertyChanged(() => Caption)));
}
If you've used MvvmCross before, you're probably familiar with the WeakSubscribe()
method, which can be great for subscribing to property changes on a model without creating strong references to it. In fact, if you've used this method before there's a good chance you already see the problem (or this post's title gave it away...it's ok, I'll give you the benefit of the doubt). The second argument to WeakSubscribe
is the event handler to use for the subscription, which in this case was a lambda expression that goes out of scope after Init()
finishes running.
Let's take a look at how weak subscriptions work in MvvmCross to see why this is important. When you call WeakSubscribe
you're given a MvxWeakEventSubscription
in return. When this subscription object is created, it creates its own event handler using OnSourceEvent
which will get called each time the source property is updated.
The implementation of this method is the key here:
protected void OnSourceEvent(object sender, TEventArgs e)
{
var target = _targetReference.Target;
if (target != null)
{
_eventHandlerMethodInfo.Invoke(target, new[] {sender, e});
}
else
{
RemoveEventHandler();
}
}
If the target is null, which is the event handler we supplied for this subscription, it removes the event handler and moves on. This is what was happening in our code...the lambda was going out of scope, and the next time the binding tried to update it would notice that and drop the handler, resulting in the property changes not bubbling up to our UI. The solution in this case was simple - use an actual method as the event callback:
public void Init()
{
// ...other stuff...
option.Quantifiable.WeakSubscribe(
() => option.Quantifiable.Quantity,
OnQuantityChanged);
}
private OnQuantityChanged(object sender, PropertyChangedEventArgs propertyChangedEventArgs)
{
RaisePropertyChanged(() => Caption);
}
Once that was updated things worked as expected. It's also worth noting that it's worth holding on to the subscription you get back from calls to WeakSubscribe
so that you can manage and dispose of them as appropriate for your applications. That was left out of this example for the sake of brevity.
This isn't rocket science, but I wanted to post it as a reminder that it's important to think about what's going on under the hood with this sort of thing and not get caught up the simplicity of a nice API. Lambda expressions are awesome and make code much more readable, but do come with a cost at times.