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?