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
2.99k stars 294 forks source link

Implements NotifyPropertyValidationForAttribute #718

Open danielmeza opened 1 year ago

danielmeza commented 1 year ago

Overview

Somes read-only properties may dependes on other to be validated, when the source property change, we shall be able to trigger validation in to a target property that dependes on it.

So a [NotifyPropertyValidationFor(nameof(TargetProperty))] can be used to annotate the source property and then generate the code to call ValidateProperty(TargetProperty, nameof(TargetProperty));

API breakdown

Notify property change attribute used to decorate validation dependencies.


// <summary>
/// An attribute that can be used to validate the target property when te annotated property changes. When this attribute is
/// used, the generated property setter will also call <see cref="ObservableValidator.Validate(object value, string propertyName)"/> for the properties specified in the attribute data. This can be useful to keep the code compact when
/// there are one or more dependent properties that should be validated when the value of the annotated observable
/// property is changed. If this attribute is used in a field without <see cref="ObservablePropertyAttribute"/>, it is ignored.
/// </summary>
[AttributeUsage(AttributeTargets.Field, AllowMultiple = true, Inherited = false)]
public class NotifyPropertyValidationForAttribute : Attribute 
{
    /// <summary>
    /// Initializes a new instance of the <see cref="NotifyPropertyValidationForAttribute"/> class.
    /// </summary>
    /// <param name="propertyName">The name of the property to validate when the annotated property changes.</param>
    public NotifyPropertyValidationForAttribute(string propertyName);

    /// <summary>
    /// Initializes a new instance of the <see cref="NotifyPropertyValidationForAttribute"/> class.
    /// </summary>
    /// <param name="propertyName">The name of the property validate when the annotated property changes.</param>
    /// <param name="otherPropertyNames">
    /// The other property names to validate when the annotated property changes. This parameter can optionally
    /// be used to indicate a series of dependent properties from the same attribute, to keep the code more compact.
    /// </param>
    public NotifyPropertyValidationForAttribute(string propertyName,  params string[] otherPropertyNames);

    /// <summary>
    /// Gets the property names to validate when the annotated property changes.
    /// </summary>
    public string[] PropertyNames {  get; }
}

The Source Generator for properties shall include the call to ValidateProperty after notify the change and only when the property has changed like this.


        /// <inheritdoc cref="_name"/>
        [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "8.2.0.0")]
        [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
        public int Name
        {
            get => _name;
            set
            {
                if (!global::System.Collections.Generic.EqualityComparer<int>.Default.Equals(_name, value))
                {
                    OnNameChanging(value);
                    OnNameChanging(default, value);
                    OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.Name);
                    _name = value;
                    ValidateProperty(value, "SelectedBirthMonthIndex");
                    OnNameChanged(value);
                    OnNameChanged(default, value);
                    //Notify validation dependency property here
                    ValidateProperty(FullName, "FullName")

OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.FullName);
                }
            }
        }

Usage example

In the next sample the FullName property depends on _name and _lastName and as a computed property validation needs to be fired externally, currently we can do this using the partials method on each source property but annotate the property with NotifyPropertyValidationFor will be easier.

public class MyClass : ObservableValidator
{
    [Required]
    [ObservableProperty]
    [NotifyPropertyValidationFor(nameof(FullName))]
    private string _name;

    [Required[
    [ObservableProperty]
    [NotifyPropertyValidationFor(nameof(FullName))]
    private string _lastName;

    [MinLength(5)]
    [MaxLength(100)]
    [CustomValidation(typeof(MyClass), nameof(ValidateFullName))]
    public string FullName => $"{Name} {LastName}";

   public static ValidationResult ValidateFullName(string name, ValidationContext context)
    {
        MyClass instance = (MyClass)context.ObjectInstance;
        bool isValid = CustonValidationHere(FullName);

        if (isValid)
        {
            return ValidationResult.Success;
        }

        return new("The name was not validated by the fancy custom validator");
    }
}

Breaking change?

I'm not sure

Alternatives

As mention before the current alternative to implement the OnPropertyChanged partial method for each source property and call to ValidateProperty(TargetProperty, nameof(TargetProperty));

Like this:

public class MyClass : ObservableValidator
{
    [Required]
    [ObservableProperty]
    private string _name;

    [Required[
    [ObservableProperty]
    private string _lastName;

    [MinLength(5)]
    [MaxLength(100)]
    [CustomValidation(typeof(MyClass), nameof(ValidateFullName))]
    public string FullName => $"{Name} {LastName}";

   public static ValidationResult ValidateFullName(string name, ValidationContext context)
    {
        MyClass instance = (MyClass)context.ObjectInstance;
        bool isValid = CustonValidationHere(FullName);

        if (isValid)
        {
            return ValidationResult.Success;
        }

        return new("The name was not validated by the fancy custom validator");
    }

    partial void OnNameChanged(string value)
    {
        ValidateProperty(FullName, nameof(FullName));
    }

    partial void OnLastNameChanged(string value)
    {
        ValidateProperty(FullName, nameof(FullName));
    }
}

Additional context

No response

Help us help you

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