This example will show you how to [validate properties] and display an error message to the user, if the entered values are invalid. For example if the user should enter their e-mail address into a TextBox
, the "@"-character is required and the TextBox
may not be empty.
You should already know how to setup a new Avalonia project. Moreover you should know the basics about the MVVM-pattern and how to bind a property of your ViewModel
to your View
. In this sample we will show you three options to validate a property. It’s up to you, which one you prefer. You can also combine them.
In the first approach we will show you is how to use [DataAnnotations
] to validate a property. To get started, please create a new project using the Avalonia MVVM Template
.
As we want to show different validation options, we add another file called ValidationUsingDataAnnotationViewModel
into the ViewModels
-folder. This class will be added as a read-only Property into our MainWindowViewModel
:
/// <summary>
/// Gets a ViewModel showing how to use DataAnnotations for validation
/// </summary>
public ValidationUsingDataAnnotationViewModel ValidationUsingDataAnnotationViewModel { get; }
= new ValidationUsingDataAnnotationViewModel();
Let’s assume we want to make a registration-form where an e-mail-address is required and must be a valid e-mail-address. We add a property called EMail
. This property will be decorated with the [RequiredAttribute
] and the [EMailAddressAttribute
]. You can combine as many attributes as you want as well as creating your own [ValidationAttribute
].
private string? _EMail;
/// <summary>
/// Validation using DataAnnotation
/// </summary>
[Required]
[EmailAddress]
public string? EMail
{
get { return _EMail; }
set { this.RaiseAndSetIfChanged(ref _EMail, value); }
}
In the file MainWindow.axaml
we add a TabControl
where each TabItem
represents one of our options. Inside the TabItem
there is just one TextBox
which binds to our property EMail
.
<TabControl TabStripPlacement="Bottom">
<!--Validation using DataAnnotations-->
<TabItem Header="DataAnnotations">
<TextBox Text="{Binding ValidationUsingDataAnnotationViewModel.EMail}" />
</TabItem>
</TabControl>
You can also implement the interface [INotifyDataErrorInfo
] into your ViewModel
or it’s base class. Some MVVM-libraries already implement this interface by default.
Again we add another file called ValidationUsingINotifyDataErrorInfoViewModel
into the ViewModels
-folder. This class will be added as a read-only Property into our MainWindowViewModel
:
/// <summary>
/// Gets a ViewModel showing how to use INotifyDataErrorInfo for validation
/// </summary>
public ValidationUsingINotifyDataErrorInfoViewModel ValidationUsingINotifyDataErrorInfoViewModel { get; }
= new ValidationUsingINotifyDataErrorInfoViewModel();
Now we have to implement the interface, if this is not already done by the base-class you use. In our sample we add the interface on our own. The interface requires these members:
|
In addition to the interface members we add the following items:
|
💡
|
the protected keyword helps us to make the properties or methods available in derived classes, but not from any external class.
|
Below you can see the final implementation:
public class ValidationUsingINotifyDataErrorInfoViewModel : ViewModelBase, INotifyDataErrorInfo
{
// Implement members of INotifyDataErrorInfo
public event EventHandler<DataErrorsChangedEventArgs>? ErrorsChanged;
// we have errors present if errors.Count is greater than 0
public bool HasErrors => errors.Count > 0;
/// <inheritdoc />
public IEnumerable GetErrors(string? propertyName)
{
// Get entity-level errors when the target property is null or empty
if (string.IsNullOrEmpty(propertyName))
{
return errors.Values.SelectMany(static errors => errors);
}
// Property-level errors, if any
if (this.errors.TryGetValue(propertyName!, out List<ValidationResult>? result))
{
return result;
}
// In case there are no errors we return an empty array.
return Array.Empty<ValidationResult>();
}
// Store Errors in a Dictionary
private Dictionary<string, List<ValidationResult>> errors = new Dictionary<string, List<ValidationResult>>();
/// <summary>
/// Clears the errors for a given property name.
/// </summary>
/// <param name="propertyName">The name of the property to clear or all properties if <see langword="null"/></param>
protected void ClearErrors(string? propertyName = null)
{
// Clear entity-level errors when the target property is null or empty
if (string.IsNullOrEmpty(propertyName))
{
errors.Clear();
}
else
{
errors.Remove(propertyName);
}
// Notify that errors have changed
ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(propertyName));
this.RaisePropertyChanged(nameof(HasErrors));
}
/// <summary>
/// Adds a given error message for a given property name.
/// </summary>
/// <param name="propertyName">the name of the property</param>
/// <param name="errorMessage">The error message to show</param>
protected void AddError(string propertyName, string errorMessage)
{
// Add the cached errors list for later use.
if (!errors.TryGetValue(propertyName, out List<ValidationResult>? propertyErrors))
{
propertyErrors = new List<ValidationResult>();
errors.Add(propertyName, propertyErrors);
}
propertyErrors.Add(new ValidationResult(errorMessage));
// Notify that errors have changed
ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(propertyName));
this.RaisePropertyChanged(nameof(HasErrors));
}
}
The property is implemented like any other property of our ViewModel
:
private string? _EMail;
/// <summary>
/// A property that is validated using INotifyDataErrorInfo
/// </summary>
public string? EMail
{
get { return _EMail; }
set { this.RaiseAndSetIfChanged(ref _EMail, value); }
}
In addition we will add a method called Validate_EMail
that will validate our property:
private void Validate_EMail()
{
// first of all clear all previous errors
ClearErrors(nameof(EMail));
// No empty string allowed
if (string.IsNullOrEmpty(EMail))
{
AddError(nameof(EMail), "This field is required");
}
// @-sign required
if (EMail is null || !EMail.Contains('@'))
{
AddError(nameof(EMail), "Don't forget the '@'-sign");
}
}
In the constructor of our ViewModel
we will listen to changes of the property EMail
and run the validation with every change. Moreover we will run the validation inside the constructor in order to make sure the initial value is already validated.
💡
|
If you want to avoid using WhenAnyValue you can also call the validation inside the properties setter.
|
public ValidationUsingINotifyDataErrorInfoViewModel()
{
// Listen to changes of "ValidationUsingINotifyDataErrorInfo" and re-evaluate the validation
this.WhenAnyValue(x => x.EMail)
.Subscribe(_ => Validate_EMail());
// run INotifyDataErrorInfo-validation on start-up
Validate_EMail();
}
Let’s add another TabItem
into our TabControl
:
<TabControl TabStripPlacement="Bottom">
<!--Validation using INotifyDataErrorInfo-->
<TabItem Header="INotifyDataErrorInfo">
<TextBox Text="{Binding ValidationUsingINotifyDataErrorInfoViewModel.EMail}" />
</TabItem>
</TabControl>
In your IDE hit Run
or Debug
to see the result:
ℹ️
|
Some MVVM -frameworks like the [Windows MVVM Community Toolkit] implement the INotifyDataErrorInfo -interface and are using [DataAnnotations ] to run the validation. In this case you may end up seeing your validation twice. If you are facing such an issue, you can [disable one of the validators] in your App.
|
The third option we want to show you is throwing an [Exception] inside the setter of your property.
❗
|
This only works inside setters. Exceptions inside the getter of your property are not allowed and will result in a crash of your application. |
Once again we add another file into the ViewModels
-folder, called ValidationUsingExceptionInsideSetterViewModel
. As before, also this class is added as a read-only property into our MainWindowViewModel
:
/// <summary>
/// Gets a ViewModel showing how to use Exceptions inside the setter for validation
/// </summary>
public ValidationUsingExceptionInsideSetterViewModel ValidationUsingExceptionInsideSetterViewModel { get; }
= new ValidationUsingExceptionInsideSetterViewModel();
As written above we can throw exceptions inside the setter of a property. We will have a set of if
-conditions, where we throw an Exception
under a certain condition.
public class ValidationUsingExceptionInsideSetterViewModel : ViewModelBase
{
private string? _EMail;
/// <summary>
/// Validation using Exceptions (only inside setter allowed!)
/// </summary>
public string? EMail
{
get { return _EMail; }
set
{
// The field may not be null or empty
if (string.IsNullOrEmpty(value))
{
throw new ArgumentNullException(nameof(EMail), "This field is required");
}
// The field must contain an '@' sign
else if (!value.Contains('@'))
{
throw new ArgumentException(nameof(EMail), "Not a valid E-Mail-Address");
}
// The checks were successful, so we can store the value
else
{
this.RaiseAndSetIfChanged(ref _EMail, value);
}
}
}
}
We add another TabItem
into our TabControl
.
<TabControl TabStripPlacement="Bottom">
<!--Validation using Exceptions in setter-->
<TabItem Header="ExceptionInsideSetter">
<TextBox Text="{Binding ValidationUsingExceptionInsideSetterViewModel.EMail}" />
</TabItem>
</TabControl>
Below please find a collection with helpful links: