Skip to content

MultiBinding in Xamarin.Forms

A Guide to Using the Current Xamarin.Forms Framework

The current release of Xamarin.Forms does not contain implementation for a MultiBinding object. For those of us who have a strong WPF background, this is a feature that would be very beneficial. With a little work, we can implement our MultiBinding class using the current Xamarin.Forms framework.

Creating a Basic MultiBinding

The existing Binding class is the glue that links any property on the binding’s source to a BindableProperty on the target BindableObject. Typically, this will be a property on a VisualElement. A simple binding might look like this:

<Label Text="{Binding Title}" />Code language: HTML, XML (xml)

This is great when we only want to bind a single value, but what about when we have multiple values we want to use. This is the problem that a MultiBinding is designed to solve when combined with a string format or a converter.

This is where we will start with our own MultiBinding class.

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Globalization;
using System.Linq;
using System.Runtime.CompilerServices;
using Xamarin.Forms;
using Xamarin.Forms.Proxy;
using Xamarin.Forms.Xaml;

[ContentProperty(nameof(Bindings))]
public class MultiBinding : IMarkupExtension
{
    public IList Bindings { get; } = new List();

    public string StringFormat { get; set; }

    public Binding ProvideValue(IServiceProvider serviceProvider)
    {
        return null;
    }

    object IMarkupExtension.ProvideValue(IServiceProvider serviceProvider)
    {
        return ProvideValue(serviceProvider);
    }
}

Since we can’t derive from the Xamarin.Forms Binding (sealed class) nor BindingBase (constructor is declared internal) classes, we will declare our MultiBinding class as a IMarkupExtension. It is important to use IMarkupExtenion rather than IMarkupExtension if you use the XAML Compile feature.

In the ProvideValue method, we will create the bindings and links that cause the MultiBinding to work. We will then monitor the dynamic BindableProperties we create for each Bindings collection binding and update our internal value when any of them change. We will create a Binding using this internal value as its source and return it as the MultiBinding.

private BindableObject _target;
private readonly InternalValue _internalValue = new InternalValue();
private readonly IList _properties = new List();

public Binding ProvideValue(IServiceProvider serviceProvider)
{
    if (string.IsNullOrWhiteSpace(StringFormat)) throw new InvalidOperationException($"{nameof(MultiBinding)} requires a {nameof(StringFormat)}");

    //Get the object that the markup extension is being applied to
    var provideValueTarget = (IProvideValueTarget)serviceProvider?.GetService(typeof(IProvideValueTarget));
    _target = provideValueTarget?.TargetObject as BindableObject;

    if (_target == null) return null;

    foreach (Binding b in Bindings)
    {
        var property = BindableProperty.Create($"Property-{Guid.NewGuid().ToString("N")}", typeof (object),
            typeof (MultiBinding), default(object), propertyChanged: (_, o, n) => SetValue());
        _properties.Add(property);
        _target.SetBinding(property, b);
    }
    SetValue();

    var binding = new Binding
    {
        Path = nameof(InternalValue.Value),
        Source = _internalValue
    };

    return binding;
}

private void SetValue()
{
    if (_target == null) return;
    var values = _properties.Select(_target.GetValue).ToArray();
    if (!string.IsNullOrWhiteSpace(StringFormat))
    {
        _internalValue.Value = string.Format(StringFormat, values);
        return;
    }
    _internalValue.Value = values;
}

private sealed class InternalValue : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    private object _value;
    public object Value
    {
        get { return _value; }
        set
        {
            if (!Equals(_value, value))
            {
                _value = value;
                OnPropertyChanged();
            }
        }
    }

    private void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}

Looking a bit deeper at the ProvideValue method, we first get access to the object that the MultiBinding is getting applied to by using the IProvideValueTarget service interface. We use this object to make calls to SetBinding and GetValue. The advantage of using this object is that our bindings will get the same BindingContext as the object that the MultiBinding is applied to.

