This blog contains the implementation of an observable binding in Xamarin.Forms using a MarkupExtension. I you want to know more about the reasoning behind this, take a look at this blog.
You can find the code here.
For other versions please visit:
The Plan
Before making a plan about observable bindings, let's think back about a normal classical binding:
<Entry Text="{Binding Name}"/>
This creates a Binder object that synchronizes the BindableProperty with the Property from the ViewModel. The binding object knows when either of them updates by registering to the PropertyChanged
event. Both BindableObject and the ViewModel implement INotifyPropertyChanged
.
Normally a MarkupExtension simply returns a value when ProvideValue()
is called. But the BindingExtension uses it to hook up the previously mentioned event handlers. We are going to do the same thing here. Except, we're going to subscribe to an observable.
The idea is to end up with code like this:
View
<Label Text="{o:Bind StringObservable, Mode=OneWay}"/>
<Entry Text="{o:Bind StringObserver, Mode=OneWayToSource}"/>
<Entry Text="{o:Bind StringSubject, Mode=TwoWay}"/>
ViewModel
public IObservable<string> StringObservable{ get; set; }
public IObserver<string> StringObserver{ get; set; }
public Subject<string> StringSubject { get; set; }
Our binding should be able to bind to any BindableProperty and support all BindingModes in Xamarin.Forms. To limit the scope, we're only going to consider BindingContext and ignore any other sources.
Creating the MarkupExtension
Let's get started by creating our MarkupExtension:
[ContentProperty("Path")]
class BindExtension : IMarkupExtension
{
private BindableObject bindingTarget;
private BindableProperty bindingProperty;
public string Path { get; set; }
public BindingMode Mode { get; set; }
public BindExtension() { }
public override object ProvideValue(IServiceProvider serviceProvider)
{
...
}
}
We create a class called BindExtension. By following the name convention for MarkupExtensions, that means that in XAML we can simply write o:Bind
where o is a prefix for the namespace.
We continue by creating a Path and a Mode property. We can simply re-use the BindingMode Type from the original BindingExtension. Notice how we set Path as the ContentProperty. This way, you don't have to write Path in the expression.
<Entry Text="{o:Bind Path=Name}"/>
<Entry Text="{o:Bind Name}"/> <!-- "Name" is used as the Path-->
If we want to listen to an observable and update the BindableProperty, we need to get a reference to both. Let's start with retrieving the BindableProperty and it's BindableObject using the IProvideValueTarget Service.
public override object ProvideValue(IServiceProvider serviceProvider)
{
var valueProvider = serviceProvider.GetService(typeof(IProvideValueTarget)) as IProvideValueTarget;
if (valueProvider != null)
{
bindingTarget = valueProvider.TargetObject as BindableObject;
bindingProperty = valueProvider.TargetProperty as BindableProperty;
...
}
return bindingProperty.DefaultValue;
}
ProvideValue()
needs to return a value immediately, we can simply return the default value for the current BindableProperty.
Great, that's all the information we need from the BindableProperty's side. Let's get a reference to the BindingContext.
public object ProvideValue(IServiceProvider serviceProvider)
{
var valueProvider = serviceProvider.GetService(typeof(IProvideValueTarget)) as IProvideValueTarget;
if (valueProvider != null)
{
bindingTarget = valueProvider.TargetObject as BindableObject;
bindingProperty = valueProvider.TargetProperty as BindableProperty;
bindingTarget.BindingContextChanged += BindingContextSource_BindingContextChanged;
var bindingContext = bindingTarget?.BindingContext;
SetupBinding(bindingContext);
}
return bindingProperty.DefaultValue;
}
The BindingContext might change over time that's why we need to register for the BindingContextChanged event. There, we will remove any previous setup and call SetupBinding()
for the new BindingContext. More about SetupBinding()
in the next part.
Once we have the BindableObject and its BindingContext, we need to resolve the path of the expression. We can use this little helper method:
private object GetObjectFromPath(object bindingContext, string path)
{
var properties = path.Split('.');
var current = bindingContext;
foreach (var prop in properties)
{
if (current == null) break;
current = current.GetType().GetRuntimeProperty(prop).GetValue(current);
}
return current;
}
Unfortunately, it requires reflection. But this is only done once per Binding as long as the BindingContext stays the same. This is exactly what the classical BindingExtension does.
Finally, let's have a look at what SetupBinding does:
private void SetupBinding(object source)
{
//sanity check
if (source == null) return;
var boundObject = GetObjectFromPath(source, Path);
if (boundObject == null) return;
//set default BindingMode
if (Mode == BindingMode.Default) Mode = bindingProperty.DefaultBindingMode;
//bind to Observable and update property
if (Mode == BindingMode.OneWay || Mode == BindingMode.TwoWay)
{
SetupListenerBinding(boundObject);
}
//send property values to Observer
if (Mode == BindingMode.OneWayToSource || Mode == BindingMode.TwoWay)
{
SetupEmitBinding(boundObject);
}
}
It basically goes through four steps:
- It receives the BindingContext and resolves the Path
- If the BindingMode is not set, it looks up the default BindingMode for this BindableProperty
- If the BindingMode indicates we need to update the View from the ViewModel, we assume that the bound object is an observable and set up a listening mechanism to update the view.
- If the BindingMode indicates we need to update the ViewModel from the View, we assume that the bound object is an observer and set up an emitter to send new values to the observer.
ViewModel → View
In this part we will create a mechanism to update our View based upon an observable. If we think back about classical Binding, the ViewModel usually implements INotifyPropertyChanged. So there, the BindingExtension simply subscribes to the PropertyChanged event. And when that event fires, it updates the view accoridingly. What we need to do is replace this event with a subscription to our observable.
Consider the following piece of code:
private void SetupListenerBinding(object observable)
{
SubscribePropertyForObservable((IObservable<object>)observable, bindingTarget, bindingProperty);
}
private void SubscribePropertyForObservable(IObservable<object> observable,
BindableObject d, BindableProperty property)
{
if (observable == null) return;
listenSubscription = observable.ObserveOn(SynchronizationContext.Current)
.Subscribe(val => d.SetValue(property, val));
}
This code brings us a long way. You can see that whenever a new value is emitted by the observable, the BindableObject will be updated by using SetValue()
.
Whether ObserveOn(SynchronizationContext.Current)
should be called here, is up for debate. By adding this, no matter which thread sends the new value, the BindableObject will always be updated on the UI Thread. But maybe that's the responsibility of the ViewModel and not this Binding. The classical Binding does not do this, but it is a simple small addition here.
But, there is a problem.
We assumed that our observable can be cast to type IObservable<object>
. Which seems to be ok. SetValue()
receives an object
anyway, so we don't care about the exact type. Plus IObservable<in T>
is covariant, which means: If A
inherits B
then IObservable<A>
inherits IObservable<B>
. That's great since everything inherits object
this should work.
But no, it doesn't.
Covariance and contravariance in generic type parameters are supported for reference types, but they are not supported for value types. (source)
So that means this mechanism doesn't work for e.g. double
and int
.
In order to fix this, we will need to exact type of the observable. SubscribePropertyForObservable
becomes:
private void SubscribePropertyForObservable<TProperty>(IObservable<TProperty> observable,
BindableObject d, BindableProperty property)
{
if (observable == null) return;
listenSubscription = observable.ObserveOn(SynchronizationContext.Current)
.Subscribe(val => d.SetValue(property, val));
}
And SetupListenerBinding becomes:
private void SetupListenerBinding(object observable)
{
//IObservable<T> --> typeof(T)
var observableGenericType = observable.GetType().GetTypeInfo()
.ImplementedInterfaces
.Single(type => type.IsConstructedGenericType && type.GetGenericTypeDefinition() == (typeof(IObservable<>)))
.GenericTypeArguments[0];
//get SubscribePropertyForObservable<> method
MethodInfo method = typeof(BindExtension).GetTypeInfo().DeclaredMethods
.Where(mi => mi.Name == nameof(SubscribePropertyForObservable))
.Single();
//SubscribePropertyForObservable<> --> SubscribePropertyForObservable<T>
MethodInfo generic = method.MakeGenericMethod(observableGenericType);
//invoke generic method SubscribePropertyForObservable<T>(observable, bindingTarget, bindingProperty)
generic.Invoke(this, new object[] { observable, bindingTarget, bindingProperty });
}
As you can see, we need reflection to get the exact Type. First we get T
from the implemented interface IObservable<T>
and then we need to call SubscribePropertyForObservable<T>
with that type. That also means we have to construct and call SubscribePropertyForObservable<T>
at runtime.
Once again we are forced to use reflection. But it does not impact the performance of the subscription, only the initial set up.
The listenSubscription
is kept in a global field, so it can be cleaned up when the BindingContext changes. When a Visual is removed from the Visual Tree, its BindingContext is set to null. So relying on the BindingContextChanged
event should suffice, and cause no memory leaks.
View → ViewModel
In this last part we will send changes in our view to an observer. In classical Binding, when a BindableProperty is modified, it simply sets the bound property on the BindingContext. Here, this Property will be an observer, and we'll have to call OnNext() when a new value arrives.
It will take two steps to reach our goal:
- Create an observable for the BindableProperty
- Subscribe the observer to that observable
First of all, how do you know that a BindableProperty received a new value? In Xamarin.Forms, BindableObject simply implements INotifyPropertyChanged, you can subscribe to the PropertyChanged
event:
public abstract class BindableObject : INotifyPropertyChanged, IDynamicResourceHandler
We can turn that into an observable as so:
public static IObservable<TProperty> Observe<TProperty>(this BindableObject bindableObject,
BindableProperty bindableProperty)
{
var propertyName = bindableProperty.PropertyName;
return Observable.FromEventPattern<PropertyChangedEventHandler, PropertyChangedEventArgs>(
handler => handler.Invoke,
h => bindableObject.PropertyChanged += h,
h => bindableObject.PropertyChanged -= h)
.Where(e => e.EventArgs.PropertyName == propertyName)
.Select(e => (TProperty)bindableObject.GetValue(bindableProperty));
}
This is an extension method that can be applied to any BindableObject. FromEventPattern
can turn any event into an observable. After specifying how to set and remove the handler, we added a filter for the property name. Remember that the PropertyChanged
event will fire for any property. But we are only interested in one. Finally, we don't want to return the EventArgs of PropertyChanged
, we should return the value for that property. We can use the GetValue()
method for that.
Subscribing to the observable is quite easy. However, we do need the exact type again for the same reason as before.
private void SubScribeObserverForProperty<TProperty>(IObserver<TProperty> observer,
BindableObject d, BindableProperty propertyToMonitor)
{
if (propertyToMonitor.DeclaringType.GetTypeInfo().IsAssignableFrom(d.GetType().GetTypeInfo())
&& observer != null)
{
emitSubscription = d.Observe<TProperty>(propertyToMonitor)
.Subscribe(observer);
}
}
Once more, emitSubscription
is kept in a global field, so it can be cleaned up when the BindingContext changes.
Finally, we need to get the type of IObsever<T>
. This is very easy since we can just use bindingProperty.ReturnType
. Calling the generic method means constructing it at runtime using reflection and then invoking it. Just like we did in the previous part.
private void SetupEmitBinding(object observer)
{
//get SubScribeObserverForProperty<> method
MethodInfo method = typeof(BindExtension).GetTypeInfo().DeclaredMethods
.Where(mi => mi.Name == nameof(SubScribeObserverForProperty))
.Single();
//SubScribeObserverForProperty<> --> SubScribeObserverForProperty<T>
MethodInfo generic = method.MakeGenericMethod(bindingProperty.ReturnType);
//invoke generic method SubScribeObserverForProperty<T>(observer, bindingTarget, bindingProperty)
generic.Invoke(this, new object[] { observer, bindingTarget, bindingProperty });
}
The Result
To test our BindingExtension, I made this very simple Xamarin.Forms application:
The View and ViewModel are as follows:
View
<StackLayout x:Name="sl">
<Label Text="{o:Bind StringStream, Mode=OneWay}"/>
<Label Text="{o:Bind StringSubject, Mode=OneWay}"/>
<Entry Text="{o:Bind StringSubject, Mode=OneWay}"/>
<Entry Text="{o:Bind StringSubject, Mode=OneWayToSource}"/>
<Entry Text="{o:Bind StringSubject, Mode=TwoWay}"/>
<Slider Value="{o:Bind DoubleSubject, Mode=TwoWay}" Maximum="100"/>
<Label Text="{o:Bind DoubleSubject, Mode=OneWay}"/>
<Button Text="remove children" Clicked="Button_Clicked"/>
</StackLayout>
ViewModel
class MainViewModel
{
public IObservable<string> StringStream { get; set; }
= Observable.Interval(TimeSpan.FromSeconds(1)).Select(i => i.ToString());
public Subject<string> StringSubject { get; set; } = new Subject<string>();
public Subject<double> DoubleSubject { get; set; } = new Subject<double>();
}
I remove all bound controls at the end to see it they are cleaned up correctly.
I'm very happy with this result. The code the developer has to write is elegant, and the performance is on par with the classical BindingExpression.
Any thoughts or constructive criticism is welcome.
Once again, You can find the code here.