Implementing robust data validation for a seamless user experience.
In Model-View-ViewModel (MVVM) architecture, validation is a crucial aspect of ensuring data integrity and providing immediate feedback to the user. .NET MAUI, being a modern cross-platform framework, offers excellent support for implementing validation patterns within your applications.
Effective validation helps prevent invalid data from being processed, leading to a more stable and user-friendly application. This guide will walk you through common approaches to implement MVVM-friendly validation in your .NET MAUI projects.
Several components play a vital role in achieving effective validation:
INotifyDataErrorInfo.INotifyDataErrorInfoThe INotifyDataErrorInfo interface is a cornerstone for implementing data validation in .NET. It allows your objects to report validation errors asynchronously and synchronously.
Here's a simplified example of a ViewModel implementing this interface:
public class UserViewModel : INotifyDataErrorInfo, INotifyPropertyChanged
{
private string _username;
private Dictionary> _errors = new Dictionary>();
public string Username
{
get => _username;
set
{
if (_username != value)
{
_username = value;
OnPropertyChanged(nameof(Username));
ValidateUsername();
}
}
}
public bool HasErrors => _errors.Any(kvp => kvp.Value?.Any() ?? false);
public event EventHandler ErrorsChanged;
private void ValidateUsername()
{
var newErrors = new List();
if (string.IsNullOrWhiteSpace(Username))
{
newErrors.Add("Username cannot be empty.");
}
else if (Username.Length < 3)
{
newErrors.Add("Username must be at least 3 characters long.");
}
UpdateErrors("Username", newErrors);
}
private void UpdateErrors(string propertyName, List newErrorMessages)
{
if (newErrorMessages != null && newErrorMessages.Any())
{
_errors[propertyName] = newErrorMessages;
}
else if (_errors.ContainsKey(propertyName))
{
_errors.Remove(propertyName);
}
ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(propertyName));
OnPropertyChanged(nameof(HasErrors));
}
public IEnumerable GetErrors(string propertyName)
{
return _errors.TryGetValue(propertyName, out var errors) ? errors : Enumerable.Empty<string>();
}
// INotifyPropertyChanged implementation (simplified)
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
_errors dictionary stores validation messages per property.Username property's setter calls ValidateUsername whenever it changes.ValidateUsername adds error messages to the _errors dictionary.UpdateErrors manages the dictionary and raises the ErrorsChanged event.HasErrors property indicates if any validation errors exist.GetErrors method is called by the binding system to retrieve errors for a specific property.For simpler validation scenarios, you can leverage .NET's built-in Data Annotations. These attributes can be applied directly to your Model properties.
To use Data Annotations effectively with MVVM and INotifyDataErrorInfo, you'll typically need a helper class or a library that bridges the gap.
ObservableValidator) that simplify Data Annotation integration with MVVM.
Here's an example using Data Annotations on a Model:
using System.ComponentModel.DataAnnotations;
public class Product
{
[Required(ErrorMessage = "Product name is required.")]
[StringLength(100, MinimumLength = 3, ErrorMessage = "Product name must be between 3 and 100 characters.")]
public string Name { get; set; }
[Range(0.01, double.MaxValue, ErrorMessage = "Price must be a positive value.")]
public decimal Price { get; set; }
}
You would then integrate these attributes into your ViewModel's validation logic, potentially using Validator.TryValidateObject or a dedicated validation framework.
The View needs to visually communicate validation errors to the user. .NET MAUI's ValidationBehavior or custom styling can achieve this.
ValidationBehavior (Conceptual Example)While not a built-in behavior in MAUI's core, many MVVM frameworks or custom implementations provide such a behavior. The concept is to attach a behavior to an input control that automatically listens for ErrorsChanged events and applies visual cues.
You can directly bind to the HasErrors property and the GetErrors method.
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:viewmodels="clr-namespace:YourApp.ViewModels"
x:Class="YourApp.Views.UserRegistrationPage"
x:DataType="viewmodels:UserViewModel">
<StackLayout Padding="20" Spacing="15">
<Label Text="Register User" FontSize="Large" HorizontalOptions="Center"/>
<Grid RowDefinitions="Auto,Auto" ColumnDefinitions="*,Auto">
<Entry Grid.Row="0" Grid.Column="0"
Placeholder="Username"
Text="{Binding Username}" />
<!-- Error indicator -->
<Image Grid.Row="0" Grid.Column="1"
Source="error_icon.png"
IsVisible="{Binding HasErrors, Converter={StaticResource InverseBooleanConverter}}"
HeightRequest="24" WidthRequest="24" Margin="5,0,0,0"/>
<!-- Displaying errors -->
<ListView Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="2"
ItemsSource="{Binding Errors, ElementName=page}" >
<ListView.ItemTemplate>
<DataTemplate>
<TextCell Text="{Binding}" TextColor="Red"/>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</Grid>
<!-- This binding requires a way to get all errors -->
<!-- A common pattern is to have a property like 'AllErrors' in ViewModel -->
<!-- For simplicity, we'll assume 'Errors' is a bindable property for the page -->
<!-- The ViewModel's GetErrors would typically be used by a behavior or validation system -->
<Button Text="Register" IsEnabled="{Binding !HasErrors}" />
</StackLayout>
<ContentPage.Resources>
<local:InverseBooleanConverter x:Key="InverseBooleanConverter"/>
</ContentPage.Resources>
</ContentPage>
In this XAML:
Entry is bound to the Username property.Image (like an error icon) is shown when HasErrors is false (meaning there are no errors to display, but we want to show an icon *if* there are potential errors). A more common approach is to bind to the *presence* of errors. Let's correct this logic.ListView is bound to display error messages. This requires a way to expose all errors from the ViewModel, perhaps via a custom property that flattens _errors. For a single property, you could bind to Errors.Username if your ViewModel exposes it directly. A better approach is a behavior.IsEnabled property of the Register button is disabled if HasErrors is true.If the username is empty, the error message "Username cannot be empty." would appear below the Entry control.
(Visual representation: Imagine a red border around the Entry and an error message listed below.)
INotifyDataErrorInfo.