CommunityToolkit / dotnet

.NET Community Toolkit is a collection of helpers and APIs that work for all .NET developers and are agnostic of any specific UI platform. The toolkit is maintained and published by Microsoft, and part of the .NET Foundation.
https://docs.microsoft.com/dotnet/communitytoolkit/?WT.mc_id=dotnet-0000-bramin
Other
3.05k stars 300 forks source link

Add properties for validation state and error #488

Open tuyen-vuduc opened 2 years ago

tuyen-vuduc commented 2 years ago

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

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

Sergio0694 commented 2 years ago
[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 🙂

tuyen-vuduc commented 2 years ago

@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 commented 2 years ago

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.