ObservableValidator
The ObservableValidator
is a base class implementing the INotifyDataErrorInfo
interface, providing support for validating properties exposed to other application modules. It also inherits from ObservableObject
, so it implements INotifyPropertyChanged
and INotifyPropertyChanging
as well. It can be used as a starting point for all kinds of objects that need to support both property change notifications and property validation.
Platform APIs: ObservableValidator, ObservableObject
How it works
ObservableValidator
has the following main features:
- It provides a base implementation for
INotifyDataErrorInfo
, exposing theErrorsChanged
event and the other necessary APIs. - It provides a series of additional
SetProperty
overloads (on top of the ones provided by the baseObservableObject
class), that offer the ability of automatically validating properties and raising the necessary events before updating their values. - It exposes a number of
TrySetProperty
overloads, that are similar toSetProperty
but with the ability of only updating the target property if the validation is successful, and to return the generated errors (if any) for further inspection. - It exposes the
ValidateProperty
method, which can be useful to manually trigger the validation of a specific property in case its value has not been updated but its validation is dependent on the value of another property that has instead been updated. - It exposes the
ValidateAllProperties
method, which automatically executes the validation of all public instance properties in the current instance, provided they have at least one[ValidationAttribute]
applied to them. - It exposes a
ClearAllErrors
method that can be useful when resetting a model bound to some form that the user might want to fill in again. - It offers a number of constructors that allow passing different parameters to initialize the
ValidationContext
instance that will be used to validate properties. This can be especially useful when using custom validation attributes that might require additional services or options to work correctly.
Simple property
Here's an example of how to implement a property that supports both change notifications as well as validation:
public class RegistrationForm : ObservableValidator
{
private string name;
[Required]
[MinLength(2)]
[MaxLength(100)]
public string Name
{
get => name;
set => SetProperty(ref name, value, true);
}
}
Here we are calling the SetProperty<T>(ref T, T, bool, string)
method exposed by ObservableValidator
, and that additional bool
parameter set to true
indicates that we also want to validate the property when its value is updated. ObservableValidator
will automatically run the validation on every new value using all the checks that are specified with the attributes applied to the property. Other components (such as UI controls) can then interact with the viewmodel and modify their state to reflect the errors currently present in the viewmodel, by registering to ErrorsChanged
and using the GetErrors(string)
method to retrieve the list of errors for each property that has been modified.
Custom validation methods
Sometimes validating a property requires a viewmodel to have access to additional services, data, or other APIs. There are different ways to add custom validation to a property, depending on the scenario and the level of flexibility that is required. Here is an example of how the [CustomValidationAttribute]
type can be used to indicate that a specific method needs to be invoked to perform additional validation of a property:
public class RegistrationForm : ObservableValidator
{
private readonly IFancyService service;
public RegistrationForm(IFancyService service)
{
this.service = service;
}
private string name;
[Required]
[MinLength(2)]
[MaxLength(100)]
[CustomValidation(typeof(RegistrationForm), nameof(ValidateName))]
public string Name
{
get => this.name;
set => SetProperty(ref this.name, value, true);
}
public static ValidationResult ValidateName(string name, ValidationContext context)
{
RegistrationForm instance = (RegistrationForm)context.ObjectInstance;
bool isValid = instance.service.Validate(name);
if (isValid)
{
return ValidationResult.Success;
}
return new("The name was not validated by the fancy service");
}
}
In this case we have a static ValidateName
method that will perform validation on the Name
property through a service that is injected into our viewmodel. This method receives the name
property value and the ValidationContext
instance in use, which contains things such as the viewmodel instance, the name of the property being validated, and optionally a service provider and some custom flags we can use or set. In this case, we are retrieving the RegistrationForm
instance from the validation context, and from there we are using the injected service to validate the property. Note that this validation will be executed next to the ones specified in the other attributes, so we are free to combine custom validation methods and existing validation attributes however we like.
Custom validation attributes
Another way of doing custom validation is by implementing a custom [ValidationAttribute]
and then inserting the validation logic into the overridden IsValid
method. This enables extra flexibility compared to the approach described above, as it makes it very easy to just reuse the same attribute in multiple places.
Suppose we wanted to validate a property based on its relative value with respect to another property in the same viewmodel. The first step would be to define a custom [GreaterThanAttribute]
, like so:
public sealed class GreaterThanAttribute : ValidationAttribute
{
public GreaterThanAttribute(string propertyName)
{
PropertyName = propertyName;
}
public string PropertyName { get; }
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
object
instance = validationContext.ObjectInstance,
otherValue = instance.GetType().GetProperty(PropertyName).GetValue(instance);
if (((IComparable)value).CompareTo(otherValue) > 0)
{
return ValidationResult.Success;
}
return new("The current value is smaller than the other one");
}
}
Next we can add this attribute into our viewmodel:
public class ComparableModel : ObservableValidator
{
private int a;
[Range(10, 100)]
[GreaterThan(nameof(B))]
public int A
{
get => this.a;
set => SetProperty(ref this.a, value, true);
}
private int b;
[Range(20, 80)]
public int B
{
get => this.b;
set
{
SetProperty(ref this.b, value, true);
ValidateProperty(A, nameof(A));
}
}
}
In this case, we have two numerical properties that must be in a specific range and with a specific relationship between each other (A
needs to be greater than B
). We have added the new [GreaterThanAttribute]
over the first property, and we also added a call to ValidateProperty
in the setter for B
, so that A
is validated again whenever B
changes (since its validation status depends on it). We just need these two lines of code in our viewmodel to enable this custom validation, and we also get the benefit of having a reusable custom validation attribute that could be useful in other viewmodels in our application as well. This approach also helps with code modularization, as the validation logic is now completely decoupled from the viewmodel definition itself.
Examples
- Check out the sample app (for multiple UI frameworks) to see the MVVM Toolkit in action.
- You can also find more examples in the unit tests.
MVVM Toolkit