Thursday, 16 March 2017

Validating User Input in Xamarin.Forms III

Previously I discussed how validation rules are specified by adding data annotation attributes, that derive from the ValidationAttribute class, to properties in model classes that require validation. In this blog post I’m going to explore how validation is triggered, both automatically and manually.

Triggering Validation

This validation approach can automatically trigger validation when properties change, and manually trigger validation of all properties. Each will be discussed in turn.

Triggering Validation when Properties Change

Validation is automatically triggered when a bound property changes. When a two way binding in a view sets a bound property in a model class, that class should invoke the SetProperty method, provided by the BindableBase class, which sets the property values and raises the PropertyChanged event. However, the SetProperty method is also overridden by the ValidatableBase class. The SetProperty method in the ValidatableBae class calls the SetProperty method in the BindableBase class, and performs validation if the property has changed. The following code example shows how validation occurs after a property change:

public bool ValidateProperty(string propertyName) { if (string.IsNullOrEmpty(propertyName)) { throw new ArgumentNullException("propertyName"); } var propertyInfo = entityToValidate.GetType().GetRuntimeProperty(propertyName); if (propertyInfo == null) { throw new ArgumentException("The entity does not contain a property with that name.", propertyName); } var propertyErrors = new List<string>(); bool isValid = TryValidateProperty(propertyInfo, propertyErrors); bool errorsChanged = SetPropertyErrors(propertyInfo.Name, propertyErrors); if (errorsChanged) { OnErrorsChanged(propertyName); OnPropertyChanged(string.Format(CultureInfo.CurrentCulture, "Item[{0}]", propertyName)); } return isValid; }

This method retrieves the property to be validated, and attempts to validate it by calling the TryValidateProperty method. If the validation results change, for example, when new validation errors are found or when previous errors have been corrected, then the ErrorsChanged and PropertyChanged events are raised for the property. The following code example shows the TryValidateProperty method:

bool TryValidateProperty(PropertyInfo propertyInfo, List<string> propertyErrors) { var results = new List<ValidationResult>(); var context = new ValidationContext(entityToValidate) { MemberName = propertyInfo.Name }; var propertyValue = propertyInfo.GetValue(entityToValidate); bool isValid = System.ComponentModel.DataAnnotations.Validator.TryValidateProperty(propertyValue, context, results); if (results.Any()) { propertyErrors.AddRange(results.Select(c => c.ErrorMessage)); } return isValid; }

This method calls the TryValidateProperty method from the Validator class, to validate the property value against the validation rules for the property. Any validation errors are added to a list of errors.

Trigger Validation Manually

Validation can be triggered manually for all properties of a model object. For example, in the sample application when the user clicks the Navigate button on the FirstPage. The button’s command delegate calls the NavigateAsync method, which is shown in the following code example:

async Task NavigateAsync() { if (user.ValidateProperties()) { await navigationService.NavigateAsync("SecondPage"); } }

This method calls the ValidateProperties method of the ValidatableBase class (because the User model class derives from the ValidatableBase class), which in turn calls the ValidateProperties method of the Validator class. Providing that validation succeeds, page navigation occurs, otherwise validation errors are displayed. The following code example shows the ValidateProperties method from the Validator class:

public bool ValidateProperties() { var propertiesWithChangedErrors = new List<string>(); var propertiesToValidate = entityToValidate.GetType() .GetRuntimeProperties() .Where(c => c.GetCustomAttributes(typeof(ValidationAttribute)).Any()); foreach (PropertyInfo propertyInfo in propertiesToValidate) { var propertyErrors = new List<string>(); TryValidateProperty(propertyInfo, propertyErrors); bool errorsChanged = SetPropertyErrors(propertyInfo.Name, propertyErrors); if (errorsChanged && !propertiesWithChangedErrors.Contains(propertyInfo.Name)) { propertiesWithChangedErrors.Add(propertyInfo.Name); } } foreach (string propertyName in propertiesWithChangedErrors) { OnErrorsChanged(propertyName); OnPropertyChanged(string.Format(CultureInfo.CurrentCulture, "Item[{0}]", propertyName)); } return errors.Values.Count == 0; }

This method retrieves any properties that have attributes that derive from the ValidationAttribute data annotation, and attempts to validate them by calling the TryValidateProperty method for each property. If the validation state changes, the ErrorsChanged and PropertyChanged events are raised for each property whose errors have changed. Changes occur when new errors are seen or when previously detected errors are no longer present.

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. In my next blog post I’ll explore this topic further. In the meantime, the code can be downloaded from GitHub.

1 comment:

  1. I came up against a problem when applying a range validation attribute to a payment input string, where the app crashed when I put in an initial decimal point and the charbychar validation could not make the implied cast to double work.
    I got round it by trapping errors within TryValidateProperty and using the 'required' message for the payment field as an adhoc error message in the catch section. The reason for using that message was that I needed the actual message to exist so that 'SetPropertyErrors' could find it to remove it once the field was valid.
    Pete Midgley ([email protected])