Validate input in Xamarin.Forms using INotifyDataErrorInfo, custom Behaviors, Effects and Prism

Developer Support

In this post, Senior Application Development Manager, Davide Zordan, explores the rich set of validation features in Xamarin.Forms.


I have recently been investigating the support available in Xamarin.Forms for validation and, in particular, researched the possibility of using INotifyDataErrorInfo to complement the traditional approach of using Behaviors.

In simple scenarios, it’s possible to perform validation by simply attaching a Behavior to the required view, as shown by the following sample code:

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

    void OnEntryTextChanged(object sender, TextChangedEventArgs args)
    {
     var textValue = args.NewTextValue;
        bool isValid = !string.IsNullOrEmpty(textValue) && textValue.Length >= 5;
        ((Entry)sender).TextColor = isValid ? Color.Default : Color.Red;
    }
}

In this case, we are validating the input and modifying the UI if the number of characters entered is less than 5.

What about more articulated scenarios when multiple business rules are required to be checked for the input values?

In these cases, we could take advantage of other types available to make our code more structured and extensible:

  • INotifyDataErrorInfo: available in Xamarin.Forms. When implemented it permits specifying custom validation supporting multiple errors per property, cross-property errors and entity-level errors;
  • DataAnnotations decorate the data models using attributes which specify validation conditions to be applied to the specific field;
  • Xamarin.Forms Behaviors specify the specific UI to be applied to the specific validation scenarios, integrating with INotifyDataErrorInfo and DataAnnotations.

To start exploring this approach, I created a new Xamarin.Forms Prism solution using the Prism Template Pack which generated the following project structure:

image

Then, I added the following new model to be validated using DataAnnotations and INotifyDataErrorInfo:

using System.ComponentModel.DataAnnotations;
using System.Runtime.CompilerServices;
using UsingValidation.Validation;

namespace UsingValidation.Models
{
    public class Item : ValidationBase
    {
        public Item()
        {
            Name = string.Empty;
            Description = string.Empty;
        }

        private string _name;
        private string _description;

        [Required(ErrorMessage = "Name cannot be empty!")]
        public string Name
        {
            get { return _name; }
            set
            {
                ValidateProperty(value);
                SetProperty(ref _name, value);
            }
        }

        [Required(ErrorMessage = "Description cannot be empty!")]
        [RegularExpression(@"\w{5,}", ErrorMessage = "Description: more than 4 letters/numbers required")]
        public string Description
        {
            get { return _description; }
            set
            {
                ValidateProperty(value);
                SetProperty(ref _description, value);
            }
        }

        protected override void ValidateProperty(object value, [CallerMemberName] string propertyName = null)
        {
            base.ValidateProperty(value, propertyName);

            OnPropertyChanged("IsSubmitEnabled");
        }

        public bool IsSubmitEnabled
        {
            get
            {
                return !HasErrors;
            }
        }
    }
}

The model uses attributes declared in the SystemComponentModel.DataAnnotations namespace which can be referenced in the solution modifying the Portable Class Library profile of the UsingValidation common project:

image

Quick tip: to be able to change the PCL profile I had to remove all the NuGet packages used by the common project, remove the Windows Phone 8 profile and then add back all the removed NuGet packages to the UsingValidation PCL.

To use the capability offered by INotifyDataErrorInfo, the model needs to implements 3 members defined in the interface:

  • GetErrors() returns an IEnumerable sequence of strings containing the error messages triggered by validation;
  • the HasErrors property returns a boolean value indicating if there are validation errors;
  • ErrorsChanged event can be triggered to Notify if the validation errors have been updated.

This interface is quite flexible and is designed to be customised depending on the different scenarios needed: I took as a starting point this implementation available on GitHub and modified it accordingly: I decided to separate the implementation of INotifyDataErrorInfo in a different base class called ValidationBase which contains the following code using a Dictionary<string, List<string>> needed for storing the generated validation errors:

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

    public ValidationBase()
    {
        ErrorsChanged += ValidationBase_ErrorsChanged;
    }

    private void ValidationBase_ErrorsChanged(object sender, DataErrorsChangedEventArgs e)
    {
        OnPropertyChanged("HasErrors");
        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());
        }
    }
}

The validation is performed by this method which evaluates the DataAnnotations decorating the model using the Validator available in the System.ComponentModel.DataAnnotations namespace and then stores the error messages in the dictionary:

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);
}

At this stage, I needed a solution for linking the UI to the model, and modifying the visuals depending on the presence or not of validation errors.

The ViewModel for the sample page, contains only a property storing an instance of the item defined in the model:

public class MainPageViewModel : BindableBase, INavigationAware
{
    public MainPageViewModel()
    {
        DemoItem = new Item();
    }