For each of our bindings, we create a bindable property as their target. For our internal value, there is a private inner class that implements INPC. This property could have been left on the MultiBinding itself, but since it needs to be public, I find that hiding it away in an internal class keeps the API of the MultiBinding cleaner.

Adding IMultiValueConverter

If we run the app, we can see the MultiBinding works with string formats, but this is only a fraction of the functionality that the WPF MultiBinding provides. In WPF, we can specify an IMultiValueConverter to synthesize the bindings’ values into a single value. Let’s add similar functionality to our own MultiBinding class.

First, we declare the converter interface. MultiBindings are mostly used to convert from the source to the target, so we will only concern ourselves with one-way conversions.

public interface IMultiValueConverter
{
    object Convert(object[] values, Type targetType, object parameter, CultureInfo culture);
}
Code language: PHP (php)

Next, we will add properties for the converter and its parameter to the MultiBinding, and update the relevant portions of the ProvideValue method.

public IMultiValueConverter Converter { get; set; }

public object ConverterParameter { get; set; }

public Binding ProvideValue(IServiceProvider serviceProvider)
{
    if (string.IsNullOrWhiteSpace(StringFormat) && Converter == null)
        throw new InvalidOperationException($"{nameof(MultiBinding)} requires a {nameof(Converter)} or {nameof(StringFormat)}");

    ...

    var binding = new Binding
    {
        Path = nameof(InternalValue.Value),
        Converter = new MultiValueConverterWrapper(Converter, StringFormat),
        ConverterParameter = ConverterParameter,
        Source = _internalValue
    };

    return binding;
}Code language: JavaScript (javascript)

In an effort to mimic WPF behavior, the converter is applied before the string format. We need to modify our SetValue method. Rather than applying the string format there, we will do it in our new MultiValueConverterWrapper.

private void SetValue()
{
    if (_target == null) return;
    _internalValue.Value = _properties.Select(_target.GetValue).ToArray();
}Code language: JavaScript (javascript)

In this new structure, we will always pass an array of values from our bound properties to the internal value. We then apply the string format and converter inside of the MultiValueConverterWrapper.

private sealed class MultiValueConverterWrapper : IValueConverter
{
    private readonly IMultiValueConverter _multiValueConverter;
    private readonly string _stringFormat;

