@@ -26,6 +26,18 @@ public abstract class ObservableValidator : ObservableObject, INotifyDataErrorIn
2626 /// </summary>
2727 private static readonly ConditionalWeakTable < Type , Action < object > > EntityValidatorMap = new ( ) ;
2828
29+ /// <summary>
30+ /// The <see cref="ConditionalWeakTable{TKey, TValue}"/> instance used to track display names for properties to validate.
31+ /// </summary>
32+ /// <remarks>
33+ /// This is necessary because we want to reuse the same <see cref="ValidationContext"/> instance for all validations, but
34+ /// with the same behavior with repsect to formatted names that new instances would have provided. The issue is that the
35+ /// <see cref="ValidationContext.DisplayName"/> property is not refreshed when we set <see cref="ValidationContext.MemberName"/>,
36+ /// so we need to replicate the same logic to retrieve the right display name for properties to validate and update that
37+ /// property manually right before passing the context to <see cref="Validator"/> and proceed with the normal functionality.
38+ /// </remarks>
39+ private static readonly ConditionalWeakTable < Type , Dictionary < string , string > > DisplayNamesMap = new ( ) ;
40+
2941 /// <summary>
3042 /// The cached <see cref="PropertyChangedEventArgs"/> for <see cref="HasErrors"/>.
3143 /// </summary>
@@ -541,6 +553,7 @@ protected void ValidateProperty(object? value, [CallerMemberName] string? proper
541553
542554 // Validate the property, by adding new errors to the existing list
543555 this . validationContext . MemberName = propertyName ;
556+ this . validationContext . DisplayName = GetDisplayNameForProperty ( propertyName ! ) ;
544557
545558 bool isValid = Validator . TryValidateProperty ( value , this . validationContext , propertyErrors ) ;
546559
@@ -611,6 +624,7 @@ private bool TryValidateProperty(object? value, string? propertyName, out IReadO
611624
612625 // Validate the property, by adding new errors to the local list
613626 this . validationContext . MemberName = propertyName ;
627+ this . validationContext . DisplayName = GetDisplayNameForProperty ( propertyName ! ) ;
614628
615629 bool isValid = Validator . TryValidateProperty ( value , this . validationContext , localErrors ) ;
616630
@@ -688,6 +702,36 @@ private void ClearErrorsForProperty(string propertyName)
688702 ErrorsChanged ? . Invoke ( this , new DataErrorsChangedEventArgs ( propertyName ) ) ;
689703 }
690704
705+ /// <summary>
706+ /// Gets the display name for a given property. It could be a custom name or just the property name.
707+ /// </summary>
708+ /// <param name="propertyName">The target property name being validated.</param>
709+ /// <returns>The display name for the property.</returns>
710+ private string GetDisplayNameForProperty ( string propertyName )
711+ {
712+ static Dictionary < string , string > GetDisplayNames ( Type type )
713+ {
714+ Dictionary < string , string > displayNames = new ( ) ;
715+
716+ foreach ( PropertyInfo property in type . GetProperties ( BindingFlags . Instance | BindingFlags . Public ) )
717+ {
718+ if ( property . GetCustomAttribute < DisplayAttribute > ( ) is DisplayAttribute attribute &&
719+ attribute . GetName ( ) is string displayName )
720+ {
721+ displayNames . Add ( property . Name , displayName ) ;
722+ }
723+ }
724+
725+ return displayNames ;
726+ }
727+
728+ // This method replicates the logic of DisplayName and GetDisplayName from the
729+ // ValidationContext class. See the original source in the BCL for more details.
730+ DisplayNamesMap . GetValue ( GetType ( ) , static t => GetDisplayNames ( t ) ) . TryGetValue ( propertyName , out string ? displayName ) ;
731+
732+ return displayName ?? propertyName ;
733+ }
734+
691735#pragma warning disable SA1204
692736 /// <summary>
693737 /// Throws an <see cref="ArgumentNullException"/> when a property name given as input is <see langword="null"/>.
0 commit comments