mirror of
https://github.com/chylex/.NET-Community-Toolkit.git
synced 2024-10-17 06:42:48 +02:00
832 lines
46 KiB
C#
832 lines
46 KiB
C#
// 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/>
|
|
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("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("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;
|
|
}
|
|
}
|