Validation in Xamarin using DataAnnotation
Asked Answered
P

4

20

I am trying to add validations in Xamarin. For that I have used this post as a reference point: Validation using Data Annotation. Following is my Behavior.

public class EntryValidationBehavior : Behavior<Entry>
    {
        private Entry _associatedObject;

        protected override void OnAttachedTo(Entry bindable)
        {
            base.OnAttachedTo(bindable);
            // Perform setup       

            _associatedObject = bindable;

            _associatedObject.TextChanged += _associatedObject_TextChanged;
        }

        void _associatedObject_TextChanged(object sender, TextChangedEventArgs e)
        {
            var source = _associatedObject.BindingContext as ValidationBase;
            if (source != null && !string.IsNullOrEmpty(PropertyName))
            {
                var errors = source.GetErrors(PropertyName).Cast<string>();
                if (errors != null && errors.Any())
                {
                    var borderEffect = _associatedObject.Effects.FirstOrDefault(eff => eff is BorderEffect);
                    if (borderEffect == null)
                    {
                        _associatedObject.Effects.Add(new BorderEffect());
                    }

                    if (Device.OS != TargetPlatform.Windows)
                    {
                        //_associatedObject.BackgroundColor = Color.Red;
                    }
                }
                else
                {
                    var borderEffect = _associatedObject.Effects.FirstOrDefault(eff => eff is BorderEffect);
                    if (borderEffect != null)
                    {
                        _associatedObject.Effects.Remove(borderEffect);
                    }

                    if (Device.OS != TargetPlatform.Windows)
                    {
                        _associatedObject.BackgroundColor = Color.Default;
                    }
                }
            }
        }

        protected override void OnDetachingFrom(Entry bindable)
        {
            base.OnDetachingFrom(bindable);
            // Perform clean up

            _associatedObject.TextChanged -= _associatedObject_TextChanged;

            _associatedObject = null;
        }

        public string PropertyName { get; set; }
    }

In my Behavior I add a background and a border as red. I want to automatically add a label to this Entry. So I was thinking to Add a stacklayout above this Entry and add a label and that Entry in it. Its very tedious to write a label for every control. Is it possible or may be some other better way?

Updated Method (Not Efficient):

 <Entry Text="{Binding Email}" Placeholder="Enter Email ID" Keyboard="Email" HorizontalTextAlignment="Center">
            <Entry.Behaviors>
                <validation:EntryValidationBehavior PropertyName="Email" />
            </Entry.Behaviors>
        </Entry>
        <Label Text="{Binding Errors[Email], Converter={StaticResource FirstErrorConverter}" 
               IsVisible="{Binding Errors[Email], Converter={StaticResource ErrorLabelVisibilityConverter}"  
               FontSize="Small" 
               TextColor="Red" />
        <Entry Text="{Binding Password}" Placeholder="Enter Password" Keyboard="Text" IsPassword="true" HorizontalTextAlignment="Center">
            <Entry.Behaviors>
                <validation:EntryValidationBehavior PropertyName="Password" />
            </Entry.Behaviors>
        </Entry>
        <Label Text="{Binding Errors[Password], Converter={StaticResource FirstErrorConverter}" 
               IsVisible="{Binding Errors[Password], Converter={StaticResource ErrorLabelVisibilityConverter}"  
               FontSize="Small" 
               TextColor="Red" />
        <Entry Text="{Binding ConfirmPassword}" Placeholder="Confirm Password" Keyboard="Text" IsPassword="true" HorizontalTextAlignment="Center">
            <Entry.Behaviors>
                <validation:EntryValidationBehavior PropertyName="ConfirmPassword" />
            </Entry.Behaviors>
        </Entry>

Converter

public class FirstErrorConverter : 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();
        }
    }

Validator:

