dotnet icon indicating copy to clipboard operation
dotnet copied to clipboard

Add properties for validation state and error

Open tuyen-vuduc opened this issue 2 years ago • 3 comments

Overview

I tried to add validations for my sign in and sign forms based on ObservableValidator.

I found that there are many lines of code can be generated by a generator.

Here is a snapshot of my code to have validation in my form

namespace ChickAndPaddy;

public abstract class BaseFormModel : ObservableValidator
{
    protected virtual string[] ValidatableAndSupportPropertyNames => new string[0];

    public virtual bool IsValid()
    {
        ValidateAllProperties();

        foreach (var propertyName in ValidatableAndSupportPropertyNames)
        {
            OnPropertyChanged(propertyName);
        }

        return !HasErrors;
    }
}

public partial class ForgotPasswordFormModel : BaseFormModel
{
    [ObservableProperty]
    [NotifyPropertyChangedFor(nameof(PhoneNumberValid), nameof(PhoneNumberInvalidMessage))]
    [Required(ErrorMessage = "Your phone number is required to recover your password.")]
    [Phone(ErrorMessage = "You have enter an invalid phone number.")]
    [NotifyDataErrorInfo]
    string phoneNumber;

    public bool PhoneNumberValid => GetErrors(nameof(PhoneNumber)).Any() == false;
    public string PhoneNumberInvalidMessage => GetErrors(nameof(PhoneNumber)).FirstOrDefault()?.ErrorMessage;

    protected override string[] ValidatableAndSupportPropertyNames => new[]
    {
        nameof(PhoneNumber),
        nameof(PhoneNumberValid),
        nameof(PhoneNumberInvalidMessage),
    };
}

Another version

public partial class ForgotPasswordFormModel : BaseFormModel
{
    [ObservableProperty]
    [NotifyPropertyChangedFor(nameof(PhoneNumberErrors))]
    [Required(ErrorMessage = "Your phone number is required to recover your password.")]
    [Phone(ErrorMessage = "You have enter an invalid phone number.")]
    [NotifyDataErrorInfo]
    string phoneNumber;

    public string PhoneNumberErrors => GetErrors(nameof(PhoneNumber));

    protected override string[] ValidatableAndSupportPropertyNames => new[]
    {
        nameof(PhoneNumber),
        nameof(PhoneNumberValid),
        nameof(PhoneNumberInvalidMessage),
    };
}

API breakdown

  • Add IsValid to ObservableValidator class

    • To validate all properties and
    • To notify validatable and support properties (so is the UI reflected)
  • Changes to the generated observable property

    • We should make a call to SetProperty instead of assigning backing field directly if the base class is ObservableObject
    • We should call to SetProperty version of ObservableValidator if the owning class inherits from that operation with param validate assigned to true if there is any validation attributes

Current generated code

/// <inheritdoc cref="userName"/>
        [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "8.0.0.0")]
        [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
        [global::System.ComponentModel.DataAnnotations.RequiredAttribute(ErrorMessage = "Please enter your phone number")]
        [global::System.ComponentModel.DataAnnotations.PhoneAttribute(ErrorMessage = "Please enter a valid phone number")]
        public string UserName
        {
            get => userName;
            set
            {
                if (!global::System.Collections.Generic.EqualityComparer<string>.Default.Equals(userName, value))
                {
                    OnUserNameChanging(value);
                    OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.UserName);
                    userName = value;
                    OnUserNameChanged(value);
                    OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.UserName);
                    OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.UserNameValid);
                    OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.UserNameInvalidMessage);
                }
            }
        }

Base class is ObjectValidator

/// <inheritdoc cref="userName"/>
        [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "8.0.0.0")]
        [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
        [global::System.ComponentModel.DataAnnotations.RequiredAttribute(ErrorMessage = "Please enter your phone number")]
        [global::System.ComponentModel.DataAnnotations.PhoneAttribute(ErrorMessage = "Please enter a valid phone number")]
        public string UserName
        {
            get => userName;
            set
            {
                if (SetProperty(ref userName, value, global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.UserName))
                {                   OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.UserNameValid);
                    OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.UserNameInvalidMessage);
                }
            }
        }

Base class is ObservableObject

