123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832 |
- // Licensed to the .NET Foundation under one or more agreements.
- // The .NET Foundation licenses this file to you under the MIT license.
- // See the LICENSE file in the project root for more information.
- using System;
- using System.Collections;
- using System.Collections.Generic;
- using System.ComponentModel;
- using System.ComponentModel.DataAnnotations;
- using System.Diagnostics.CodeAnalysis;
- using System.Linq;
- using System.Linq.Expressions;
- using System.Reflection;
- using System.Runtime.CompilerServices;
- namespace CommunityToolkit.Mvvm.ComponentModel;
- /// <summary>
- /// A base class for objects implementing the <see cref="INotifyDataErrorInfo"/> interface. This class
- /// also inherits from <see cref="ObservableObject"/>, so it can be used for observable items too.
- /// </summary>
- public abstract class ObservableValidator : ObservableObject, INotifyDataErrorInfo
- {
- /// <summary>
- /// The <see cref="ConditionalWeakTable{TKey,TValue}"/> instance used to track compiled delegates to validate entities.
- /// </summary>
- private static readonly ConditionalWeakTable<Type, Action<object>> EntityValidatorMap = new();
- /// <summary>
- /// The <see cref="ConditionalWeakTable{TKey, TValue}"/> instance used to track display names for properties to validate.
- /// </summary>
- /// <remarks>
- /// This is necessary because we want to reuse the same <see cref="ValidationContext"/> instance for all validations, but
- /// with the same behavior with respect to formatted names that new instances would have provided. The issue is that the
- /// <see cref="ValidationContext.DisplayName"/> property is not refreshed when we set <see cref="ValidationContext.MemberName"/>,
- /// so we need to replicate the same logic to retrieve the right display name for properties to validate and update that
- /// property manually right before passing the context to <see cref="Validator"/> and proceed with the normal functionality.
- /// </remarks>
- private static readonly ConditionalWeakTable<Type, Dictionary<string, string>> DisplayNamesMap = new();
- /// <summary>
- /// The cached <see cref="PropertyChangedEventArgs"/> for <see cref="HasErrors"/>.
- /// </summary>
- private static readonly PropertyChangedEventArgs HasErrorsChangedEventArgs = new(nameof(HasErrors));
- /// <summary>
- /// The <see cref="ValidationContext"/> instance currently in use.
- /// </summary>
- private readonly ValidationContext validationContext;
- /// <summary>
- /// The <see cref="Dictionary{TKey,TValue}"/> instance used to store previous validation results.
- /// </summary>
- private readonly Dictionary<string, List<ValidationResult>> errors = new();
- /// <summary>
- /// Indicates the total number of properties with errors (not total errors).
- /// This is used to allow <see cref="HasErrors"/> to operate in O(1) time, as it can just
- /// check whether this value is not 0 instead of having to traverse <see cref="errors"/>.
- /// </summary>
- private int totalErrors;
- /// <inheritdoc/>
- public event EventHandler<DataErrorsChangedEventArgs>? ErrorsChanged;
- /// <summary>
- /// Initializes a new instance of the <see cref="ObservableValidator"/> class.
- /// This constructor will create a new <see cref="ValidationContext"/> that will
- /// be used to validate all properties, which will reference the current instance
- /// and no additional services or validation properties and settings.
- /// </summary>
- [RequiresUnreferencedCode("The type of the current instance cannot be statically discovered.")]
- protected ObservableValidator()
- {
- this.validationContext = new ValidationContext(this);
- }
- /// <summary>
- /// Initializes a new instance of the <see cref="ObservableValidator"/> class.
- /// This constructor will create a new <see cref="ValidationContext"/> that will
- /// be used to validate all properties, which will reference the current instance.
- /// </summary>
- /// <param name="items">A set of key/value pairs to make available to consumers.</param>
- [RequiresUnreferencedCode("The type of the current instance cannot be statically discovered.")]
- protected ObservableValidator(IDictionary<object, object?>? items)
- {
- this.validationContext = new ValidationContext(this, items);
- }
- /// <summary>
- /// Initializes a new instance of the <see cref="ObservableValidator"/> class.
- /// This constructor will create a new <see cref="ValidationContext"/> that will
- /// be used to validate all properties, which will reference the current instance.
- /// </summary>
- /// <param name="serviceProvider">An <see cref="IServiceProvider"/> instance to make available during validation.</param>
- /// <param name="items">A set of key/value pairs to make available to consumers.</param>
- [RequiresUnreferencedCode("The type of the current instance cannot be statically discovered.")]
- protected ObservableValidator(IServiceProvider? serviceProvider, IDictionary<object, object?>? items)
- {
- this.validationContext = new ValidationContext(this, serviceProvider, items);
- }
- /// <summary>
- /// Initializes a new instance of the <see cref="ObservableValidator"/> class.
- /// This constructor will store the input <see cref="ValidationContext"/> instance,
- /// and it will use it to validate all properties for the current viewmodel.
- /// </summary>
- /// <param name="validationContext">
- /// The <see cref="ValidationContext"/> instance to use to validate properties.
- /// <para>
- /// This instance will be passed to all <see cref="Validator.TryValidateObject(object, ValidationContext, ICollection{ValidationResult})"/>
- /// calls executed by the current viewmodel, and its <see cref="ValidationContext.MemberName"/> property will be updated every time
- /// before the call is made to set the name of the property being validated. The property name will not be reset after that, so the
- /// value of <see cref="ValidationContext.MemberName"/> will always indicate the name of the last property that was validated, if any.
- /// </para>
- /// </param>
- /// <exception cref="System.ArgumentNullException">Thrown if <paramref name="validationContext"/> is <see langword="null"/>.</exception>
- protected ObservableValidator(ValidationContext validationContext)
- {
- ArgumentNullException.ThrowIfNull(validationContext);
- this.validationContext = validationContext;
- }
- /// <inheritdoc/>
- [Display(AutoGenerateField = false)]
- public bool HasErrors => this.totalErrors > 0;
- /// <summary>
- /// Compares the current and new values for a given property. If the value has changed,
- /// raises the <see cref="ObservableObject.PropertyChanging"/> event, updates the property with
- /// the new value, then raises the <see cref="ObservableObject.PropertyChanged"/> event.
- /// </summary>
- /// <typeparam name="T">The type of the property that changed.</typeparam>
- /// <param name="field">The field storing the property's value.</param>
- /// <param name="newValue">The property's value after the change occurred.</param>
- /// <param name="validate">If <see langword="true"/>, <paramref name="newValue"/> will also be validated.</param>
- /// <param name="propertyName">(optional) The name of the property that changed.</param>
- /// <returns><see langword="true"/> if the property was changed, <see langword="false"/> otherwise.</returns>
- /// <remarks>
- /// This method is just like <see cref="ObservableObject.SetProperty{T}(ref T,T,string)"/>, just with the addition
- /// of the <paramref name="validate"/> parameter. If that is set to <see langword="true"/>, the new value will be
- /// validated and <see cref="ErrorsChanged"/> will be raised if needed. Following the behavior of the base method,
- /// the <see cref="ObservableObject.PropertyChanging"/> and <see cref="ObservableObject.PropertyChanged"/> events
- /// are not raised if the current and new value for the target property are the same.
- /// </remarks>
- /// <exception cref="System.ArgumentNullException">Thrown if <paramref name="propertyName"/> is <see langword="null"/>.</exception>
- [RequiresUnreferencedCode("The type of the current instance cannot be statically discovered.")]
- protected bool SetProperty<T>([NotNullIfNotNull(nameof(newValue))] ref T field, T newValue, bool validate, [CallerMemberName] string propertyName = null!)
- {
- ArgumentNullException.ThrowIfNull(propertyName);
- bool propertyChanged = SetProperty(ref field, newValue, propertyName);
- if (propertyChanged && validate)
- {
- ValidateProperty(newValue, propertyName);
- }
- return propertyChanged;
- }
- /// <summary>
- /// Compares the current and new values for a given property. If the value has changed,
- /// raises the <see cref="ObservableObject.PropertyChanging"/> event, updates the property with
- /// the new value, then raises the <see cref="ObservableObject.PropertyChanged"/> event.
- /// See additional notes about this overload in <see cref="SetProperty{T}(ref T,T,bool,string)"/>.
- /// </summary>
- /// <typeparam name="T">The type of the property that changed.</typeparam>
- /// <param name="field">The field storing the property's value.</param>
- /// <param name="newValue">The property's value after the change occurred.</param>
- /// <param name="comparer">The <see cref="IEqualityComparer{T}"/> instance to use to compare the input values.</param>
- /// <param name="validate">If <see langword="true"/>, <paramref name="newValue"/> will also be validated.</param>
- /// <param name="propertyName">(optional) The name of the property that changed.</param>
- /// <returns><see langword="true"/> if the property was changed, <see langword="false"/> otherwise.</returns>
- /// <exception cref="System.ArgumentNullException">Thrown if <paramref name="comparer"/> or <paramref name="propertyName"/> are <see langword="null"/>.</exception>
- [RequiresUnreferencedCode("The type of the current instance cannot be statically discovered.")]
- protected bool SetProperty<T>([NotNullIfNotNull(nameof(newValue))] ref T field, T newValue, IEqualityComparer<T> comparer, bool validate, [CallerMemberName] string propertyName = null!)
- {
- ArgumentNullException.ThrowIfNull(comparer);
- ArgumentNullException.ThrowIfNull(propertyName);
- bool propertyChanged = SetProperty(ref field, newValue, comparer, propertyName);
- if (propertyChanged && validate)
- {
- ValidateProperty(newValue, propertyName);
- }
- return propertyChanged;
- }
- /// <summary>
- /// Compares the current and new values for a given property. If the value has changed,
- /// raises the <see cref="ObservableObject.PropertyChanging"/> event, updates the property with
- /// the new value, then raises the <see cref="ObservableObject.PropertyChanged"/> event. Similarly to
- /// the <see cref="ObservableObject.SetProperty{T}(T,T,Action{T},string)"/> method, this overload should only be
- /// used when <see cref="ObservableObject.SetProperty{T}(ref T,T,string)"/> can't be used directly.
- /// </summary>
- /// <typeparam name="T">The type of the property that changed.</typeparam>
- /// <param name="oldValue">The current property value.</param>
- /// <param name="newValue">The property's value after the change occurred.</param>
- /// <param name="callback">A callback to invoke to update the property value.</param>
- /// <param name="validate">If <see langword="true"/>, <paramref name="newValue"/> will also be validated.</param>
- /// <param name="propertyName">(optional) The name of the property that changed.</param>
- /// <returns><see langword="true"/> if the property was changed, <see langword="false"/> otherwise.</returns>
- /// <remarks>
- /// This method is just like <see cref="ObservableObject.SetProperty{T}(T,T,Action{T},string)"/>, just with the addition
- /// of the <paramref name="validate"/> parameter. As such, following the behavior of the base method,
- /// the <see cref="ObservableObject.PropertyChanging"/> and <see cref="ObservableObject.PropertyChanged"/> events
- /// are not raised if the current and new value for the target property are the same.
- /// </remarks>
- /// <exception cref="System.ArgumentNullException">Thrown if <paramref name="callback"/> or <paramref name="propertyName"/> are <see langword="null"/>.</exception>
- [RequiresUnreferencedCode("The type of the current instance cannot be statically discovered.")]
- protected bool SetProperty<T>(T oldValue, T newValue, Action<T> callback, bool validate, [CallerMemberName] string propertyName = null!)
- {
- ArgumentNullException.ThrowIfNull(callback);
- ArgumentNullException.ThrowIfNull(propertyName);
- bool propertyChanged = SetProperty(oldValue, newValue, callback, propertyName);
- if (propertyChanged && validate)
- {
- ValidateProperty(newValue, propertyName);
- }
- return propertyChanged;
- }
- /// <summary>
- /// Compares the current and new values for a given property. If the value has changed,
- /// raises the <see cref="ObservableObject.PropertyChanging"/> event, updates the property with
- /// the new value, then raises the <see cref="ObservableObject.PropertyChanged"/> event.
- /// See additional notes about this overload in <see cref="SetProperty{T}(T,T,Action{T},bool,string)"/>.
- /// </summary>
- /// <typeparam name="T">The type of the property that changed.</typeparam>
- /// <param name="oldValue">The current property value.</param>
- /// <param name="newValue">The property's value after the change occurred.</param>
- /// <param name="comparer">The <see cref="IEqualityComparer{T}"/> instance to use to compare the input values.</param>
- /// <param name="callback">A callback to invoke to update the property value.</param>
- /// <param name="validate">If <see langword="true"/>, <paramref name="newValue"/> will also be validated.</param>
- /// <param name="propertyName">(optional) The name of the property that changed.</param>
- /// <returns><see langword="true"/> if the property was changed, <see langword="false"/> otherwise.</returns>
- /// <exception cref="System.ArgumentNullException">Thrown if <paramref name="comparer"/>, <paramref name="callback"/> or <paramref name="propertyName"/> are <see langword="null"/>.</exception>
- [RequiresUnreferencedCode("The type of the current instance cannot be statically discovered.")]
- protected bool SetProperty<T>(T oldValue, T newValue, IEqualityComparer<T> comparer, Action<T> callback, bool validate, [CallerMemberName] string propertyName = null!)
- {
- ArgumentNullException.ThrowIfNull(comparer);
- ArgumentNullException.ThrowIfNull(callback);
- ArgumentNullException.ThrowIfNull(propertyName);
- bool propertyChanged = SetProperty(oldValue, newValue, comparer, callback, propertyName);
- if (propertyChanged && validate)
- {
- ValidateProperty(newValue, propertyName);
- }
- return propertyChanged;
- }
- /// <summary>
- /// Compares the current and new values for a given nested property. If the value has changed,
- /// raises the <see cref="ObservableObject.PropertyChanging"/> event, updates the property and then raises the
- /// <see cref="ObservableObject.PropertyChanged"/> event. The behavior mirrors that of
- /// <see cref="ObservableObject.SetProperty{TModel,T}(T,T,TModel,Action{TModel,T},string)"/>, with the difference being that this
- /// method is used to relay properties from a wrapped model in the current instance. For more info, see the docs for
- /// <see cref="ObservableObject.SetProperty{TModel,T}(T,T,TModel,Action{TModel,T},string)"/>.
- /// </summary>
- /// <typeparam name="TModel">The type of model whose property (or field) to set.</typeparam>
- /// <typeparam name="T">The type of property (or field) to set.</typeparam>
- /// <param name="oldValue">The current property value.</param>
- /// <param name="newValue">The property's value after the change occurred.</param>
- /// <param name="model">The model </param>
- /// <param name="callback">The callback to invoke to set the target property value, if a change has occurred.</param>
- /// <param name="validate">If <see langword="true"/>, <paramref name="newValue"/> will also be validated.</param>
- /// <param name="propertyName">(optional) The name of the property that changed.</param>
- /// <returns><see langword="true"/> if the property was changed, <see langword="false"/> otherwise.</returns>
- /// <exception cref="System.ArgumentNullException">Thrown if <paramref name="model"/>, <paramref name="callback"/> or <paramref name="propertyName"/> are <see langword="null"/>.</exception>
- [RequiresUnreferencedCode("The type of the current instance cannot be statically discovered.")]
- protected bool SetProperty<TModel, T>(T oldValue, T newValue, TModel model, Action<TModel, T> callback, bool validate, [CallerMemberName] string propertyName = null!)
- where TModel : class
- {
- ArgumentNullException.ThrowIfNull(model);
- ArgumentNullException.ThrowIfNull(callback);
- ArgumentNullException.ThrowIfNull(propertyName);
- bool propertyChanged = SetProperty(oldValue, newValue, model, callback, propertyName);
- if (propertyChanged && validate)
- {
- ValidateProperty(newValue, propertyName);
- }
- return propertyChanged;
- }
- /// <summary>
- /// Compares the current and new values for a given nested property. If the value has changed,
- /// raises the <see cref="ObservableObject.PropertyChanging"/> event, updates the property and then raises the
- /// <see cref="ObservableObject.PropertyChanged"/> event. The behavior mirrors that of
- /// <see cref="ObservableObject.SetProperty{TModel,T}(T,T,IEqualityComparer{T},TModel,Action{TModel,T},string)"/>,
- /// with the difference being that this method is used to relay properties from a wrapped model in the
- /// current instance. For more info, see the docs for
- /// <see cref="ObservableObject.SetProperty{TModel,T}(T,T,IEqualityComparer{T},TModel,Action{TModel,T},string)"/>.
- /// </summary>
- /// <typeparam name="TModel">The type of model whose property (or field) to set.</typeparam>
- /// <typeparam name="T">The type of property (or field) to set.</typeparam>
- /// <param name="oldValue">The current property value.</param>
- /// <param name="newValue">The property's value after the change occurred.</param>
- /// <param name="comparer">The <see cref="IEqualityComparer{T}"/> instance to use to compare the input values.</param>
- /// <param name="model">The model </param>
- /// <param name="callback">The callback to invoke to set the target property value, if a change has occurred.</param>
- /// <param name="validate">If <see langword="true"/>, <paramref name="newValue"/> will also be validated.</param>
- /// <param name="propertyName">(optional) The name of the property that changed.</param>
- /// <returns><see langword="true"/> if the property was changed, <see langword="false"/> otherwise.</returns>
- /// <exception cref="System.ArgumentNullException">Thrown if <paramref name="comparer"/>, <paramref name="model"/>, <paramref name="callback"/> or <paramref name="propertyName"/> are <see langword="null"/>.</exception>
- [RequiresUnreferencedCode("The type of the current instance cannot be statically discovered.")]
- protected bool SetProperty<TModel, T>(T oldValue, T newValue, IEqualityComparer<T> comparer, TModel model, Action<TModel, T> callback, bool validate, [CallerMemberName] string propertyName = null!)
- where TModel : class
- {
- ArgumentNullException.ThrowIfNull(comparer);
- ArgumentNullException.ThrowIfNull(model);
- ArgumentNullException.ThrowIfNull(callback);
- ArgumentNullException.ThrowIfNull(propertyName);
- bool propertyChanged = SetProperty(oldValue, newValue, comparer, model, callback, propertyName);
- if (propertyChanged && validate)
- {
- ValidateProperty(newValue, propertyName);
- }
- return propertyChanged;
- }
- /// <summary>
- /// Tries to validate a new value for a specified property. If the validation is successful,
- /// <see cref="ObservableObject.SetProperty{T}(ref T,T,string?)"/> is called, otherwise no state change is performed.
- /// </summary>
- /// <typeparam name="T">The type of the property that changed.</typeparam>
- /// <param name="field">The field storing the property's value.</param>
- /// <param name="newValue">The property's value after the change occurred.</param>
- /// <param name="errors">The resulting validation errors, if any.</param>
- /// <param name="propertyName">(optional) The name of the property that changed.</param>
- /// <returns>Whether the validation was successful and the property value changed as well.</returns>
- /// <exception cref="System.ArgumentNullException">Thrown if <paramref name="propertyName"/> is <see langword="null"/>.</exception>
- [RequiresUnreferencedCode("The type of the current instance cannot be statically discovered.")]
- protected bool TrySetProperty<T>(ref T field, T newValue, out IReadOnlyCollection<ValidationResult> errors, [CallerMemberName] string propertyName = null!)
- {
- ArgumentNullException.ThrowIfNull(propertyName);
- return TryValidateProperty(newValue, propertyName, out errors) &&
- SetProperty(ref field, newValue, propertyName);
- }
- /// <summary>
- /// Tries to validate a new value for a specified property. If the validation is successful,
- /// <see cref="ObservableObject.SetProperty{T}(ref T,T,IEqualityComparer{T},string?)"/> is called, otherwise no state change is performed.
- /// </summary>
- /// <typeparam name="T">The type of the property that changed.</typeparam>
- /// <param name="field">The field storing the property's value.</param>
- /// <param name="newValue">The property's value after the change occurred.</param>
- /// <param name="comparer">The <see cref="IEqualityComparer{T}"/> instance to use to compare the input values.</param>
- /// <param name="errors">The resulting validation errors, if any.</param>
- /// <param name="propertyName">(optional) The name of the property that changed.</param>
- /// <returns>Whether the validation was successful and the property value changed as well.</returns>
- /// <exception cref="System.ArgumentNullException">Thrown if <paramref name="comparer"/> or <paramref name="propertyName"/> are <see langword="null"/>.</exception>
- [RequiresUnreferencedCode("The type of the current instance cannot be statically discovered.")]
- protected bool TrySetProperty<T>(ref T field, T newValue, IEqualityComparer<T> comparer, out IReadOnlyCollection<ValidationResult> errors, [CallerMemberName] string propertyName = null!)
- {
- ArgumentNullException.ThrowIfNull(comparer);
- ArgumentNullException.ThrowIfNull(propertyName);
- return TryValidateProperty(newValue, propertyName, out errors) &&
- SetProperty(ref field, newValue, comparer, propertyName);
- }
- /// <summary>
- /// Tries to validate a new value for a specified property. If the validation is successful,
- /// <see cref="ObservableObject.SetProperty{T}(T,T,Action{T},string?)"/> is called, otherwise no state change is performed.
- /// </summary>
- /// <typeparam name="T">The type of the property that changed.</typeparam>
- /// <param name="oldValue">The current property value.</param>
- /// <param name="newValue">The property's value after the change occurred.</param>
- /// <param name="callback">A callback to invoke to update the property value.</param>
- /// <param name="errors">The resulting validation errors, if any.</param>
- /// <param name="propertyName">(optional) The name of the property that changed.</param>
- /// <returns>Whether the validation was successful and the property value changed as well.</returns>
- /// <exception cref="System.ArgumentNullException">Thrown if <paramref name="callback"/> or <paramref name="propertyName"/> are <see langword="null"/>.</exception>
- [RequiresUnreferencedCode("The type of the current instance cannot be statically discovered.")]
- protected bool TrySetProperty<T>(T oldValue, T newValue, Action<T> callback, out IReadOnlyCollection<ValidationResult> errors, [CallerMemberName] string propertyName = null!)
- {
- ArgumentNullException.ThrowIfNull(callback);
- ArgumentNullException.ThrowIfNull(propertyName);
- return TryValidateProperty(newValue, propertyName, out errors) &&
- SetProperty(oldValue, newValue, callback, propertyName);
- }
- /// <summary>
- /// Tries to validate a new value for a specified property. If the validation is successful,
- /// <see cref="ObservableObject.SetProperty{T}(T,T,IEqualityComparer{T},Action{T},string?)"/> is called, otherwise no state change is performed.
- /// </summary>
- /// <typeparam name="T">The type of the property that changed.</typeparam>
- /// <param name="oldValue">The current property value.</param>
- /// <param name="newValue">The property's value after the change occurred.</param>
- /// <param name="comparer">The <see cref="IEqualityComparer{T}"/> instance to use to compare the input values.</param>
- /// <param name="callback">A callback to invoke to update the property value.</param>
- /// <param name="errors">The resulting validation errors, if any.</param>
- /// <param name="propertyName">(optional) The name of the property that changed.</param>
- /// <returns>Whether the validation was successful and the property value changed as well.</returns>
- /// <exception cref="System.ArgumentNullException">Thrown if <paramref name="comparer"/>, <paramref name="callback"/> or <paramref name="propertyName"/> are <see langword="null"/>.</exception>
- [RequiresUnreferencedCode("The type of the current instance cannot be statically discovered.")]
- protected bool TrySetProperty<T>(T oldValue, T newValue, IEqualityComparer<T> comparer, Action<T> callback, out IReadOnlyCollection<ValidationResult> errors, [CallerMemberName] string propertyName = null!)
- {
- ArgumentNullException.ThrowIfNull(comparer);
- ArgumentNullException.ThrowIfNull(callback);
- ArgumentNullException.ThrowIfNull(propertyName);
- return TryValidateProperty(newValue, propertyName, out errors) &&
- SetProperty(oldValue, newValue, comparer, callback, propertyName);
- }
- /// <summary>
- /// Tries to validate a new value for a specified property. If the validation is successful,
- /// <see cref="ObservableObject.SetProperty{TModel,T}(T,T,TModel,Action{TModel,T},string?)"/> is called, otherwise no state change is performed.
- /// </summary>
- /// <typeparam name="TModel">The type of model whose property (or field) to set.</typeparam>
- /// <typeparam name="T">The type of property (or field) to set.</typeparam>
- /// <param name="oldValue">The current property value.</param>
- /// <param name="newValue">The property's value after the change occurred.</param>
- /// <param name="model">The model </param>
- /// <param name="callback">The callback to invoke to set the target property value, if a change has occurred.</param>
- /// <param name="errors">The resulting validation errors, if any.</param>
- /// <param name="propertyName">(optional) The name of the property that changed.</param>
- /// <returns>Whether the validation was successful and the property value changed as well.</returns>
- /// <exception cref="System.ArgumentNullException">Thrown if <paramref name="model"/>, <paramref name="callback"/> or <paramref name="propertyName"/> are <see langword="null"/>.</exception>
- [RequiresUnreferencedCode("The type of the current instance cannot be statically discovered.")]
- protected bool TrySetProperty<TModel, T>(T oldValue, T newValue, TModel model, Action<TModel, T> callback, out IReadOnlyCollection<ValidationResult> errors, [CallerMemberName] string propertyName = null!)
- where TModel : class
- {
- ArgumentNullException.ThrowIfNull(model);
- ArgumentNullException.ThrowIfNull(callback);
- ArgumentNullException.ThrowIfNull(propertyName);
- return TryValidateProperty(newValue, propertyName, out errors) &&
- SetProperty(oldValue, newValue, model, callback, propertyName);
- }
- /// <summary>
- /// Tries to validate a new value for a specified property. If the validation is successful,
- /// <see cref="ObservableObject.SetProperty{TModel,T}(T,T,IEqualityComparer{T},TModel,Action{TModel,T},string?)"/> is called, otherwise no state change is performed.
- /// </summary>
- /// <typeparam name="TModel">The type of model whose property (or field) to set.</typeparam>
- /// <typeparam name="T">The type of property (or field) to set.</typeparam>
- /// <param name="oldValue">The current property value.</param>
- /// <param name="newValue">The property's value after the change occurred.</param>
- /// <param name="comparer">The <see cref="IEqualityComparer{T}"/> instance to use to compare the input values.</param>
- /// <param name="model">The model </param>
- /// <param name="callback">The callback to invoke to set the target property value, if a change has occurred.</param>
- /// <param name="errors">The resulting validation errors, if any.</param>
- /// <param name="propertyName">(optional) The name of the property that changed.</param>
- /// <returns>Whether the validation was successful and the property value changed as well.</returns>
- /// <exception cref="System.ArgumentNullException">Thrown if <paramref name="comparer"/>, <paramref name="model"/>, <paramref name="callback"/> or <paramref name="propertyName"/> are <see langword="null"/>.</exception>
- [RequiresUnreferencedCode("The type of the current instance cannot be statically discovered.")]
- protected bool TrySetProperty<TModel, T>(T oldValue, T newValue, IEqualityComparer<T> comparer, TModel model, Action<TModel, T> callback, out IReadOnlyCollection<ValidationResult> errors, [CallerMemberName] string propertyName = null!)
- where TModel : class
- {
- ArgumentNullException.ThrowIfNull(comparer);
- ArgumentNullException.ThrowIfNull(model);
- ArgumentNullException.ThrowIfNull(callback);
- ArgumentNullException.ThrowIfNull(propertyName);
- return TryValidateProperty(newValue, propertyName, out errors) &&
- SetProperty(oldValue, newValue, comparer, model, callback, propertyName);
- }
- /// <summary>
- /// Clears the validation errors for a specified property or for the entire entity.
- /// </summary>
- /// <param name="propertyName">
- /// The name of the property to clear validation errors for.
- /// If a <see langword="null"/> or empty name is used, all entity-level errors will be cleared.
- /// </param>
- protected void ClearErrors(string? propertyName = null)
- {
- // Clear entity-level errors when the target property is null or empty
- if (string.IsNullOrEmpty(propertyName))
- {
- ClearAllErrors();
- }
- else
- {
- ClearErrorsForProperty(propertyName!);
- }
- }
- /// <inheritdoc cref="INotifyDataErrorInfo.GetErrors(string)"/>
- public IEnumerable<ValidationResult> GetErrors(string? propertyName = null)
- {
- // Get entity-level errors when the target property is null or empty
- if (string.IsNullOrEmpty(propertyName))
- {
- // Local function to gather all the entity-level errors
- [MethodImpl(MethodImplOptions.NoInlining)]
- IEnumerable<ValidationResult> GetAllErrors()
- {
- return this.errors.Values.SelectMany(static errors => errors);
- }
- return GetAllErrors();
- }
- // Property-level errors, if any
- if (this.errors.TryGetValue(propertyName!, out List<ValidationResult>? errors))
- {
- return errors;
- }
- // The INotifyDataErrorInfo.GetErrors method doesn't specify exactly what to
- // return when the input property name is invalid, but given that the return
- // type is marked as a non-nullable reference type, here we're returning an
- // empty array to respect the contract. This also matches the behavior of
- // this method whenever errors for a valid properties are retrieved.
- return Array.Empty<ValidationResult>();
- }
- /// <inheritdoc/>
- IEnumerable INotifyDataErrorInfo.GetErrors(string? propertyName) => GetErrors(propertyName);
- /// <summary>
- /// Validates all the properties in the current instance and updates all the tracked errors.
- /// If any changes are detected, the <see cref="ErrorsChanged"/> event will be raised.
- /// </summary>
- /// <remarks>
- /// Only public instance properties (excluding custom indexers) that have at least one
- /// <see cref="ValidationAttribute"/> applied to them will be validated. All other
- /// members in the current instance will be ignored. None of the processed properties
- /// will be modified - they will only be used to retrieve their values and validate them.
- /// </remarks>
- [RequiresUnreferencedCode(
- "This method requires the generated CommunityToolkit.Mvvm.ComponentModel.__Internals.__ObservableValidatorExtensions type not to be removed to use the fast path. " +
- "If this type is removed by the linker, or if the target recipient was created dynamically and was missed by the source generator, a slower fallback " +
- "path using a compiled LINQ expression will be used. This will have more overhead in the first invocation of this method for any given recipient type. " +
- "Additionally, due to the usage of validation APIs, the type of the current instance cannot be statically discovered.")]
- protected void ValidateAllProperties()
- {
- // Fast path that tries to create a delegate from a generated type-specific method. This
- // is used to make this method more AOT-friendly and faster, as there is no dynamic code.
- [RequiresUnreferencedCode("The type of the current instance cannot be statically discovered.")]
- static Action<object> GetValidationAction(Type type)
- {
- if (type.Assembly.GetType("CommunityToolkit.Mvvm.ComponentModel.__Internals.__ObservableValidatorExtensions") is Type extensionsType &&
- extensionsType.GetMethod("CreateAllPropertiesValidator", new[] { type }) is MethodInfo methodInfo)
- {
- return (Action<object>)methodInfo.Invoke(null, new object?[] { null })!;
- }
- return GetValidationActionFallback(type);
- }
- // Fallback method to create the delegate with a compiled LINQ expression
- static Action<object> GetValidationActionFallback(Type type)
- {
- // Get the collection of all properties to validate
- (string Name, MethodInfo GetMethod)[] validatableProperties = (
- from property in type.GetProperties(BindingFlags.Instance | BindingFlags.Public)
- where property.GetIndexParameters().Length == 0 &&
- property.GetCustomAttributes<ValidationAttribute>(true).Any()
- let getMethod = property.GetMethod
- where getMethod is not null
- select (property.Name, getMethod)).ToArray();
- // Short path if there are no properties to validate
- if (validatableProperties.Length == 0)
- {
- return static _ => { };
- }
- // MyViewModel inst0 = (MyViewModel)arg0;
- ParameterExpression arg0 = Expression.Parameter(typeof(object));
- UnaryExpression inst0 = Expression.Convert(arg0, type);
- // Get a reference to ValidateProperty(object, string)
- MethodInfo validateMethod = typeof(ObservableValidator).GetMethod(nameof(ValidateProperty), BindingFlags.Instance | BindingFlags.NonPublic)!;
- // We want a single compiled LINQ expression that validates all properties in the
- // actual type of the executing viewmodel at once. We do this by creating a block
- // expression with the unrolled invocations of all properties to validate.
- // Essentially, the body will contain the following code:
- // ===============================================================================
- // {
- // inst0.ValidateProperty(inst0.Property0, nameof(MyViewModel.Property0));
- // inst0.ValidateProperty(inst0.Property1, nameof(MyViewModel.Property1));
- // ...
- // inst0.ValidateProperty(inst0.PropertyN, nameof(MyViewModel.PropertyN));
- // }
- // ===============================================================================
- // We also add an explicit object conversion to represent boxing, if a given property
- // is a value type. It will just be a no-op if the value is a reference type already.
- // Note that this generated code is technically accessing a protected method from
- // ObservableValidator externally, but that is fine because IL doesn't really have
- // a concept of member visibility, that's purely a C# build-time feature.
- BlockExpression body = Expression.Block(
- from property in validatableProperties
- select Expression.Call(inst0, validateMethod, new Expression[]
- {
- Expression.Convert(Expression.Call(inst0, property.GetMethod), typeof(object)),
- Expression.Constant(property.Name)
- }));
- return Expression.Lambda<Action<object>>(body, arg0).Compile();
- }
- // Get or compute the cached list of properties to validate. Here we're using a static lambda to ensure the
- // delegate is cached by the C# compiler, see the related issue at https://github.com/dotnet/roslyn/issues/5835.
- EntityValidatorMap.GetValue(
- GetType(),
- [RequiresUnreferencedCode("The type of the current instance cannot be statically discovered.")] static (t) => GetValidationAction(t))(this);
- }
- /// <summary>
- /// Validates a property with a specified name and a given input value.
- /// If any changes are detected, the <see cref="ErrorsChanged"/> event will be raised.
- /// </summary>
- /// <param name="value">The value to test for the specified property.</param>
- /// <param name="propertyName">The name of the property to validate.</param>
- /// <exception cref="ArgumentNullException">Thrown when <paramref name="propertyName"/> is <see langword="null"/>.</exception>
- [RequiresUnreferencedCode("The type of the current instance cannot be statically discovered.")]
- protected internal void ValidateProperty(object? value, [CallerMemberName] string propertyName = null!)
- {
- ArgumentNullException.ThrowIfNull(propertyName);
- // Check if the property had already been previously validated, and if so retrieve
- // the reusable list of validation errors from the errors dictionary. This list is
- // used to add new validation errors below, if any are produced by the validator.
- // If the property isn't present in the dictionary, add it now to avoid allocations.
- if (!this.errors.TryGetValue(propertyName, out List<ValidationResult>? propertyErrors))
- {
- propertyErrors = new List<ValidationResult>();
- this.errors.Add(propertyName, propertyErrors);
- }
- bool errorsChanged = false;
- // Clear the errors for the specified property, if any
- if (propertyErrors.Count > 0)
- {
- propertyErrors.Clear();
- errorsChanged = true;
- }
- // Validate the property, by adding new errors to the existing list
- this.validationContext.MemberName = propertyName;
- this.validationContext.DisplayName = GetDisplayNameForProperty(propertyName);
- bool isValid = Validator.TryValidateProperty(value, this.validationContext, propertyErrors);
- // Update the shared counter for the number of errors, and raise the
- // property changed event if necessary. We decrement the number of total
- // errors if the current property is valid but it wasn't so before this
- // validation, and we increment it if the validation failed after being
- // correct before. The property changed event is raised whenever the
- // number of total errors is either decremented to 0, or incremented to 1.
- if (isValid)
- {
- if (errorsChanged)
- {
- this.totalErrors--;
- if (this.totalErrors == 0)
- {
- OnPropertyChanged(HasErrorsChangedEventArgs);
- }
- }
- }
- else if (!errorsChanged)
- {
- this.totalErrors++;
- if (this.totalErrors == 1)
- {
- OnPropertyChanged(HasErrorsChangedEventArgs);
- }
- }
- // Only raise the event once if needed. This happens either when the target property
- // had existing errors and is now valid, or if the validation has failed and there are
- // new errors to broadcast, regardless of the previous validation state for the property.
- if (errorsChanged || !isValid)
- {
- ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(propertyName));
- }
- }
- /// <summary>
- /// Tries to validate a property with a specified name and a given input value, and returns
- /// the computed errors, if any. If the property is valid, it is assumed that its value is
- /// about to be set in the current object. Otherwise, no observable local state is modified.
- /// </summary>
- /// <param name="value">The value to test for the specified property.</param>
- /// <param name="propertyName">The name of the property to validate.</param>
- /// <param name="errors">The resulting validation errors, if any.</param>
- [RequiresUnreferencedCode("The type of the current instance cannot be statically discovered.")]
- private bool TryValidateProperty(object? value, string propertyName, out IReadOnlyCollection<ValidationResult> errors)
- {
- // Add the cached errors list for later use.
- if (!this.errors.TryGetValue(propertyName!, out List<ValidationResult>? propertyErrors))
- {
- propertyErrors = new List<ValidationResult>();
- this.errors.Add(propertyName!, propertyErrors);
- }
- bool hasErrors = propertyErrors.Count > 0;
- List<ValidationResult> localErrors = new();
- // Validate the property, by adding new errors to the local list
- this.validationContext.MemberName = propertyName;
- this.validationContext.DisplayName = GetDisplayNameForProperty(propertyName!);
- bool isValid = Validator.TryValidateProperty(value, this.validationContext, localErrors);
- // We only modify the state if the property is valid and it wasn't so before. In this case, we
- // clear the cached list of errors (which is visible to consumers) and raise the necessary events.
- if (isValid && hasErrors)
- {
- propertyErrors.Clear();
- this.totalErrors--;
- if (this.totalErrors == 0)
- {
- OnPropertyChanged(HasErrorsChangedEventArgs);
- }
- ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(propertyName));
- }
- errors = localErrors;
- return isValid;
- }
- /// <summary>
- /// Clears all the current errors for the entire entity.
- /// </summary>
- private void ClearAllErrors()
- {
- if (this.totalErrors == 0)
- {
- return;
- }
- // Clear the errors for all properties with at least one error, and raise the
- // ErrorsChanged event for those properties. Other properties will be ignored.
- foreach (KeyValuePair<string, List<ValidationResult>> propertyInfo in this.errors)
- {
- bool hasErrors = propertyInfo.Value.Count > 0;
- propertyInfo.Value.Clear();
- if (hasErrors)
- {
- ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(propertyInfo.Key));
- }
- }
- this.totalErrors = 0;
- OnPropertyChanged(HasErrorsChangedEventArgs);
- }
- /// <summary>
- /// Clears all the current errors for a target property.
- /// </summary>
- /// <param name="propertyName">The name of the property to clear errors for.</param>
- private void ClearErrorsForProperty(string propertyName)
- {
- if (!this.errors.TryGetValue(propertyName!, out List<ValidationResult>? propertyErrors) ||
- propertyErrors.Count == 0)
- {
- return;
- }
- propertyErrors.Clear();
- this.totalErrors--;
- if (this.totalErrors == 0)
- {
- OnPropertyChanged(HasErrorsChangedEventArgs);
- }
- ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(propertyName));
- }
- /// <summary>
- /// Gets the display name for a given property. It could be a custom name or just the property name.
- /// </summary>
- /// <param name="propertyName">The target property name being validated.</param>
- /// <returns>The display name for the property.</returns>
- [RequiresUnreferencedCode("The type of the current instance cannot be statically discovered.")]
- private string GetDisplayNameForProperty(string propertyName)
- {
- static Dictionary<string, string> GetDisplayNames(Type type)
- {
- Dictionary<string, string> displayNames = new();
- foreach (PropertyInfo property in type.GetProperties(BindingFlags.Instance | BindingFlags.Public))
- {
- if (property.GetCustomAttribute<DisplayAttribute>() is DisplayAttribute attribute &&
- attribute.GetName() is string displayName)
- {
- displayNames.Add(property.Name, displayName);
- }
- }
- return displayNames;
- }
- // This method replicates the logic of DisplayName and GetDisplayName from the
- // ValidationContext class. See the original source in the BCL for more details.
- _ = DisplayNamesMap.GetValue(GetType(), static t => GetDisplayNames(t)).TryGetValue(propertyName, out string? displayName);
- return displayName ?? propertyName;
- }
- }
|