public class ValidationBase : BindableBase, INotifyDataErrorInfo
    {
        private Dictionary<string, List<string>> _errors = new Dictionary<string, List<string>>();
        public Dictionary<string, List<string>> Errors
        {
            get { return _errors; }
        }


        public ValidationBase()
        {
            ErrorsChanged += ValidationBase_ErrorsChanged;
        }

        private void ValidationBase_ErrorsChanged(object sender, DataErrorsChangedEventArgs e)
        {
            OnPropertyChanged("HasErrors");
            OnPropertyChanged("Errors");
            OnPropertyChanged("ErrorsList");
        }

        #region INotifyDataErrorInfo Members

        public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;

        public IEnumerable GetErrors(string propertyName)
        {
            if (!string.IsNullOrEmpty(propertyName))
            {
                if (_errors.ContainsKey(propertyName) && (_errors[propertyName].Any()))
                {
                    return _errors[propertyName].ToList();
                }
                else
                {
                    return new List<string>();
                }
            }
            else
            {
                return _errors.SelectMany(err => err.Value.ToList()).ToList();
            }
        }

        public bool HasErrors
        {
            get
            {
                return _errors.Any(propErrors => propErrors.Value.Any());
            }
        }

        #endregion

        protected virtual void ValidateProperty(object value, [CallerMemberName] string propertyName = null)
        {
            var validationContext = new ValidationContext(this, null)
            {
                MemberName = propertyName
            };

            var validationResults = new List<ValidationResult>();
            Validator.TryValidateProperty(value, validationContext, validationResults);

            RemoveErrorsByPropertyName(propertyName);

            HandleValidationResults(validationResults);
            RaiseErrorsChanged(propertyName);
        }

        private void RemoveErrorsByPropertyName(string propertyName)
        {
            if (_errors.ContainsKey(propertyName))
            {
                _errors.Remove(propertyName);
            }

           // RaiseErrorsChanged(propertyName);
        }

        private void HandleValidationResults(List<ValidationResult> validationResults)
        {
            var resultsByPropertyName = from results in validationResults
                                        from memberNames in results.MemberNames
                                        group results by memberNames into groups
                                        select groups;

            foreach (var property in resultsByPropertyName)
            {
                _errors.Add(property.Key, property.Select(r => r.ErrorMessage).ToList());
               // RaiseErrorsChanged(property.Key);
            }
        }

        private void RaiseErrorsChanged(string propertyName)
        {
            ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(propertyName));
        }

        public IList<string> ErrorsList
        {
            get
            {
                return GetErrors(string.Empty).Cast<string>().ToList();
            }
        }
    }

The problem with this solution is that FirstErrorConverter is called for each property in a page every time any one of the property changes. So for example there are 10 properties that need to be validated. The method will be called 10 times. Secondly The Red Border takes about a second to display for the first time.

Principle answered 27/2, 2018 at 10:22 Comment(4)
For the many-labels-problem, you could instead apply your behaviour to a layout like a StackLayout or maybe something more efficient like a Grid. Your Entry would go inside this layout. This gives you the ability to dynamically add views/controls to this layout in your behaviour.Sundew
Behavior is associated with Entry. How will I access the label? Can you give some example?. Do you think its the best way of doing validation. Xamarin is not very well documented in case of validations.Principle
The converter is executed many times, becuase all the bindings are bound to Errors collection which changes, so they have to update as well. It does not matter that the specific property errors collection did not change, the binding cannot know that. However, this should not hinder the performance at all, evaluating the converter should be very lightweight. Have you tried stepping through the code to see what actually takes the one second to complete? It seems too much for any kind of operation.Palladino
I use ValidationRules they work pretty neat. Have you tried these yet?Piquet
Q
6

That approach looks amazing, and open a lot of possibilities for improvements.

Just to don't let it without an answer, I think you can try to create a component that wraps the views you wanna handle and expose the events and properties you need to use outside. It'll be reusable and it does the trick.

So, step-by-step it would be:

  1. Create your wrapper component;
  2. Target this control on your Behavior;
  3. Expose / handle the properties and events you intend to use;
  4. Replace the simple Entry by this CheckableEntryView on your code.

Here is the component's XAML code:

<ContentView xmlns="http://xamarin.com/schemas/2014/forms" 
         xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
         x:Class="MyApp.CheckableEntryView">
<ContentView.Content>
    <StackLayout>
        <Label x:Name="lblContraintText" 
               Text="This is not valid"
               TextColor="Red"
               AnchorX="0"
               AnchorY="0"
               IsVisible="False"/>
        <Entry x:Name="txtEntry"
               Text="Value"/>
    </StackLayout>
</ContentView.Content>

And it's code-behind:

