Search code examples
c#xamarinxamarin.formsprismivalueconverter

Xamarin Forms IValueConverter: Handle converting error


I have a IValueConverter which converts a byte[] to a string and vice versa. The converter is only able to convert it from an user given string to a byte when the string is correctly formated. Right now I just return the original object when the conversion fails which will give me a

Binding: '4' can not be converted to type 'System.Byte[]'.

error in the log. This is okay, but I would like to inform the user that the string he wrote is not correctly formatted by showing an red border arround the Editor and disable the "Send" Button.

Is it possible to tell the UI by MVVM pattern (PRISM) that the conversion has failed? In WPF there is a ValidationRule which can be used, I hadn't found anything similar for Xamarin.

The converter:

public class ByteArrayConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        if (value is byte[] b)
            return BitConverter.ToString(b);//Success
        return value;//Failed
    }

    public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        if (value is string str && (str.Length - 2) % 3 == 0)
        {
            int len = (str.Length + 1) / 3;
            byte[] byteArray = new byte[len];

            for (int i = 0; i < len; i++)
                byteArray[i] = System.Convert.ToByte(str.Substring(3 * i, 2), 16);
            return byteArray;//Success
        }
        return value;//Failed
    }
}

The XAML:

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:prism="http://prismlibrary.com"
             xmlns:vc="clr-namespace:XXX.ValueConverter"
             prism:ViewModelLocator.AutowireViewModel="True"
             x:Class="XXX.Views.Device.SendMessagePage">
    <ContentPage.Resources>
        <vc:ByteArrayConverter x:Key="byteArrayConverter"/>
    </ContentPage.Resources>
    <Editor Text="{Binding Payload, Converter={StaticResource byteArrayConverter}}"></Editor>
    
</ContentPage>

Solution

  • For Xamarin.Forms, you can use IValidationRule to do this.

    Firstly, creating a class that derives from the IValidationRule interface to are specify Validation rules.

     public interface IValidationRule<T>
    {
        string ValidationMessage { get; set; }
        bool Check(T value);
    }
    
     public class IsNotNullOrEmptyRule<T> : IValidationRule<T>
    {
        public string ValidationMessage { get; set; }
    
        public bool Check(T value)
        {
            if (value == null)
            {
                return false;
            }
    
            var str = $"{value }";
            return !string.IsNullOrWhiteSpace(str);
        }
    }
    
    public class HasValidAgeRule<T> : IValidationRule<T>
    {
        public string ValidationMessage { get; set; }
    
        public bool Check(T value)
        {
            if (value is DateTime bday)
            {
                DateTime today = DateTime.Today;
                int age = today.Year - bday.Year;
                return (age >= 18);
            }
    
            return false;
        }
    }
    

    Secondly, adding Validation Rules to a Property.

    public interface IValidatable<T> : INotifyPropertyChanged
    {
        List<IValidationRule<T>> Validations { get; }
    
        List<string> Errors { get; set; }
    
        bool Validate();
    
        bool IsValid { get; set; }
    }
    
     public class ValidatableObject<T> : IValidatable<T>
    {
        public event PropertyChangedEventHandler PropertyChanged;
    
        public List<IValidationRule<T>> Validations { get; } = new List<IValidationRule<T>>();
    
        public List<string> Errors { get; set; } = new List<string>();
    
        public bool CleanOnChange { get; set; } = true;
    
        T _value;
        public T Value
        {
            get => _value;
            set
            {
                _value = value;
    
                if (CleanOnChange)
                    IsValid = true;
            }
        }
    
        public bool IsValid { get; set; } = true;
    
        public virtual bool Validate()
        {
            Errors.Clear();
    
            IEnumerable<string> errors = Validations.Where(v => !v.Check(Value))
                .Select(v => v.ValidationMessage);
    
            Errors = errors.ToList();
            IsValid = !Errors.Any();
    
            return this.IsValid;
        }
        public override string ToString()
        {
            return $"{Value}";
        }
    }
    
     public class validationmodel: INotifyPropertyChanged
    {
        public ValidatableObject<string> FirstName { get; set; } = new ValidatableObject<string>();
        public ValidatableObject<string> LastName { get; set; } = new ValidatableObject<string>();
        public ValidatableObject<DateTime> BirthDay { get; set; } = new ValidatableObject<DateTime>() { Value = DateTime.Now };
        public validationmodel()
        {
    
            FirstName.Value = null;
            
            
            AddValidationRules();
            AreFieldsValid();
        }
    
        public event PropertyChangedEventHandler PropertyChanged;
    
        public void AddValidationRules()
        {
            FirstName.Validations.Add(new IsNotNullOrEmptyRule<string> { ValidationMessage = "First Name Required" });
            LastName.Validations.Add(new IsNotNullOrEmptyRule<string> { ValidationMessage = "Last Name Required" });
            BirthDay.Validations.Add(new HasValidAgeRule<DateTime> { ValidationMessage = "You must be 18 years of age or older" });
        }
    
        bool AreFieldsValid()
        {
            bool isFirstNameValid = FirstName.Validate();
            bool isLastNameValid = LastName.Validate();
            bool isBirthDayValid = BirthDay.Validate();
            return isFirstNameValid && isLastNameValid && isBirthDayValid;
        }
    
    
    
     }
    

    Highlighting a Control that Contains Invalid Data:

    public class FirstValidationErrorConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            ICollection<string> errors = value as ICollection<string>;
            return errors != null && errors.Count > 0 ? errors.ElementAt(0) : null;
        }
    
       
    
        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }
    
     public class InverseBoolConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            if (!(value is bool))
            {
                throw new InvalidOperationException("The target must be a boolean");
            }
    
            return !(bool)value;
        }
    
        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            return null;
        }
    }
    
    public class BehaviorBase<T> : Behavior<T>
    where T : BindableObject
    {
        #region Properties
        public T AssociatedObject
        {
            get;
            private set;
        }
        #endregion
        #region NormalMethods
        private void OnBindingContextChanged(object sender, EventArgs e)
        {
            OnBindingContextChanged();
        }
        #endregion
        #region Overrides
        protected override void OnAttachedTo(T bindable)
        {
            base.OnAttachedTo(bindable);
            AssociatedObject = bindable;
            if (bindable.BindingContext != null)
            {
                BindingContext = bindable.BindingContext;
            }
    
            bindable.BindingContextChanged += OnBindingContextChanged;
        }
        protected override void OnDetachingFrom(T bindable)
        {
            base.OnDetachingFrom(bindable);
            bindable.BindingContextChanged -= OnBindingContextChanged;
            AssociatedObject = null;
        }
        protected override void OnBindingContextChanged()
        {
            base.OnBindingContextChanged();
            BindingContext = AssociatedObject.BindingContext;
        }
        #endregion
    }
    
     public class EntryLineValidationBehaviour : BehaviorBase<Entry>
    {
        #region StaticFields
        public static readonly BindableProperty IsValidProperty = BindableProperty.Create(nameof(IsValid), typeof(bool), typeof(EntryLineValidationBehaviour), true, BindingMode.Default, null, (bindable, oldValue, newValue) => OnIsValidChanged(bindable, newValue));
        #endregion
        #region Properties
        public bool IsValid
        {
            get
            {
                return (bool)GetValue(IsValidProperty);
            }
            set
            {
                SetValue(IsValidProperty, value);
            }
        }
        #endregion
        #region StaticMethods
        private static void OnIsValidChanged(BindableObject bindable, object newValue)
        {
            if (bindable is EntryLineValidationBehaviour IsValidBehavior &&
                 newValue is bool IsValid)
            {
                IsValidBehavior.AssociatedObject.PlaceholderColor = IsValid ? Color.Default : Color.Red;
            }
        }
    
        #endregion
    }
    

    MainPage.xaml:

    <ContentPage
    x:Class="validationapp.MainPage"
    xmlns="http://xamarin.com/schemas/2014/forms"
    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
    xmlns:behaviour="clr-namespace:validationapp.Behaviors"
    xmlns:converter="clr-namespace:validationapp.converters"
    xmlns:local="clr-namespace:validationapp">
    <ContentPage.Resources>
       
        <converter:InverseBoolConverter x:Key="InverseBoolConverter" />
        <converter:FirstValidationErrorConverter x:Key="FirstValidationErrorConverter" />
        <Style x:Key="ErrorTextStyle" TargetType="Label">
            <Setter Property="TextColor" Value="Red" />
            <Setter Property="FontSize" Value="12" />
        </Style>
    </ContentPage.Resources>
    <StackLayout>
        <!--  First Name  -->
        <Entry Placeholder="First Name" Text="{Binding FirstName.Value}">
            <Entry.Behaviors>
                <behaviour:EntryLineValidationBehaviour IsValid="{Binding FirstName.IsValid}" />
            </Entry.Behaviors>
        </Entry>
    
        <Label
            IsVisible="{Binding FirstName.IsValid, Converter={StaticResource InverseBoolConverter}}"
            Style="{StaticResource ErrorTextStyle}"
            Text="{Binding FirstName.Errors, Converter={StaticResource FirstValidationErrorConverter}}" />
        <!--  /First Name  -->
    
        <!--  Last Name  -->
        <Entry Placeholder="Last Name" Text="{Binding LastName.Value}">
            <Entry.Behaviors>
                <behaviour:EntryLineValidationBehaviour IsValid="{Binding LastName.IsValid}" />
            </Entry.Behaviors>
        </Entry>
    
        <Label
            IsVisible="{Binding LastName.IsValid, Converter={StaticResource InverseBoolConverter}}"
            Style="{StaticResource ErrorTextStyle}"
            Text="{Binding LastName.Errors, Converter={StaticResource FirstValidationErrorConverter}}" />
        <!--  /Last Name  -->
    
    
        <!--  Birthday  -->
        <DatePicker Date="{Binding BirthDay.Value}" />
        <Label
            IsVisible="{Binding BirthDay.IsValid, Converter={StaticResource InverseBoolConverter}}"
            Style="{StaticResource ErrorTextStyle}"
            Text="{Binding BirthDay.Errors, Converter={StaticResource FirstValidationErrorConverter}}" />
        <!--  Birthday  -->
    </StackLayout>
    

    You can also Triggering Validation when Properties Change for Entry's Command.

    More detailed info about Validation, please take a look:

    https://learn.microsoft.com/en-us/xamarin/xamarin-forms/enterprise-application-patterns/validation