Friday, 24 March 2017

Validating User Input in Xamarin.Forms IV

Previously I discussed how validation can be automatically triggered when properties changes, or manually triggered to validate all properties. In this blog post I’m going to explore how to notify the user about any validation errors that have occurred.

Displaying Validation Errors

Validation errors are shown to the user by highlighting the control that contains invalid data, and by displaying an error message beneath the control, as shown in the following screenshot:

Validation errors

Highlighting a Control that contains Invalid Data

The Validation attached behaviour class is used to highlight controls where validation errors occur. The following code example shows how the Validation behaviour is attached to an Entry control:

<Entry Placeholder="Enter forename here" Text="{Binding User.Forename, Mode=TwoWay}" local:Validation.Errors="{Binding User.Errors[Forename]}" />

The Validation attached behaviour gets and sets the Errors bindable property, and in this example binds it to the Errors property of the User model class that’s exposed by the associated view model. The User.Errors property is provided by the ValidatableBase class, from which all model classes derive, and is an instance of the Validator class. The indexer of the Validator class returns a ReadOnlyCollection of error strings, to retrieve any validation errors for the Forename property. The Validation attached behavior class is shown in the following code example:

public static class Validation { public static readonly BindableProperty ErrorsProperty = BindableProperty.CreateAttached( "Errors", typeof(ReadOnlyCollection<string>), typeof(Validation), Validator.EmptyErrorsCollection, propertyChanged: OnPropertyErrorsChanged); public static ReadOnlyCollection<string> GetErrors(BindableObject element) { return (ReadOnlyCollection<string>)element.GetValue(ErrorsProperty); } public static void SetErrors(BindableObject element, ReadOnlyCollection<string> value) { element.SetValue(ErrorsProperty, value); } static void OnPropertyErrorsChanged(BindableObject element, object oldValue, object newValue) { var view = element as View; if (view == null | oldValue == null || newValue == null) { return; } var propertyErrors = (ReadOnlyCollection<string>)newValue; if (propertyErrors.Any()) { view.Effects.Add(new BorderEffect()); } else { var effectToRemove = view.Effects.FirstOrDefault(e => e is BorderEffect); if (effectToRemove != null) { view.Effects.Remove(effectToRemove); } } } }

The Errors attached property is registered as a ReadOnlyCollection of strings, by the BindableProperty.CreateAttached method. When the value of the Errors bindable property changes, the OnPropertyErrorsChanged method is invoked. The parameters for this method provide the instance of the control that the Errors bindable property is attached to, and any validation errors for the control. The BorderEffect class is added to the control’s Effects collection if validation errors are present, otherwise it’s removed from the control’s Effects collection.

The BorderEffect class subclasses the RoutingEffect class, and is shown in the following code example:

class BorderEffect : RoutingEffect { public BorderEffect() : base("Xamarin.BorderEffect") { } }

The RoutingEffect class represents a platform-independent effect that wraps an inner effect that’s usually platform-specific. This simplifies the effect removal process, since there is no compile-time access to the type information for a platform-specific effect. The BorderEffect calls the base class constructor, passing in a parameter consisting of a concatenation of the resolution group name, and the unique ID that’s specified on each platform-specific effect class.

The following code example shows the Xamarin.BorderEffect implementation for iOS:

[assembly: ResolutionGroupName("Xamarin")] [assembly: ExportEffect(typeof(BorderEffect), "BorderEffect")] namespace MVVMUtopia.iOS { public class BorderEffect : PlatformEffect { protected override void OnAttached() { try { Control.Layer.BorderColor = UIColor.Red.CGColor; Control.Layer.BorderWidth = 1; } catch (Exception ex) { Console.WriteLine("Cannot set property on attached control. Error: ", ex.Message); } } protected override void OnDetached() { try { Control.Layer.BorderWidth = 0; } catch (Exception ex) { Console.WriteLine("Cannot set property on attached control. Error: ", ex.Message); } } } }

The OnAttached method sets Control.Layer properties to values to create the red border, and the OnDetached method removes the red border. This functionality is wrapped in a try/catch block in case the control that the effect is attached to does not have the required Control.Layer properties.

For more information about effects, see Effects.

Displaying an Error Message

Each control that undergoes validation also has a Label beneath it, which is used to display the error message associated with the control. The following code example shows the Label displayed underneath the Entry that accepts a user’s forename:

<Label Text="{Binding User.Errors[Forename], Converter={StaticResource FirstErrorConverter}" />

The Label binds its Text property to the Errors property of the User model class that’s exposed by the associated view model. The User.Errors property is provided by the ValidatableBase class, from which all model classes derive, and is an instance of the Validator class. The indexer of the Validator class returns a ReadOnlyCollection of error strings, with the FirstErrorConverter retrieving the first error from the collection, for display.

Summary

Any mobile app that accepts user input should ensure that the input is valid. This could involve, for example, checking that input contains only characters in a particular range, or is of a certain length. Without validation, a user can supply data that can cause the app to fail.

In the context of MVVM, user input can be synchronously validated client-side in view model objects or in model objects. However, validating data in view models often means duplicating model properties. Instead, view models can delegate validation to the model objects they contain, with validation then being performed on the model objects. Validation rules can be specified on the model properties by using data annotations that derive from the ValidationAttribute class.

To participate in validation, model classes must derive from the ValidatableBase class, which provides an error container whose contents are updated whenever a model class property value changes. The Validator and ValidatableBase classes both implement INotifyPropertyChanged in order to provide property change notification.

The SetProperty method in the ValidatableBase class performs validation when a model property is set to a new value. The validation rules come from data annotation attributes that derive from the ValidationAttribute class. The attributes are taken from the declaration of the model property being validated.

Users are notified of validation errors by highlighting the controls that contain the invalid data with red borders, and by displaying error messages that inform the user why the data is invalid.

The code can be downloaded from GitHub.

1 comment:

  1. I've got samples of how to do this in Android if you need them using the build in Android validation capabilities.

    ReplyDelete