[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class CheckableEntryView : ContentView
{
    public event EventHandler<TextChangedEventArgs> TextChanged;

    private BindableProperty TextProperty = BindableProperty.Create(nameof(Text), typeof(string), typeof(CheckableEntryView), string.Empty);
    public string Text
    {
        get { return (string)GetValue(TextProperty); }
        set { SetValue( TextProperty, value); }
    }

    public CheckableEntryView ()
    {
        InitializeComponent();

        txtEntry.TextChanged += OnTextChanged;
        txtEntry.SetBinding(Entry.TextProperty, new Binding(nameof(Text), BindingMode.Default, null, null, null, this));
    }

    protected virtual void OnTextChanged(object sender, TextChangedEventArgs args)
    {
        TextChanged?.Invoke(this, args);
    }

    public Task ShowValidationMessage()
    {
        Task.Yield();
        lblContraintText.IsVisible = true;
        return lblContraintText.ScaleTo(1, 250, Easing.SinInOut);
    }

    public Task HideValidationMessage()
    {
        Task.Yield();
        return lblContraintText.ScaleTo(0, 250, Easing.SinInOut)
            .ContinueWith(t => 
                Device.BeginInvokeOnMainThread(() => lblContraintText.IsVisible = false));
    }
}

I've changed the behavior's event logic to make it simpler. Just for your information, it is:

void _associatedObject_TextChanged(object sender, TextChangedEventArgs e)
{
    if(e.NewTextValue == "test")
        ((CheckableEntryView)sender).ShowValidationMessage();
    else
        ((CheckableEntryView)sender).HideValidationMessage();
}

To use it you do basically the same thing you did before:

<local:CheckableEntryView HorizontalOptions="FillAndExpand">
    <local:CheckableEntryView.Behaviors>
        <local:EntryValidationBehavior PropertyName="Test"/><!-- this property is not being used on this example -->
    </local:CheckableEntryView.Behaviors>
</local:CheckableEntryView>

This is how it would looks like:

gif sample

I didn't bound the validation message on this sample code, but you can keep the same idea.

I hope it helps you.

Quern answered 8/3, 2018 at 16:51 Comment(2)
This is great, the only problem is property cascading from the custom view to the views inside should be done manually!Gathers
Sure @Shan. It restricts too many features of the inner views. I bet the validation rules approach should fit better, as it was said on question comments.Quern
T
5

Using Validation in Enterprise Apps from the Xamarin.FormsEnterprise Application Patterns eBook and the EntryLabelView component below, the XAML might look as follows:

xmlns:local="clr-namespace:View"
...
<local:EntryLabelView ValidatableObject="{Binding MyValue, Mode=TwoWay}"
                      ValidateCommand="{Binding ValidateValueCommand}" />

Viewmodel:

private ValidatableObject<string> _myValue;

public ViewModel()
{
  _myValue = new ValidatableObject<string>();

  _myValue.Validations.Add(new IsNotNullOrEmptyRule<string> { ValidationMessage = "A value is required." });
}

public ValidatableObject<string> MyValue
{
  get { return _myValue; }
  set
  {
      _myValue = value;
      OnPropertyChanged(nameof(MyValue));
  }
}

public ICommand ValidateValueCommand => new Command(() => ValidateValue());

private bool ValidateValue()
{
  return _myValue.Validate(); //updates ValidatableObject.Errors
}

Implementations of classes referenced, including ValidatableObject, IsNotNullOrEmptyRule, EventToCommandBehavior, and FirstValidationErrorConverter can be found in the eShopOnContainers sample.

EntryLabelView.xaml: (Please note the use of Source={x:Reference view})

<ContentView xmlns="http://xamarin.com/schemas/2014/forms"
         xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
         xmlns:converters="clr-namespace:Toolkit.Converters;assembly=Toolkit"
         xmlns:behaviors="clr-namespace:Toolkit.Behaviors;assembly=Toolkit"
         x:Name="view"
         x:Class="View.EntryLabelView">
  <ContentView.Resources>
    <converters:FirstValidationErrorConverter x:Key="FirstValidationErrorConverter" />
  </ContentView.Resources>
  <ContentView.Content>
    <StackLayout>
      <Entry Text="{Binding ValidatableObject.Value, Mode=TwoWay, Source={x:Reference view}}">
        <Entry.Behaviors>
          <behaviors:EventToCommandBehavior 
                            EventName="TextChanged"
                            Command="{Binding ValidateCommand, Source={x:Reference view}}" />
        </Entry.Behaviors>
      </Entry>
      <Label Text="{Binding ValidatableObject.Errors, Source={x:Reference view},
                        Converter={StaticResource FirstValidationErrorConverter}}" />
    </StackLayout>
  </ContentView.Content>
</ContentView>

EntryLabelView.xaml.cs: (Please note the use of OnPropertyChanged).

