Skip to content

Commit aaf9e98

Browse files
Merge pull request #39 from windows-toolkit/docs/observablevalidator
Added docs for ObservableValidator
2 parents a96a6cb + 0f63b1f commit aaf9e98

File tree

2 files changed

+160
-0
lines changed

2 files changed

+160
-0
lines changed

docs/mvvm/ObservableValidator.md

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
---
2+
title: ObservableValidator
3+
author: Sergio0694
4+
description: A base class for objects implementing the INotifyDataErrorInfo interface.
5+
keywords: windows 10, uwp, windows community toolkit, uwp community toolkit, uwp toolkit, mvvm, componentmodel, property changed, notification, errors, validate, validation, binding, net core, net standard
6+
dev_langs:
7+
- csharp
8+
---
9+
10+
# ObservableValidator
11+
12+
The [`ObservableValidator`](https://docs.microsoft.com/dotnet/api/microsoft.toolkit.mvvm.componentmodel.ObservableValidator) is a base class implementing the [`INotifyDataErrorInfo`](https://docs.microsoft.com/dotnet/api/system.componentmodel.INotifyDataErrorInfo) interface, providing support for validating properties exposed to other application modules. It also inherits from `ObservableObject`, so it implements [`INotifyPropertyChanged`](https://docs.microsoft.com/dotnet/api/system.componentmodel.inotifypropertychanged) and [`INotifyPropertyChanging`](https://docs.microsoft.com/dotnet/api/system.componentmodel.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.
13+
14+
> **Platform APIs:** [ObservableValidator](https://docs.microsoft.com/dotnet/api/microsoft.toolkit.mvvm.componentmodel.ObservableValidator), [ObservableObject](https://docs.microsoft.com/dotnet/api/microsoft.toolkit.mvvm.componentmodel.ObservableObject)
15+
16+
## How it works
17+
18+
`ObservableValidator` has the following main features:
19+
20+
- It provides a base implementation for `INotifyDataErrorInfo`, exposing the `ErrorsChanged` event and the other necessary APIs.
21+
- It provides a series of additional `SetProperty` overloads (on top of the ones provided by the base `ObservableObject` class), that offer the ability of automatically validating properties and raising the necessary events before updating their values.
22+
- It exposes a number of `TrySetProperty` overloads, that are similar to `SetProperty` 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.
23+
- 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.
24+
- 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]`](https://docs.microsoft.com/dotnet/api/system.componentmodel.dataannotations.validationattribute) applied to them.
25+
- 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.
26+
- It offers a number of constructors that allow passing different parameters to initialize the [`ValidationContext`](https://docs.microsoft.com/dotnet/api/system.componentmodel.dataannotations.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.
27+
28+
## Simple property
29+
30+
Here's an example of how to implement a property that supports both change notifications as well as validation:
31+
32+
```csharp
33+
public class RegistrationForm : ObservableValidator
34+
{
35+
private string name;
36+
37+
[Required]
38+
[MinLength(2)]
39+
[MaxLength(100)]
40+
public string Name
41+
{
42+
get => name;
43+
set => SetProperty(ref name, value, true);
44+
}
45+
}
46+
```
47+
48+
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.
49+
50+
## Custom validation methods
51+
52+
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]`](https://docs.microsoft.com/dotnet/api/system.componentmodel.dataannotations.customvalidationattribute) type can be used to indicate that a specific method needs to be invoked to perform additional validation of a property:
53+
54+
```csharp
55+
public class RegistrationForm : ObservableValidator
56+
{
57+
private readonly IFancyService service;
58+
59+
public RegistrationForm(IFancyService service)
60+
{
61+
this.service = service;
62+
}
63+
64+
private string name;
65+
66+
[Required]
67+
[MinLength(2)]
68+
[MaxLength(100)]
69+
[CustomValidation(typeof(RegistrationForm), nameof(ValidateName))]
70+
public string Name
71+
{
72+
get => this.name;
73+
set => SetProperty(ref this.name, value, true);
74+
}
75+
76+
public static ValidationResult ValidateName(string name, ValidationContext context)
77+
{
78+
RegistrationForm instance = (RegistrationForm)context.ObjectInstance;
79+
bool isValid = instance.service.Validate(name);
80+
81+
if (isValid)
82+
{
83+
return ValidationResult.Success;
84+
}
85+
86+
return new("The name was not validated by the fancy service");
87+
}
88+
}
89+
```
90+
91+
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.
92+
93+
## Custom validation attributes
94+
95+
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.
96+
97+
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:
98+
99+
```csharp
100+
public sealed class GreaterThanAttribute : ValidationAttribute
101+
{
102+
public GreaterThanAttribute(string propertyName)
103+
{
104+
PropertyName = propertyName;
105+
}
106+
107+
public string PropertyName { get; }
108+
109+
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
110+
{
111+
object
112+
instance = validationContext.ObjectInstance,
113+
otherValue = instance.GetType().GetProperty(PropertyName).GetValue(instance);
114+
115+
if (((IComparable)value).CompareTo(otherValue) > 0)
116+
{
117+
return ValidationResult.Success;
118+
}
119+
120+
return new("The current value is smaller than the other one");
121+
}
122+
}
123+
```
124+
125+
Next we can add this attribute into our viewmodel:
126+
127+
```csharp
128+
public class ComparableModel : ObservableValidator
129+
{
130+
private int a;
131+
132+
[Range(10, 100)]
133+
[GreaterThan(nameof(B))]
134+
public int A
135+
{
136+
get => this.a;
137+
set => SetProperty(ref this.a, value, true);
138+
}
139+
140+
private int b;
141+
142+
[Range(20, 80)]
143+
public int B
144+
{
145+
get => this.b;
146+
set
147+
{
148+
SetProperty(ref this.b, value, true);
149+
ValidateProperty(A, nameof(A));
150+
}
151+
}
152+
}
153+
```
154+
155+
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.
156+
157+
## Examples
158+
159+
You can find more examples in the [unit tests](https://github.com/Microsoft/WindowsCommunityToolkit//blob/master/UnitTests/UnitTests.Shared/Mvvm).

docs/toc.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,7 @@
183183
## ComponentModel
184184
### [ObservableObject](mvvm/ObservableObject.md)
185185
### [ObservableRecipient](mvvm/ObservableRecipient.md)
186+
### [ObservableValidator](mvvm/ObservableValidator.md)
186187
## Input
187188
### [RelayCommand and RelayCommand<T>](mvvm/RelayCommand.md)
188189
### [AsyncRelayCommand and AsyncRelayCommand<T>](mvvm/AsyncRelayCommand.md)

0 commit comments

Comments
 (0)