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

  • If you make your markup extension implement both BindableObject and IMarkupExtension then it will carry its own model. You can declare R, G, B, A as BindableProperties with both getters/setters. You can declare a get only Color property. Then, you make the IMarkupExtension just serve up that final Color property.

    /// <summary>
    /// Provides the RgbColor XAML markup extension for creating colors from R, G, B, A bindable properties.
    /// </summary>
    /// <code>
    /// &gt;local:RgbColor R="{Binding ...}", G="{Binding ...}", B="{Binding ...}", A="{Binding ...}" /&lt;
    /// </code>
    [ContentProperty(nameof(R))]
    public class RgbColorExtension : BindableObject, IMarkupExtension<BindingBase>
    {
        public static readonly BindableProperty RProperty = BindableProperty.Create(nameof(R), typeof(float), typeof(RgbColorExtension),
            propertyChanged: (b, o, n) => ((RgbColorExtension)b).OnColorChanged());
        public float R
        {
            get { return (float)GetValue(RProperty); }
            set { SetValue(RProperty, value); }
        }
    
        public static readonly BindableProperty GProperty = BindableProperty.Create(nameof(G), typeof(float), typeof(RgbColorExtension),
            propertyChanged: (b, o, n) => ((RgbColorExtension)b).OnColorChanged());
        public float G
        {
            get { return (float)GetValue(GProperty); }
            set { SetValue(GProperty, value); }
        }
    
        public static readonly BindableProperty BProperty = BindableProperty.Create(nameof(B), typeof(float), typeof(RgbColorExtension),
            propertyChanged: (b, o, n) => ((RgbColorExtension)b).OnColorChanged());
        public float B
        {
            get { return (float)GetValue(BProperty); }
            set { SetValue(BProperty, value); }
        }
    
        public static readonly BindableProperty AProperty = BindableProperty.Create(nameof(A), typeof(float), typeof(RgbColorExtension),
            propertyChanged: (b, o, n) => ((RgbColorExtension)b).OnColorChanged());
        public float A
        {
            get { return (float)GetValue(AProperty); }
            set { SetValue(AProperty, value); }
        }
    
        public Color Color => Color.FromRgba(R / 255f, G / 255f, B / 255f, A / 255f);
    
        public void OnColorChanged() => OnPropertyChanged(nameof(Color));
    
        public object ProvideValue(IServiceProvider serviceProvider)
            => (this as IMarkupExtension<BindingBase>).ProvideValue(serviceProvider);
    
        BindingBase IMarkupExtension<BindingBase>.ProvideValue(IServiceProvider serviceProvider)
            => new Binding(nameof(Color), source: this);
    }