Hello,
I'm working on a .NET MAUI application and trying to integrate the SfDataForm component with the FluentValidation library for robust data validation. I've created a custom behavior to bridge the gap between SfDataForm and FluentValidation, but I'm running into some challenges and would appreciate your expertise.
Here's what I've implemented so far:
FluentValidation Validator:
I've defined a FluentValidation validator for my data transfer object (DTO), ReceiptItemDto:
public class ReceiptItemDtoValidator : AbstractValidator<ReceiptItemDto>
{
public ReceiptItemDtoValidator()
{
RuleFor(x => x.Id).NotEmpty();
RuleFor(x => x.Name).NotEmpty();
RuleFor(x => x.Quantity).GreaterThan(0);
RuleFor(x => x.Unit).NotEmpty();
RuleFor(x => x.UnitPrice).GreaterThan(0);
RuleFor(x => x.TotalPrice).GreaterThan(0);
RuleFor(x => x.Category).NotEmpty();
}
}
I've created a custom behavior that attaches to the SfDataForm and performs validation using the FluentValidation validator. The behavior is responsible for triggering validation, applying the validation results, and displaying error messages.
The code for the behavior is as follows:
using System.ComponentModel;
using FluentValidation;
using Syncfusion.Maui.DataForm;
using Microsoft.Maui.Controls; // Import this!
using Syncfusion.Maui.Editors; // Import this!
namespace SnapSpend.Features.Home.Behaviors;
public class FluentValidationBehavior<T> : Behavior<SfDataForm> where T : class
{
private SfDataForm? _dataForm;
private bool _isValidating;
public static readonly BindableProperty ValidatorProperty = BindableProperty.Create(
nameof(Validator),
typeof(IValidator<T>),
typeof(FluentValidationBehavior<T>),
null,
propertyChanged: OnValidatorChanged);
public IValidator<T>? Validator
{
get => (IValidator<T>)GetValue(ValidatorProperty);
set => SetValue(ValidatorProperty, value);
}
protected override void OnAttachedTo(SfDataForm dataForm)
{
base.OnAttachedTo(dataForm);
_dataForm = dataForm;
_dataForm.ValidateForm += OnValidateForm;
_dataForm.PropertyChanged += OnDataFormPropertyChanged;
_dataForm.GenerateDataFormItem += DataFormOnGenerateDataFormItem;
_dataForm.ValidateProperty += (_, args) => ValidateProperty(args.PropertyName);
// Validate on initial load
if (_dataForm is not null)
{
MainThread.BeginInvokeOnMainThread(ValidateAllProperties);
}
}
private static void OnValidatorChanged(BindableObject bindable, object oldValue, object newValue)
{
if (bindable is FluentValidationBehavior<T> behavior && behavior._dataForm != null)
{
behavior.ValidateAllProperties();
}
}
private void DataFormOnGenerateDataFormItem(object? sender, GenerateDataFormItemEventArgs e)
{
e.DataFormItem.PropertyChanged += (_, args) =>
{
if (string.IsNullOrEmpty(args.PropertyName)) return;
var result = ValidateProperty(args.PropertyName);
if (result.Item1) return;
_dataForm?.ScrollTo(args.PropertyName);
};
}
private void ValidateAllProperties()
{
if (_dataForm?.DataObject is not T || Validator is null) return;
var properties = typeof(T).GetProperties()
.Where(p => p is { CanRead: true, CanWrite: true })
.Select(p => p.Name)
.ToList();
_dataForm.Validate(properties);
UpdateFormValidState();
}
private (bool, string) ValidateProperty(string propertyName)
{
var output = (true, string.Empty);
if (_dataForm?.DataObject is not T model || Validator is null) return output;
var validationContext = new ValidationContext<T>(model);
var validationResult = Validator.Validate(validationContext);
var propertyErrors = validationResult.Errors
.Where(x => x.PropertyName == propertyName)
.ToList();
if (_dataForm.DataObject is INotifyDataErrorInfo notifyDataErrorInfo)
{
// Use reflection to access AddError and ClearErrors methods
var clearErrorsMethod = notifyDataErrorInfo.GetType().GetMethod("ClearErrors");
var addErrorMethod = notifyDataErrorInfo.GetType().GetMethod("AddError");
if (clearErrorsMethod != null && addErrorMethod != null)
{
clearErrorsMethod.Invoke(notifyDataErrorInfo, new object[] { propertyName });
if (propertyErrors is { Count: > 0 })
{
var errorMessage = string.Join(Environment.NewLine, propertyErrors.Select(x => x.ErrorMessage));
addErrorMethod.Invoke(notifyDataErrorInfo, new object[] { propertyName, errorMessage });
output = (false, errorMessage);
}
}
}
UpdateFormValidState();
return output;
}
protected override void OnDetachingFrom(SfDataForm bindable)
{
if (_dataForm is not null)
{
_dataForm.ValidateForm -= OnValidateForm;
_dataForm.PropertyChanged -= OnDataFormPropertyChanged;
_dataForm.GenerateDataFormItem -= DataFormOnGenerateDataFormItem;
}
base.OnDetachingFrom(bindable);
}
private void OnDataFormPropertyChanged(object? sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(SfDataForm.DataObject) && !_isValidating)
{
ValidateAllProperties();
}
}
private void OnValidateForm(object? sender, DataFormValidateFormEventArgs e)
{
if (_dataForm?.DataObject is not T || Validator is null) return;
_isValidating = true;
ValidateAllProperties();
_isValidating = false;
}
private void UpdateFormValidState()
{
if (_dataForm?.DataObject is not T model || Validator is null) return;
var validationResult = Validator.Validate(model);
if (_dataForm.BindingContext is IValidatable validatable)
{
validatable.IsValid = validationResult.IsValid;
}
}
}
XAML Usage:
<dataForm:SfDataForm x:Name="SfDataForm"
ValidationMode="PropertyChanged"
DataObject="{Binding SelectedReceiptItem}">
<dataForm:SfDataForm.Behaviors>
<behaviors1:FluentValidationBehavior x:Name="ValidationBehavior"
x:TypeArguments="dtos:ReceiptItemDto">
</behaviors1:FluentValidationBehavior>
</dataForm:SfDataForm.Behaviors>
</dataForm:SfDataForm>Challenges I'm Facing:
Hi Mihai ,
Thank you for reaching out. Based on your code snippets, we have created a simple sample which is attached for your reference. To help us investigate the issue, please either modify this sample to reproduce the problem or share the sample where you are experiencing the issue. This will allow us to analyze the behavior and provide the best possible solution.
Regards,
Vidyalakshmi M.