Quantcast
Channel: Recent Threads — Xamarin Community Forums
Viewing all articles
Browse latest Browse all 204402

BindableProperty setter and getter in infinite loop

$
0
0

I've created a simple custom ViewCell subclass, and I've hit on a problem with bindings, specifically related to a slider and bindable properties.

The cell has the bindable properties MaxValue, MinValue, DoubleTapCommand and Value so it can be consumed as follows:

<TableSection Title="Distance from Sun (million km)">
    <TextCell Text="{Binding DistanceFromSun, StringFormat='{0:F0}', Mode=TwoWay}"/>
    <local:Numerical_Input_Cell
        MaxValue="1000"
        Value="{Binding DistanceFromSun, Mode=TwoWay}"
        DoubleTapCommand="{Binding DoubleTapCommand}"
        />
</TableSection>

where the binding context is a ViewModel. Looking at the cell itself,

<?xml version="1.0" encoding="UTF-8"?>
<ViewCell xmlns="http://xamarin.com/schemas/2014/forms"
          xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
          x:Name="ThisCell"
          x:Class="SimpleTableView.Numerical_Input_Cell">
    <ViewCell.View>
        <StackLayout Padding="4">
            <StackLayout Orientation="Horizontal" Padding="8">
                <Button Text="-" Clicked="Button_Reduce_Clicked" HorizontalOptions="Center"/>            
                <Slider Minimum="{Binding Source={x:Reference ThisCell}, Path=MinValue}"
                        Maximum="{Binding Source={x:Reference ThisCell}, Path=MaxValue}"
                        MinimumTrackColor="Blue"
                        MaximumTrackColor="Red"
                        HorizontalOptions="FillAndExpand"
                        x:Name="Slider"
                        Value="{Binding Value, Source={x:Reference ThisCell}, Mode=TwoWay}" />
                <Button Text="+" Clicked="Button_Increase_Clicked"  HorizontalOptions="Center"/>            
            </StackLayout>
            <Entry Text="{Binding Source={x:Reference Slider}, Path=Value, StringFormat='{0:F1}', Mode=OneWay}"
                   HorizontalOptions="FillAndExpand"
                   HorizontalTextAlignment="Center"
                   Completed="Entry_Completed"
                   x:Name="ValueEntry"
                   />
        </StackLayout>
    </ViewCell.View>
</ViewCell>

Note the Entry control which allows the user to enter a fractional value instead of using the slider (the idea being it allows more precision). Now the code behind:

namespace SimpleTableView
{
    [XamlCompilation(XamlCompilationOptions.Compile)]
    public partial class Numerical_Input_Cell : ViewCell
    {
        // ****************************** SLIDER *******************************
        public static readonly BindableProperty MinValueProperty =
            BindableProperty.Create(propertyName: "MinValue",
                            returnType: typeof(double),
                            declaringType: typeof(Numerical_Input_Cell),
                            defaultValue: 0.0);

        public double MinValue
        {
            get => (double)GetValue(MinValueProperty);
            set => SetValue(MinValueProperty, value);
        }

        public static readonly BindableProperty MaxValueProperty =
            BindableProperty.Create(propertyName: "MaxValue",
                            returnType: typeof(double),
                            declaringType: typeof(Numerical_Input_Cell),
                            defaultValue: 100.0);

        public double MaxValue
        {
            get => (double)GetValue(MaxValueProperty);
            set => SetValue(MaxValueProperty, value);
        }

        public static readonly BindableProperty ValueProperty =
            BindableProperty.Create(propertyName: "Value",
                            returnType: typeof(double),
                            declaringType: typeof(Numerical_Input_Cell),
                            defaultValue: 0.0);

        public double Value
        {
            get => (double)GetValue(ValueProperty);
            set => SetValue(ValueProperty, value);
        }

        // ************************** BUTTON EVENTS ****************************
        void Button_Reduce_Clicked(System.Object sender, System.EventArgs e) => Value -= (Value >= 100.0) ? 100.0 : 0.0;
        void Button_Increase_Clicked(System.Object sender, System.EventArgs e) => Value += (Value <= 900.0) ? 100.0 : 0.0;

        // ************************** ENTRY STRING ****************************
        void Entry_Completed(System.Object sender, System.EventArgs e)
        {
            //Validate
            double proposedValue;
            string strValue = ValueEntry.Text;
            bool parsed = double.TryParse(strValue, out proposedValue);
            if (parsed == false) return;
            if ((proposedValue >= MinValue) && (proposedValue <= MaxValue))
            {
                //THIS CAN CAUSE AN ENDLESS LOOP IN THE 'Value' getter/setter if set to a fractional number
                Value = proposedValue;
            }
        }

        // ****************************** GESURE ******************************
        public static readonly BindableProperty DoubleTapCommandProperty =
            BindableProperty.Create(propertyName: "DoubleTapCommand",
                                    returnType: typeof(ICommand),
                                    declaringType: typeof(Numerical_Input_Cell),
                                    defaultValue: null);
        public ICommand DoubleTapCommand
        {
            get => (ICommand)GetValue(DoubleTapCommandProperty);
            set => SetValue(DoubleTapCommandProperty, value);
        }


        // **************************** CONSTRUCTOR ***************************
        public Numerical_Input_Cell()
        {
            InitializeComponent();

            // ** Create Gesture Recogniser **
            var tapGestureRecognizer = new TapGestureRecognizer();
            tapGestureRecognizer.NumberOfTapsRequired = 2;
            tapGestureRecognizer.Tapped += (s, e) =>
            {
                if ((DoubleTapCommand != null) && DoubleTapCommand.CanExecute(null))
                {
                    DoubleTapCommand.Execute(null);
                }
            };
            //Attach gesture recogniser to the view
            View.GestureRecognizers.Add(tapGestureRecognizer);
        }

    }
}

Note
There is only a one-way binding from the slider to the Entry box. I did not want the slider updating with every key entry, so instead I've used an event handler. When the entry box is edited and the keyboard dismissed, this is handled in the following event handler:

        void Entry_Completed(System.Object sender, System.EventArgs e)
        {
            //Validate
            double proposedValue;
            string strValue = ValueEntry.Text;
            bool parsed = double.TryParse(strValue, out proposedValue);
            if (parsed == false) return;
            if ((proposedValue >= MinValue) && (proposedValue <= MaxValue))
            {
                //THIS CAN CAUSE AN ENDLESS LOOP IN THE 'Value' getter/setter if set to a fractional number
                Value = proposedValue;
            }
        }

Here is the issue:
For the most part, everything seems to work as expected. I move the slider, the text in the Entry box updates. If I enter a text value in range and dismiss the keyboard, and it's a whole number, the slider moves accordingly.

If I enter a fractional value, the UI freezes. I've discovered that the following bindable property Value setter/getter pair get stuck in an endless loop.

        public double Value
        {
            get => (double)GetValue(ValueProperty);
            set => SetValue(ValueProperty, value);
        }

The issue seems to be related to the slider and some form of rounding. Here is a clue:

If I replace the line Value = proposedValue; to Slider.Value = proposedValue; it no longer hangs, but the value gets rounded to the nearest integer despite Slider.Value being of type double

I can work around all of this by only setting integer values on the slider and scaling output etc., but I'm curious to know if anyone knows why this is happening and more importantly, whether I am doing somewhere here that invalidates the bindings / breaks the APIs?


Viewing all articles
Browse latest Browse all 204402

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>