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:

11 thoughts on “MultiBinding in Xamarin.Forms”

  1. Hi!
    This is pure magic, thank you very much for your effort.
    This works great, I missed multi bindings so much in Xamarin and finally can use them! However I experienced a problem when using Converters for the binded values and want to propose a solution to it (I’m using the approach with the fake assembly name “Xamarin.Forms.UnitTests” from your repo):

    What causes the problem:
    ~~~~~~~~~~~~~~~~~~~~~

    What is the problem:
    ~~~~~~~~~~~~~~~~~
    If the converter is used, then the bindableObj.GetValues() in SetInternalValue will always contain old values, meaning that a change caused by the converter is not considered (SetInternalValue is invoked, but GetValues will deliver the old value). Then, the AllTrueConverter in the example above will receive wrong values (I didn’t really get why, the converter ANormalConverter is invoked and returns the correct value, but AllTrueConverter receives the old value then)

    How I fixed the problem for me:
    ~~~~~~~~~~~~~~~~~~~~~~~~~~
    1) Create an extra class for providing the BindingPropertyChangedDelegate in MultiBinding.cs, which will allow us to replace the value at the matching index:
    private sealed class PropertyChangedMappingProvider
    {
    public PropertyChangedMappingProvider(int index, MultiBinding reference)
    {
    this.ChangedDelegate = (bindableObj, o, n) =>
    {
    reference.SetInternalValue(bindableObj, index, o, n);
    };
    }

    public BindableProperty.BindingPropertyChangedDelegate ChangedDelegate;
    }

    2) Change the assignment of the BindingPropertyChangedDelegate in Method “Apply”:
    PropertyChangedMappingProvider changedProvider = new PropertyChangedMappingProvider(i, this);
    var property = BindableProperty.Create($”{nameof(MultiBinding)}Property-{Guid.NewGuid():N}”, typeof(object),
    typeof(MultiBinding), default(object), propertyChanged: changedProvider.ChangedDelegate);

    3) Change SetInternalValue:
    private void SetInternalValue(BindableObject source, int index = -1, object oldValue = null, object newValue = null)
    {
    if (source == null || isApplying) return;

    if (index >=0 && index < Properties.Length)
    {
    var values = source.GetValues(Properties);
    // Be sure to override the changed property with the new value
    values[index] = newValue;
    internalValue.Value = values;
    }
    else
    {
    internalValue.Value = source.GetValues(Properties);
    }
    }

    I struggled a lot to find the cause and I guess this is a very, very dirty fix, though it was interesting to debug through it and see how all glues together.
    Maybe you might consider to provide a more elegant solution than this in the future ;)

    Warm regards,
    Mathias Lackner

  2. After updating to Xamarin.Forms 2.5.1.444934 I can no longer build this project. I’m assuming that the InternalsVisibleTo attribute has been removed for the unit testing assembly.

    1. Although in browsing the source online, it appears to still be there. If that be the case, I’m not quite sure why upgrading to the latest Xamarin.Forms would cause the project to fail compilation.

    2. So I just now reverted back to Xamarin.Forms 2.5.0.280555 and the project compiles just fine once again. I’m perplexed.

    3. Well, I finally realized that an additional optional parameter was added to the Apply and Unapply methods.
      It works now after adding those!

      1. Hi Gregory,

        Sorry for the confusion. It sounds like you figured it out. I have updated the code in github and in this post to reflect the latest API in Xamarin.Forms 2.5.1

  3. Hi Kevin, Love the work on this one! But I can’t figure out where FirstNotNullConverter is defined? I am getting

    Additional information: Position 22:8. Type helper:FirstNotNullConverter not found in xmlns clr-namespace:Ximon.Helpers;assembly=Ximon

    Yes, I checked my namespace and it is OK.

    I copied the code directly from GitHub (I used the NON-SETTER version)

    And My XAML:

    ….

    Any ideas? Thanks!!!

    1. Hi Jim,

      The converter is a very simple class that simply grabs the first value that is not null. Here is the complete code for it:

      using System;
      using System.Globalization;
      using System.Linq;
      using Xamarin.Forms.Proxy;

      namespace MultiBindingExample
      {
      public class FirstNotNullConverter : IMultiValueConverter
      {
      public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
      {
      return values.FirstOrDefault(x => x != null) ?? parameter;
      }
      }
      }

Leave a Reply

Your email address will not be published. Required fields are marked *