    public MultiValueConverterWrapper(IMultiValueConverter multiValueConverter, string stringFormat)
    {
        _multiValueConverter = multiValueConverter;
        _stringFormat = stringFormat;
    }

    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        if (_multiValueConverter != null)
        {
            value = _multiValueConverter.Convert(value as object[], targetType, parameter, culture);
        }
        if (!string.IsNullOrWhiteSpace(_stringFormat))
        {
            var array = value as object[];
            // ReSharper disable once ConvertIfStatementToNullCoalescingExpression
            if (array != null)
            {
                value = string.Format(_stringFormat, array);
            }
            else
            {
                value = string.Format(_stringFormat, value);
            }
        }
        return value;
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

This allows us to create our multi-value converters that can translate an array of objects into our desired type. In the following example, our value converter selects the first non-null value from the array or the converter’s parameter if there are no non-null values. It then takes the value and applies a string format to it.

<VisualElement.Resources>
 <ResourceDictionary>
   <local:FirstNotNullConverter x:Key="FirstNotNullConverter" />
 </ResourceDictionary>
</VisualElement.Resources>

...

<Label>
 <Label.Text>
   <local:MultiBinding StringFormat="Hello {0}" Converter="{StaticResource FirstNotNullConverter}" ConverterParameter="(No Name)">
     <Binding Path="Person1" />
     <Binding Path="Person2" />
   </local:MultiBinding>
 </Label.Text>
</Label>Code language: HTML, XML (xml)

Handling Triggers, Styles, and Setters

It is worth pointing out that our MultiBinding, in its current state, will fail if it is used inside of a setter (applied either directly on a VisualElement’s trigger or as part of a Style). This is because in we are using the TargetObject from the IProvideValueTarget service. Because a setter is not a BindableObject the cast fails, and we have no BindableObject to use to set the bindings on. If all you care about is having a MultiBinding that you can apply directly to elements, you can stop here.

The following options are very hacky, kittens will be killed, and you may be devoured by a raptor.

With that warning out of the way, let’s continue.

Handling the case where the MultiBinding is used directly inside of a VisualElement’s trigger can be solved by accessing some internal members. The two classes that currently exist in Xamarin.Forms that implement the IProvideValueTarget interface also implement another internal interface IProvideParentValues interface.

namespace Xamarin.Forms.Xaml
{
    internal interface IProvideParentValues : IProvideValueTarget
    {
        IEnumerable ParentObjects { get; }    
    }
}Code language: JavaScript (javascript)

Using reflection, we could retrieve the parent objects from our IProvideValueTarget. We can simply grab the first BindableObject that we find in the ParentObjects. This will effectively handle the case where the MultiBinding is used within a Setter that is a child of the target element:

<Label> 
  <Label.Triggers>
    <DataTrigger Binding="{Binding HasFullName}" Value="True" TargetType="Label">
      <Setter Property="Text">
        <Setter.Value>
          <local:MultiBinding StringFormat="{}{0}, {1}">
            <Binding Path="LastName" />
            <Binding Path="FirstName" />
          </local:MultiBinding>
        </Setter.Value>
      </Setter>
    </DataTrigger>
  </Label.Triggers>
</Label>Code language: HTML, XML (xml)

This does not solve the case where the setter is not a child of the element (such is the case when used inside of a style):

<Style TargetType="Label" x:Key="LabelStyle">
  <Setter Property="Text" TargetType="Label">
    <Setter.Value>
      <local:MultiBinding StringFormat="{}{0}, {1}">
        <Binding Path="LastName" />
        <Binding Path="FirstName" />
      </local:MultiBinding>
    </Setter.Value>
  </Setter>
</Style>
...
<Label Style="{StaticResource LabelStyle}" />Code language: HTML, XML (xml)

This case is much more complicated. Because multiple elements can share a single style when a setter’s value derives from BindingBase it creates a shallow clone of the binding and applies the cloned binding to the target element. This is a problem for our current implementation because it would apply a shallow clone of the binding returned from the ProvideValue method. However, there is a solution.Examining the manifest of the Xamarin.Forms.Core assembly we can see that contains several InternalsVisibleToAttributes. Specifically, we are going to pick on this one:

[assembly: InternalsVisibleTo("Xamarin.Forms.Core.UnitTests")]Code language: JSON / JSON with Comments (json)

We can create our proxy PCL project with an Assembly name of “Xamarin.Forms.Core.UnitTests”. This project can be referenced by our main PCL project and will have access to all of the internal members of the Xamarin.Forms.Core assembly.

Screen Shot 2016-03-15 at 11.40.37 AM

This allows us to derive from BindingBase and implement a true MultiBindings class.

[ContentProperty(nameof(Bindings))]
public class MultiBinding : BindingBase
{
    private readonly BindingExpression _bindingExpression;
    private readonly InternalValue _internalValue = new InternalValue();
    private readonly IList _properties = new List();
    private bool _isApplying;
    private IMultiValueConverter _converter;
    private object _converterParameter;
    public IList Bindings { get; } = new List();

    public IMultiValueConverter Converter
    {
        get { return _converter; }
        set
        {
            ThrowIfApplied();
            _converter = value;
        }
    }

