John Livermore
John Livermore

Reputation: 31313

XAML - IValueConverter and Behavior conflict with each other causing an endless loop to occur

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.

  1. The user must be able to enter an empty value for the int.
  2. The user is only allowed to enter an integer value.

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?

Steps to Reproduce

  1. Run the demo as found in the github repo below
  2. Enter '05' into the input box and app freezes in an endless loop

Reproduction Link

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

Answers (2)

Sir Rufo
Sir Rufo

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

Pratik
Pratik

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

Related Questions