Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Validation in Xamarin using DataAnnotation

Tags:

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.

like image 599
Safi Mustafa Avatar asked Feb 27 '18 10:02

Safi Mustafa


People also ask

How do you validate entry in xamarin?

PropertyChanged—the form validates a data field when its value changes. Manually (the default value)—the form validates data fields when the Validate() method is called. You can also call the Validate(String) method to validate a specific data field.

How do I use xamarin community toolkit?

Setup Xamarin Community ToolkitOpen an existing project, or create a new project using the Blank Forms App template. In the Solution Explorer panel, right click on your project name and select Manage NuGet Packages. Search for Xamarin. CommunityToolkit, and choose the desired NuGet Package from the list.


2 Answers

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.

like image 103
Diego Rafael Souza Avatar answered Oct 11 '22 21:10

Diego Rafael Souza


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));
    }
}
like image 23
Benl Avatar answered Oct 11 '22 22:10

Benl