    public object ConverterParameter
    {
        get { return _converterParameter; }
        set
        {
            ThrowIfApplied();
            _converterParameter = value;
        }
    }
    ...
}

Rather than using a markup extension, we can simply derive from BindingBase directly. Because we are deriving from BindingBase we no longer need our StringFormat property since it is declared on the base class. The Converter and ConverterParameter properties are now implemented with a backing field so that we can throw if they get modified after the binding is applied.

public MultiBinding()
{
    Mode = BindingMode.OneWay;
    _bindingExpression = new BindingExpression(this, nameof(InternalValue.Value));
}Code language: JavaScript (javascript)

We also have access to the internal BindingExpression class. We will use it in a similar manner as Xamarin’s Binding class to get similar behavior.

internal override void Apply(object context, BindableObject bindObj, BindableProperty targetProperty,
    bool fromBindingContextChanged = false)
{
    if (Mode != BindingMode.OneWay)
        throw new InvalidOperationException($"{nameof(MultiBinding)} only supports {nameof(Mode)}.{nameof(BindingMode.OneWay)}");

    base.Apply(context, bindObj, targetProperty, fromBindingContextChanged);

    _isApplying = true;
    Properties = new BindableProperty[Bindings.Count];
    int i = 0;
    foreach (BindingBase binding in Bindings)
    {
        var property = BindableProperty.Create($"{nameof(MultiBinding)}Property-{Guid.NewGuid():N}", typeof(object),
            typeof(MultiBinding), default(object), propertyChanged: (bindableObj, o, n) =>
            {
                SetInternalValue(bindableObj);
            });
        Properties[i++] = property;
        bindObj.SetBinding(property, binding);
    }
    _isApplying = false;
    SetInternalValue(bindObj);

    _bindingExpression.Apply(_internalValue, bindObj, targetProperty);
}

internal override void Apply(bool fromTarget)
{
    base.Apply(fromTarget);
    foreach (BindingBase binding in Bindings)
    {
        binding.Apply(fromTarget);
    }
    _bindingExpression.Apply(fromTarget);
}

internal override void Unapply(bool fromBindingContextChanged = false)
{
    base.Unapply(fromBindingContextChanged);
    foreach (BindingBase binding in Bindings)
    {
        binding.Unapply(fromBindingContextChanged);
    }
    Properties = null;
    _bindingExpression?.Unapply();
}

internal override object GetSourceValue(object value, Type targetPropertyType)
{
    if (Converter != null)
        value = Converter.Convert(value as object[], targetPropertyType, ConverterParameter, CultureInfo.CurrentUICulture);
    if (StringFormat != null && value != null)
    {
        // ReSharper disable once ConvertIfStatementToNullCoalescingExpression
        if (value is object[] array)
        {
            value = string.Format(StringFormat, array);
        }
        else
        {
            value = string.Format(StringFormat, value);
        }
    }
    return value;
}

internal override object GetTargetValue(object value, Type sourcePropertyType)
{
    throw new InvalidOperationException($"{nameof(MultiBinding)} only supports {nameof(Mode)}.{nameof(BindingMode.OneWay)}");
}

private void SetInternalValue(BindableObject source)
{
    if (source == null || _isApplying) return;
    _internalValue.Value = source.GetValues(Properties);
}Code language: JavaScript (javascript)

The apply and unapply methods are invoked as the MultiBinding is applied to an object or when its binding context changes. Because we now know when we are being applied to an element, we can properly create our child properties and bindings. The child bindings use the actual context object, while the MultiBinding itself uses the InternalValue class as its source. Our previous MultiValueConverterWrapper is replaced by similar logic inside the GetSourceValue method used to provide a value from the MultiBinding to the target property.

internal override BindingBase Clone()
{
    var rv = new MultiBinding
    {
        Converter = Converter,
        ConverterParameter = ConverterParameter,
        StringFormat = StringFormat
    };
    rv._internalValue.Value = _internalValue.Value;

    foreach (var binding in Bindings.Select(x => x.Clone()))
    {
        rv.Bindings.Add(binding);
    }
    return rv;
}Code language: PHP (php)

Finally, the clone method solves the problem of the MultiBinding being used inside of a style’s setter. Because bindings are cloned when a style is applied to an element, deriving from BindingBase and implementing the Clone method allows the MultiBinding to work correctly.

Though Xamarin.Forms is certainly maturing; it still lacks some functionality that WPF developers would expect. It would be ideal if the library exposed more functionality and opportunities for developers to extend and implement these features. Hopefully, this will improve in the future. The full code can be found on GitHub. The code for the simple binding (the one that won’t work inside of setters) can be found on GitHub.

Tags:

Comments are closed.