/// <inheritdoc cref="userName"/>
        [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "8.0.0.0")]
        [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
        [global::System.ComponentModel.DataAnnotations.RequiredAttribute(ErrorMessage = "Please enter your phone number")]
        [global::System.ComponentModel.DataAnnotations.PhoneAttribute(ErrorMessage = "Please enter a valid phone number")]
        public string UserName
        {
            get => userName;
            set
            {
                var validate = true;  // check by generator to know if there are any validation attributes attached to the field
                if (SetProperty(ref userName, value, validate, global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.UserName))
                {                   OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.UserNameValid);
                    OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.UserNameInvalidMessage);
                }
            }
        }

Usage example

// Call this method to validate and notify validatable and support properties
validator.IsValid();

Breaking change?

No

Alternatives

Define properties manually

namespace ChickAndPaddy;

public abstract class BaseFormModel : ObservableValidator
{
    protected virtual string[] ValidatableAndSupportPropertyNames => new string[0];

    public virtual bool IsValid()
    {
        ValidateAllProperties();

        foreach (var propertyName in ValidatableAndSupportPropertyNames)
        {
            OnPropertyChanged(propertyName);
        }

        return !HasErrors;
    }
}

public partial class ForgotPasswordFormModel : BaseFormModel
{
    [ObservableProperty]
    [NotifyPropertyChangedFor(nameof(PhoneNumberValid), nameof(PhoneNumberInvalidMessage))]
    [Required(ErrorMessage = "Your phone number is required to recover your password.")]
    [Phone(ErrorMessage = "You have enter an invalid phone number.")]
    [NotifyDataErrorInfo]
    string phoneNumber;

    public bool PhoneNumberValid => GetErrors(nameof(PhoneNumber)).Any() == false;
    public string PhoneNumberInvalidMessage => GetErrors(nameof(PhoneNumber)).FirstOrDefault()?.ErrorMessage;

    protected override string[] ValidatableAndSupportPropertyNames => new[]
    {
        nameof(PhoneNumber),
        nameof(PhoneNumberValid),
        nameof(PhoneNumberInvalidMessage),
    };
}

Another version

public partial class ForgotPasswordFormModel : BaseFormModel
{
    [ObservableProperty]
    [NotifyPropertyChangedFor(nameof(PhoneNumberErrors))]
    [Required(ErrorMessage = "Your phone number is required to recover your password.")]
    [Phone(ErrorMessage = "You have enter an invalid phone number.")]
    [NotifyDataErrorInfo]
    string phoneNumber;

    public string PhoneNumberErrors => GetErrors(nameof(PhoneNumber));

    protected override string[] ValidatableAndSupportPropertyNames => new[]
    {
        nameof(PhoneNumber),
        nameof(PhoneNumberValid),
        nameof(PhoneNumberInvalidMessage),
    };
}

Additional context

No response

Help us help you

Yes, I'd like to be assigned to work on this item

tuyen-vuduc avatar Oct 28 '22 19:10 tuyen-vuduc

[ObservableProperty]
[NotifyPropertyChangedFor(nameof(PhoneNumberValid), nameof(PhoneNumberInvalidMessage))]
[Required(ErrorMessage = "Your phone number is required to recover your password.")]
[Phone(ErrorMessage = "You have enter an invalid phone number.")]
string phoneNumber;

It seems you didn't add [NotifyDataErrorInfo], doesn't that solve the issue already? The generator doesn't generate validation code by default, that's by design, it's opt-in 🙂

Sergio0694 avatar Oct 28 '22 19:10 Sergio0694

@Sergio0694 Thanks for your suggestion.

By using [NotifyDataErrorInfo], we don't need partial void On[PropertyName]Changing(string value) implementation any more.

However, if there is a way to add other properties as well, it will really save time and effort. (I am a bit old guy of web validation where I want to validate inputs individually as well as the form as a whole)

The other point, if we can make use of SetProperty in ObservableObject or ObservableValidator, we can shorten the generated code.

How do you think?

tuyen-vuduc avatar Oct 29 '22 04:10 tuyen-vuduc

If you look carefully, my suggestion brings in new methods to do the web form like validation,

public abstract class BaseFormModel : ObservableValidator
{
    protected virtual string[] ValidatableAndSupportPropertyNames => new string[0];

    public virtual bool IsValid()
    {
        ValidateAllProperties();

        foreach (var propertyName in ValidatableAndSupportPropertyNames)
        {
            OnPropertyChanged(propertyName);
        }

        return !HasErrors;
    }
}

If we can bring them into ObservableValidator class and generate ValidatableAndSupportPropertyNames as the subclass, it'll really help.

tuyen-vuduc avatar Oct 29 '22 04:10 tuyen-vuduc