Search code examples
c#xamarinmauimarkup-extensionsbindableproperty

How to create markup extensions having bindable properties?


I want to have a markup extension called RgbColorExtension where R property is bindable to let me adjust its value with a slider.

The following attempt does not work. Changing the slider does not effect the R property.

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="Sandbox.MainPage"
             xmlns:local="clr-namespace:Sandbox"
             >
    <VerticalStackLayout>
        <Slider x:Name="slider" Maximum="1"/>
        <Label Text="{Binding Value,Source={x:Reference slider}}"/>
        <BoxView HeightRequest="200" WidthRequest="200">
            <BoxView.Color>
                <local:RgbColor R="{Binding Value,Source={x:Reference slider}}" G="0" B="0" A="0.5"/>
            </BoxView.Color>
        </BoxView>
        <BoxView 
            HeightRequest="200" 
            WidthRequest="200"
            Color="{local:RgbColor R={Binding Value,Source={x:Reference slider}},G=0.5,B=0.5,A=.5}"
            />
    </VerticalStackLayout>

</ContentPage>

namespace Sandbox;

public class RgbColorExtension : BindableObject, IMarkupExtension<Color>
{

    public static readonly BindableProperty RProperty =
        BindableProperty.Create(nameof(R), typeof(float), typeof(RgbColorExtension), 0.5f);

    public float R
    {
        get => (float)GetValue(RProperty);
        set => SetValue(RProperty, value);
    }


    public float G { get; set; }
    public float B { get; set; }
    public float A { get; set; }


    public Color ProvideValue(IServiceProvider serviceProvider)
    {
        return Color.FromRgba(R, G, B, A);
    }

    object IMarkupExtension.ProvideValue(IServiceProvider serviceProvider)
    {
        return (this as IMarkupExtension<Color>).ProvideValue(serviceProvider);
    }
}

What am I missing here?


Solution

  • Since you have 4 parameters, you can have your IMarkupExtension implement (1) BindableObject (which you've already done so), (2) MultiBinding, with (3) an IMultiValueConverter. The 4 float values can participate in the MultiBinding and the IMultiValueConverter converts those 4 float values into a Color value.

    Here's a working implementation:

    [ContentProperty("R")]
    public class RgbColorExtension : BindableObject, IMarkupExtension<BindingBase>, IMultiValueConverter
    {
        public static readonly BindableProperty RProperty
            = BindableProperty.Create(nameof(R), typeof(float), typeof(RgbColorExtension));
    
        public static readonly BindableProperty GProperty
            = BindableProperty.Create(nameof(G), typeof(float), typeof(RgbColorExtension));
    
        public static readonly BindableProperty BProperty
            = BindableProperty.Create(nameof(B), typeof(float), typeof(RgbColorExtension));
    
        public static readonly BindableProperty AProperty
            = BindableProperty.Create(nameof(A), typeof(float), typeof(RgbColorExtension));
    
        public float R
        {
            get { return (float)GetValue(RProperty); }
            set { SetValue(RProperty, value); }
        }
    
        public float G
        {
            get { return (float)GetValue(GProperty); }
            set { SetValue(GProperty, value); }
        }
    
        public float B
        {
            get { return (float)GetValue(BProperty); }
            set { SetValue(BProperty, value); }
        }
    
        public float A
        {
            get { return (float)GetValue(AProperty); }
            set { SetValue(AProperty, value); }
        }
    
        public object ProvideValue(IServiceProvider serviceProvider)
        {
            return (this as IMarkupExtension<BindingBase>).ProvideValue(serviceProvider);
        }
    
        BindingBase IMarkupExtension<BindingBase>.ProvideValue(IServiceProvider serviceProvider)
        {
            return new MultiBinding()
            {
                Converter = this,
                Mode = BindingMode.OneWay,
                Bindings = new Collection<BindingBase>
                {
                    new Binding(nameof(R), BindingMode.OneWay, null, null, null, this),
                    new Binding(nameof(G), BindingMode.OneWay, null, null, null, this),
                    new Binding(nameof(B), BindingMode.OneWay, null, null, null, this),
                    new Binding(nameof(A), BindingMode.OneWay, null, null, null, this)
                }
            };
        }
    
        public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
        {
            float R = (float)values[0];
            float G = (float)values[1];
            float B = (float)values[2];
            float A = (float)values[3];
            return Color.FromRgba(R, G, B, A);
        }
    
        public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }