@@ -26,6 +26,18 @@ public abstract class ObservableValidator : ObservableObject, INotifyDataErrorIn
26
26
/// </summary>
27
27
private static readonly ConditionalWeakTable < Type , Action < object > > EntityValidatorMap = new ( ) ;
28
28
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
+
29
41
/// <summary>
30
42
/// The cached <see cref="PropertyChangedEventArgs"/> for <see cref="HasErrors"/>.
31
43
/// </summary>
@@ -541,6 +553,7 @@ protected void ValidateProperty(object? value, [CallerMemberName] string? proper
541
553
542
554
// Validate the property, by adding new errors to the existing list
543
555
this . validationContext . MemberName = propertyName ;
556
+ this . validationContext . DisplayName = GetDisplayNameForProperty ( propertyName ! ) ;
544
557
545
558
bool isValid = Validator . TryValidateProperty ( value , this . validationContext , propertyErrors ) ;
546
559
@@ -611,6 +624,7 @@ private bool TryValidateProperty(object? value, string? propertyName, out IReadO
611
624
612
625
// Validate the property, by adding new errors to the local list
613
626
this . validationContext . MemberName = propertyName ;
627
+ this . validationContext . DisplayName = GetDisplayNameForProperty ( propertyName ! ) ;
614
628
615
629
bool isValid = Validator . TryValidateProperty ( value , this . validationContext , localErrors ) ;
616
630
@@ -688,6 +702,36 @@ private void ClearErrorsForProperty(string propertyName)
688
702
ErrorsChanged ? . Invoke ( this , new DataErrorsChangedEventArgs ( propertyName ) ) ;
689
703
}
690
704
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
+
691
735
#pragma warning disable SA1204
692
736
/// <summary>
693
737
/// Throws an <see cref="ArgumentNullException"/> when a property name given as input is <see langword="null"/>.
0 commit comments