[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class EntryLabelView : ContentView
{
    public EntryLabelView ()
    {
        InitializeComponent ();
    }

    public static readonly BindableProperty ValidatableObjectProperty = BindableProperty.Create(
        nameof(ValidatableObject), typeof(ValidatableObject<string>), typeof(EntryLabelView), default(ValidatableObject<string>),
        BindingMode.TwoWay,
        propertyChanged: (b, o, n) => ((EntryLabelView)b).ValidatableObjectChanged(o, n));

    public ValidatableObject<string> ValidatableObject
    {
        get { return (ValidatableObject<string>)GetValue(ValidatableObjectProperty); }
        set { SetValue(ValidatableObjectProperty, value); }
    }

    void ValidatableObjectChanged(object o, object n)
    {
        ValidatableObject = (ValidatableObject<string>)n;
        OnPropertyChanged(nameof(ValidatableObject));
    }

    public static readonly BindableProperty ValidateCommandProperty = BindableProperty.Create(
        nameof(Command), typeof(ICommand), typeof(EntryLabelView), null,
        propertyChanged: (b, o, n) => ((EntryLabelView)b).CommandChanged(o, n));

    public ICommand ValidateCommand
    {
        get { return (ICommand)GetValue(ValidateCommandProperty); }
        set { SetValue(ValidateCommandProperty, value); }
    }

    void CommandChanged(object o, object n)
    {
        ValidateCommand = (ICommand)n;
        OnPropertyChanged(nameof(ValidateCommand));
    }
}
Triciatrick answered 22/10, 2018 at 14:22 Comment(0)
G
3

After spending sometime, I've come up with a hybrid of all the suggestions. Your FirstErrorConverter is fired multiple times since you are calling ErrorsList property changed. Instead use a Dictionary with _errors as backing field. This is how ViewModelBase looks like:

public ViewModelBase()
{
    PropertyInfo[] properties = GetType().GetProperties();
    foreach (PropertyInfo property in properties)
    {
        var attrs = property.GetCustomAttributes(true);
        if (attrs?.Length > 0)
        {
            Errors[property.Name] = new SmartCollection<ValidationResult>();
        }
    }
}

private Dictionary<string, SmartCollection<ValidationResult>> _errors = new Dictionary<string, SmartCollection<ValidationResult>>();
public Dictionary<string, SmartCollection<ValidationResult>> Errors
{
    get => _errors;
    set => SetProperty(ref _errors, value);
}

protected void Validate(string propertyName, string propertyValue)
{
    var validationContext = new ValidationContext(this, null)
    {
        MemberName = propertyName
    };

    var validationResults = new List<ValidationResult>();
    var isValid = Validator.TryValidateProperty(propertyValue, validationContext, validationResults);

    if (!isValid)
    {
        Errors[propertyName].Reset(validationResults);
    }
    else
    {
        Errors[propertyName].Clear();
    }
}

Since ObservableCollection fires CollectionChanged event on every item add, I went with SmartCollection with an additional property called FirstItem

public class SmartCollection<T> : ObservableCollection<T>
{
    public T FirstItem => Items.Count > 0 ? Items[0] : default(T);

    public SmartCollection()
        : base()
    {
    }

    public SmartCollection(IEnumerable<T> collection)
        : base(collection)
    {
    }

    public SmartCollection(List<T> list)
        : base(list)
    {
    }

    public void AddRange(IEnumerable<T> range)
    {
        foreach (var item in range)
        {
            Items.Add(item);
        }

        this.OnPropertyChanged(new PropertyChangedEventArgs("FirstItem"));
        this.OnPropertyChanged(new PropertyChangedEventArgs("Count"));
        this.OnPropertyChanged(new PropertyChangedEventArgs("Item[]"));
        this.OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
    }

    public void Reset(IEnumerable<T> range)
    {
        this.Items.Clear();

        AddRange(range);
    }
}

This is how my xaml looks like:

<StackLayout Orientation="Vertical">
    <Entry Placeholder="Email" Text="{Binding Email}">
        <Entry.Behaviors>
            <behaviors:EntryValidatorBehavior PropertyName="Email" />
        </Entry.Behaviors>
    </Entry>
    <Label Text="{Binding Errors[Email].FirstItem, Converter={StaticResource firstErrorToTextConverter}}"
           IsVisible="{Binding Errors[Email].Count, Converter={StaticResource errorToBoolConverter}}" />

    <Entry Placeholder="Password" Text="{Binding Password}">
        <Entry.Behaviors>
            <behaviors:EntryValidatorBehavior PropertyName="Password" />
        </Entry.Behaviors>
    </Entry>
    <Label Text="{Binding Errors[Password].FirstItem, Converter={StaticResource firstErrorToTextConverter}}"
           IsVisible="{Binding Errors[Password].Count, Converter={StaticResource errorToBoolConverter}}" />
</StackLayout>

Everything else is same!!

look gif

Gathers answered 21/10, 2018 at 2:55 Comment(0)
S
0

I might be a bit late, but for anyone who stumbles across this post in the future.

Maybe give this Library a try: https://www.nuget.org/packages/Xamarin.AttributeValidation/

It allows you to validate your UI by simply putting Attributes above your properties in the ViewModel. That's it. Nothing more to do. Just like you do in ASP.NET Core. The Validation Messages are automatically shown in the form of a little floating text bubble, hovering over the entry. Very similar to the native Android Validation.

For an example, or a detailed explanation, check out the GitHub repository: https://github.com/kevin-mueller/Xamarin.AttributeValidation

Steelyard answered 24/4, 2020 at 7:33 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.