Reputation: 31313
With a Xamarin Forms application, I have an IValueConverter and Behavior that conflict with each other causing an endless loop to occur. I have created a simple application that demonstrates this problem that can be downloaded (link below), and I have included the relevant code below.
Here are the requirements I am trying to achieve in this scenario.
For #1, I use a nullable int in the backend model. If I were to use just an 'int', then the field would always end up with a '0' in it if it were cleared. So, the IValueConverter implementation StringToIntConverter is used to convert the value from a string to an int, and if an empty string is passed, the property is set to null.
For #2, the Behavior IntegerValidationBehavior inspects each keystroke and eliminates any non-integer values including periods. In addition, for this example, I only show the numeric keyboard. However, it allows some non-integer characters like the period, so the IntegerValidationBehavior is needed.
For normal inputs it works great. But if you start with a '0' and then enter another number, it goes haywire ending up in an endless loop. I have verified this on various XF versions as well as both iOS and Android platforms.
How would I change the code to meet my requirements?
https://github.com/JohnLivermore/SampleXamarinApp/tree/endlessloop
IntegerValidationBehavior
public class IntegerValidationBehavior : Behavior<Entry>
{
protected override void OnAttachedTo(Entry entry)
{
entry.TextChanged += OnEntryTextChanged;
base.OnAttachedTo(entry);
}
protected override void OnDetachingFrom(Entry entry)
{
entry.TextChanged -= OnEntryTextChanged;
base.OnDetachingFrom(entry);
}
private static void OnEntryTextChanged(object sender, TextChangedEventArgs args)
{
if (!string.IsNullOrWhiteSpace(args.NewTextValue))
{
//make sure all characters are numbers
var isValid = args.NewTextValue.ToCharArray().All(x => char.IsDigit(x));
((Entry)sender).Text = isValid ? args.NewTextValue : args.NewTextValue.Remove(args.NewTextValue.Length - 1);
}
}
}
StringToIntConverter
public class StringToIntConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value == null)
return "";
else
return ((int)value).ToString();
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
var val = value as string;
if (string.IsNullOrWhiteSpace(val))
return null;
else
{
var result = 0;
int.TryParse(val, out result);
return result;
}
}
}
XAML
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:d="http://xamarin.com/schemas/2014/forms/design"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:behaviors="clr-namespace:SampleApp"
mc:Ignorable="d"
x:Class="SampleApp.MainPage">
<StackLayout>
<Entry Keyboard="Numeric"
Text="{Binding Model.Length, Mode=TwoWay, Converter={StaticResource StringToInt}}">
<Entry.Behaviors>
<behaviors:IntegerValidationBehavior />
</Entry.Behaviors>
</Entry>
<Label Text="{Binding Model.LengthString}"
TextColor="Black" />
<Button Text="Process"
Command="{Binding Process}" />
</StackLayout>
</ContentPage>
Model
public class MainPageModel : FreshBasePageModel
{
public MainPageModel()
{
Model = new Model();
}
public Model Model { get; set; }
}
public class Model : INotifyPropertyChanged
{
private int? _length;
public int? Length
{
get { return _length; }
set { SetProperty(ref _length, value); }
}
protected virtual void OnPropertyChanged([CallerMemberName]string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
protected bool SetProperty<T>(ref T storage, T value, [CallerMemberName]string propertyName = null)
{
if (EqualityComparer<T>.Default.Equals(storage, value))
{
return false;
}
storage = value;
OnPropertyChanged(propertyName);
return true;
}
public event PropertyChangedEventHandler PropertyChanged;
}
Upvotes: 3
Views: 219
Reputation: 19096
I changed your code to
public class StringToIntConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value == null)
return null;
else
return ((int)value).ToString();
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
var val = value as string;
if ( int.TryParse( val, out var result ) )
return result;
else
return null;
}
}
public class IntegerValidationBehavior : Behavior<Entry>
{
protected override void OnAttachedTo(Entry entry)
{
entry.TextChanged += OnEntryTextChanged;
base.OnAttachedTo(entry);
}
protected override void OnDetachingFrom(Entry entry)
{
entry.TextChanged -= OnEntryTextChanged;
base.OnDetachingFrom(entry);
}
private static void OnEntryTextChanged(object sender, TextChangedEventArgs args)
{
if (args.NewTextValue != null)
{
//make sure all characters are numbers
var isValid = int.TryParse( args.NewTextValue, out _ );
if ( !isValid )
((Entry)sender).Text = args.OldTextValue; // = isValid ? args.NewTextValue : args.NewTextValue.Remove(args.NewTextValue.Length - 1);
}
}
}
and the endless loop is gone.
Upvotes: 0
Reputation: 720
Replace below methode with your OnEntryTextChanged Method in IntegerValidationBehavior file and check it's worked.
private static void OnEntryTextChanged(object sender, TextChangedEventArgs args)
{
if (!string.IsNullOrWhiteSpace(args.NewTextValue))
{
//make sure all characters are numbers
var isValid = args.NewTextValue.ToCharArray().All(x => char.IsDigit(x));
if (isValid && args.NewTextValue.Length > 1 && args.NewTextValue.StartsWith("0"))
return;
((Entry)sender).Text = isValid ? args.NewTextValue : args.NewTextValue.Remove(args.NewTextValue.Length - 1);
}
}
Upvotes: 2