    private Item _item;
    public Item DemoItem
    {
        get { return _item; }
        set { SetProperty(ref _item, value); }
    }

    public void OnNavigatedFrom(NavigationParameters parameters)
    {
    }

    public void OnNavigatedTo(NavigationParameters parameters)
    {
        DemoItem.Name = string.Empty;
        DemoItem.Description = string.Empty;
    }
}

Then, the corresponding XAML contains two Entry views used for input and a ListView used for showing the validation errors:

<?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="clr-namespace:Prism.Mvvm;assembly=Prism.Forms"
             xmlns:validation="clr-namespace:UsingValidation.Validation"
             xmlns:effects="clr-namespace:UsingValidation.Effects"             
             prism:ViewModelLocator.AutowireViewModel="True"
             x:Class="UsingValidation.Views.MainPage"
             Title="MainPage">  
  <Grid VerticalOptions="Center">
……   
    <Label Text ="Name: " VerticalTextAlignment="Center" Grid.Column="1" />   
    <Entry Text="{Binding Name, Mode=TwoWay}" BindingContext="{Binding DemoItem}"
            Grid.Column="2" HorizontalOptions="FillAndExpand">
        <Entry.Behaviors>
            <validation:EntryValidationBehavior PropertyName="Name" />    
        </Entry.Behaviors>
    </Entry>

    <Label Text ="Description: " VerticalTextAlignment="Center" Grid.Column="1" Grid.Row="2" />
    <Entry Text="{Binding Description, Mode=TwoWay}" BindingContext="{Binding DemoItem}"
            Grid.Column="2" HorizontalOptions="FillAndExpand" Grid.Row="2">
    <Entry.Behaviors>
        <validation:EntryValidationBehavior PropertyName="Description" />
    </Entry.Behaviors>
    </Entry>

    <ListView ItemsSource="{Binding DemoItem.ErrorsList}" 
              Grid.Row="4" Grid.Column="1" Grid.ColumnSpan="2" />

    <Button Text="Submit" IsEnabled="{Binding DemoItem.IsSubmitEnabled}" 
            Grid.Row="6" Grid.Column="1" Grid.ColumnSpan="2" />
  </Grid>
</ContentPage>

The sample page uses a Behavior called EntryValidationBehavior which take care of changing the colour of the Entry background views in the case validation errors are present:

using System.Linq;
using UsingValidation.Effects;
using Xamarin.Forms;

namespace UsingValidation.Validation
{
    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; }
    }
}

The UI is also fine-tuned using a Xamarin.Forms effect applied only to the UWP platform, in order to change the colour of the Entry border when validation errors occur:

using UsingValidation.UWP.Effects;
using Windows.UI;
using Windows.UI.Xaml.Media;
using Xamarin.Forms;
using Xamarin.Forms.Platform.UWP;

[assembly: ResolutionGroupName("UsingValidationSample")]
[assembly: ExportEffect(typeof(BorderEffect), "BorderEffect")]
namespace UsingValidation.UWP.Effects
{
    public class BorderEffect : PlatformEffect
    {
        Brush _previousBrush;
        Brush _previousBorderBrush;
        Brush _previousFocusBrush;
        FormsTextBox _control;

        protected override void OnAttached()
        {
            _control = Control as FormsTextBox;           
            if (_control != null)
            {
                _previousBrush = _control.Background;
                _previousFocusBrush = _control.BackgroundFocusBrush;
                _previousBorderBrush = _control.BorderBrush;
                _control.Background = new SolidColorBrush(Colors.Red);
                _control.BackgroundFocusBrush = new SolidColorBrush(Colors.Red);
                _control.BorderBrush = new SolidColorBrush(Colors.Red);                 
            }
        }

        protected override void OnDetached()
        {
            if (_control != null)
            {
                _control.Background = _previousBrush;
                _control.BackgroundFocusBrush = _previousFocusBrush;
                _control.BorderBrush = _previousBorderBrush;
            }
        }
    }
}

And this is the result when the application is executed on Android, iOS and UWP:

image

image

Conclusions

Xamarin.Forms provides a rich set of features for implementing validation: the usage of INotifyDataErrorInfo, Data Annotations, Behaviors and Effects permit the handling of complex scenarios including multiple conditions per field, cross-property validation, entity-level validation and custom UI depending on the platform.

The source code of the sample demo is available for download on GitHub here.

Happy coding!


Premier Support for Developers provides strategic technology guidance, critical support coverage, and a range of essential services to help teams optimize development lifecycles and improve software quality.  Contact your Application Development Manager (ADM) or email us to learn more about what we can do for you.

0 comments

Discussion is closed.

Feedback usabilla icon