Integrating SfDataForm with FluentValidation

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:

  • I'm applying the behavior to my SfDataForm in XAML like this:

    <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:

    1. Validation Errors Not Displaying Initially:

      • The validation errors are not displayed when the page first loads or when the DataObject is initially set. They only appear after the user interacts with the input fields.

    2. Inaccurate Validation Timing:

      • When typing in a text input, the validation error sometimes reports an incorrect number of characters. For example, if a field requires a minimum of 3 characters, the error might be triggered when only 2 characters have been typed. This suggests a timing issue where the validation is running before the property value is fully updated.

    3. Clearing Input Not Triggering Validation:

      • If I completely delete the value from a text input, the validation error indicating that the field is required is not immediately displayed. It only appears after I start typing a new value.

    4. Accessing Editor Control:

      • I am having trouble to access to the underlying editor control (Entry, SfComboBox, etc.) within the DataFormOnGenerateDataFormItem event to attach to the Unfocused event. The DataFormItem does not expose a View property as initially expected.

    Specific Questions:

    1. What is the correct approach to trigger initial validation when the SfDataForm is loaded and the DataObject is set?

    2. How can I ensure that the validation logic always runs against the most up-to-date property value, avoiding the timing issues I'm seeing?

    3. What is the recommended way to access the underlying editor control for each DataFormItem so that I can attach to the Unfocused event and trigger validation when the field loses focus, even if the value hasn't changed?

    4. Are there any best practices or recommended patterns for integrating SfDataForm with FluentValidation that I should be aware of?



1 Reply

VM Vidyalakshmi Mani Syncfusion Team February 7, 2025 02:58 PM UTC

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.



Attachment: MAUIDataForm_e936f718.zip

Loader.
Up arrow icon