Skip to content

Commit f432b84

Browse files
committed
Fixed display names loading for validated properties
1 parent 2f38939 commit f432b84

File tree

1 file changed

+44
-0
lines changed

1 file changed

+44
-0
lines changed

Microsoft.Toolkit.Mvvm/ComponentModel/ObservableValidator.cs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)