xamarin.formsbindableproperty

ContentView -> Custom Frame Binding Not Working


I copied/wrote a class that inherits from Frame

public class Circle : Frame
{
    //private double _radius;

    public static readonly BindableProperty RadiusProperty = BindableProperty.Create(nameof(Radius), typeof(double), typeof(Circle), 126.0, BindingMode.TwoWay);
    public double Radius
    {
        get => (double)GetValue(RadiusProperty); //_radius;
        set
        {
            SetValue(RadiusProperty, value);
            OnPropertyChanged();
            AdjustSize();
        }
    }

    private void AdjustSize()
    {
        HeightRequest = Radius;
        WidthRequest = Radius;
        Margin = new Thickness(0,0,0,0);
        Padding = new Thickness(0, 0, 0, 0);
        CornerRadius = (float) (Radius / 2);
    }

    public Circle()
    {
        HorizontalOptions = LayoutOptions.Center;
    }
}

The consuming page defines these BinadableProperties

    public static readonly BindableProperty InnerColorProperty = BindableProperty.Create("InnerColor", typeof(Color), typeof(CircleProgressView), defaultValue: Color.FromHex("#34495E"), BindingMode.TwoWay);
    public Color InnerColor
    {
        get => (Color)GetValue(InnerColorProperty);
        set => SetValue(InnerColorProperty, value);
    }

    public static readonly BindableProperty InnerRadiusProperty = BindableProperty.Create("InnerRadius", typeof(double), typeof(CircleProgressView), 126.0, BindingMode.TwoWay);
    public double InnerRadius
    {
        get => (double)GetValue(InnerRadiusProperty);
        set => SetValue(InnerRadiusProperty, value);
    }

And uses the Circle like so

<components:Circle Grid.Row="0" BackgroundColor="{Binding InnerColor}" Radius="{Binding InnerRadius}" >

Alas, the bindable's setter, and hence AdjustSize(), is never called nor is the default value used. Instead of a circle I end up with a rectangle. The BackgroundColor, which is a property of Frame, binds and works fine.

If I remove the BindableProperty and leave behind a regular INotify property

public class Circle : Frame
{
    private double _radius;

    public double Radius
    {
        get => _radius;
        set
        {
            _radius = value;
            OnPropertyChanged();
            AdjustSize();
        }
    }

    private void AdjustSize()
    {
        HeightRequest = Radius;
        WidthRequest = Radius;
        Margin = new Thickness(0,0,0,0);
        Padding = new Thickness(0, 0, 0, 0);
        CornerRadius = (float) (Radius / 2);
    }

    public Circle()
    {
        HorizontalOptions = LayoutOptions.Center;
    }
}

The compiler complains if I keep the InnerRadius binding

Severity Code Description Project File Line Suppression State Error Position 17:92. No property, bindable property, or event found for 'Radius', or mismatching type between value and property. ...\Components\CircleProgressView.xaml 17

I can replace the Radius binding with a hardcoded value and it runs fine, a circle appears.

<components:Circle Grid.Row="0" BackgroundColor="{Binding InnerColor}" Radius="126" >

What's wrong with a BindableProperty in a regular C# class?


Solution

  • Firstly, we need to handle data in the property changed event of bindable property instead of the setter method of a normal property. So modify your Circle class like:

    public static readonly BindableProperty RadiusProperty = BindableProperty.Create(nameof(Radius), typeof(double), typeof(Circle), 125.0, BindingMode.TwoWay, propertyChanged: RadiusChanged);
    public double Radius
    {
        get => (double)GetValue(RadiusProperty); //_radius;
        set => SetValue(RadiusProperty, value);
    }
    static void RadiusChanged(BindableObject bindableObject, object oldValue, object newValue)
    {
        Circle circle = bindableObject as Circle;
        circle.HeightRequest = (double)newValue;
        circle.WidthRequest = (double)newValue;
        circle.CornerRadius = (float)((double)newValue / 2);
    }
    

    This is because we bind data in XAML we should manipulate the bindable property's changed event directly.

    Secondly, I saw you bound the property using the parent page's bindable property. Normally, we won't do that. We will consume a view model as the page's binding context and then bind the property to the binding context. However, if you do want to consume the parent page's bindable property as the Circle's binding context, try this way:

    <ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
                 xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
                 x:Class="Sample.SecondPage"
                 xmlns:components="clr-namespace:Sample"
                 x:Name="Page">
        <ContentPage.Content>
            <StackLayout>
                <components:Circle BackgroundColor="{Binding InnerColor, Source={x:Reference Page}}" Radius="{Binding InnerRadius, Source={x:Reference Page}}"/>
            </StackLayout>
        </ContentPage.Content>
    </ContentPage>
    

    Name your parent page first and change the circle's source to that.

    Here, I used a different default Radius value comparing to InnerRadius so the property changed event will be called at the initial time.