Skip to content

Instantly share code, notes, and snippets.

@Keboo
Last active September 17, 2020 07:18
Show Gist options
  • Star 8 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save Keboo/0d6e42028ea9e4256715 to your computer and use it in GitHub Desktop.
Save Keboo/0d6e42028ea9e4256715 to your computer and use it in GitHub Desktop.
A simple MultiBinding class for Xamarin.Forms. This "binding" only works when directly applied to an element.
/*
WARNING: This MultiBinding implementation only works when it is directly applied to its target property.
It will fail if used inside of a setter (such is the case when used within a trigger or style).
*/
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;
namespace MultiBindingExample
{
[ContentProperty(nameof(Bindings))]
public class MultiBinding : IMarkupExtension<Binding>
{
private BindableObject _target;
private readonly InternalValue _internalValue = new InternalValue();
private readonly IList<BindableProperty> _properties = new List<BindableProperty>();
public IList<Binding> Bindings { get; } = new List<Binding>();
public string StringFormat { get; set; }
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)}");
//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),
Converter = new MultiValueConverterWrapper(Converter, StringFormat),
ConverterParameter = ConverterParameter,
Source = _internalValue
};
return binding;
}
object IMarkupExtension.ProvideValue(IServiceProvider serviceProvider)
{
return ProvideValue(serviceProvider);
}
private void SetValue()
{
if (_target == null) return;
_internalValue.Value = _properties.Select(_target.GetValue).ToArray();
}
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));
}
}
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();
}
}
}
}
@SimonTod
Copy link

Hey this functionnality looks great. Do you think you could show a simple example on how to use it from Xamarin.Forms ?
Thanks

@jjTudu
Copy link

jjTudu commented Dec 21, 2017

Step 1. Basically include the namespace in XAML, where you have implemented above class, like below
xmlns:multi="clr-namespace:Mobi.Helpers"

Step 2. Use the multi wherever you need multibinding support, may use Converter or StringFormat

<!--Place you need-->
<TapGestureRecognizer.CommandParameter>
 <multi:MultiBinding StringFormat="" Converter="">
  <Binding Path=""/>
   <Binding Path=""/>
  </multi:MultiBinding>
   </TapGestureRecognizer.CommandParameter> 

Done!

@Cedware
Copy link

Cedware commented Jan 10, 2018

Thanks a lot, the missing MultiBinding really drove me crazy

@pcdus
Copy link

pcdus commented Feb 12, 2018

Hi,

Can we use this MultiBinding for an ObservableCollection?
I would like to highlight an item of this collection, according a test related to another Property of the ViewModel.
The ideal should be to pass a parameter to the Converter:
IsVisible="{Binding Key, Converter={StaticResource isVisibleConverter}, ConverterParameter={Binding .}}"
But it's not possible throught the ConverterParameter.

So I've tried this:

	<StackLayout Grid.Column="1"
				 Orientation="Horizontal">
		<StackLayout.IsVisible>
			<extensions:MultiBinding Converter="{StaticResource isVisibleMultipleConverter}">
				<Binding Path="Key" />
				<Binding Source="Key" />
				<Binding Path="." />
			</extensions:MultiBinding>
		</StackLayout.IsVisible>

But all the values returned in the Converter are still null...

@Istafein
Copy link

@Goldstrike Have you had any updates about this issue?

@HenKun
Copy link

HenKun commented Mar 5, 2018

Note that 1) the IMultiValueConverter class has to be copied from the Proxy project and 2) for .NetStandard projects < 2.0 must include nuget package of Sytem.ComponentModel to have access to IServiceProvider

@imagin-code
Copy link

This does work for the most part, but I noticed it fails once you start working with regular updates. For instance, if a bound property should update x times over a certain period, references will eventually turn up null (the MultiBinding is not nested in a trigger or style, either). In other words, it's great for one-time conversions, but that's it. If I find a fix for this, I'll post it here.

@imagin-code
Copy link

I've spent the last day trying to figure this out, but no luck. To reproduce the issue, set a timer and call OnPropertyChanged on the property to update periodically. MultiBinding.SetValue is called several times, but then stops; the timer, meanwhile, keeps running. I've tried rewriting MultiBinding several times, but I can't fix this!

@dupuisdavid
Copy link

dupuisdavid commented Jun 8, 2018

Hello,

I have strange result in my converter Convert method when I loop over the values object.
For example, I have 2 binded properties in XAML, but I have 3 items in values object collection, with null for first value...
Any idea?

Regards

@crisoe7
Copy link

crisoe7 commented Dec 10, 2019

I use the DataTemplate in "ItemsView"
I use in DataTrigger, but the BindingContext of my control is always null
<DataTemplate> <StackLayout> <Label > <Label.Text> <Binding Path="Index"/> //working normally </Label.Text> </Label> <Button> <Button.Triggers> <DataTrigger TargetType="Button" Value="True"> <DataTrigger.Binding> <local:MultiBinding Converter="{StaticResource tConverter}"> <Binding Source="{x:Reference caruselView}" Path="Position"/> //working normally <Binding Path="Index"/> //Always null </local:MultiBinding> </DataTrigger.Binding> <Setter Property="BackgroundColor" Value="#551133FF"/> </DataTrigger> </Button.Triggers> </Button> </StackLayout> </DataTemplate>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment