fix(module: form): validation attributes transfer (#4084)

* fix(module: form): validation attribute transfer

* add missing required parameter to rules

* fix enum validate messages

* fix test

* fix field type

* fix test

* fix test

* fix test
This commit is contained in:
James Yeung 2024-08-12 10:02:56 +08:00 коммит произвёл GitHub
Родитель 5d153a6e05
Коммит 04b4b47b94
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
14 изменённых файлов: 128 добавлений и 99 удалений

Просмотреть файл

@ -38,6 +38,8 @@ namespace AntDesign
internal PropertyReflector? PopertyReflector => _propertyReflector;
internal Type ValueUnderlyingType => _nullableUnderlyingType ?? typeof(TValue);
[CascadingParameter(Name = "FormItem")]
protected IFormItem FormItem { get; set; }
@ -370,8 +372,6 @@ namespace AntDesign
}
}
FieldIdentifier IControlValueAccessor.FieldIdentifier => FieldIdentifier;
/// <inheritdoc />
public override Task SetParametersAsync(ParameterView parameters)
{
@ -403,10 +403,10 @@ namespace AntDesign
}
EditContext = Form?.EditContext;
_nullableUnderlyingType = Nullable.GetUnderlyingType(typeof(TValue));
}
_nullableUnderlyingType = Nullable.GetUnderlyingType(typeof(TValue));
EditContext.OnValidationStateChanged += _validationStateChangedHandler;
if (ValueExpression != null)
@ -484,10 +484,5 @@ namespace AntDesign
InvokeAsync(StateHasChanged);
}
}
void IControlValueAccessor.OnValidated(string[] validationMessages)
{
OnValidated(validationMessages);
}
}
}

Просмотреть файл

@ -21,7 +21,6 @@
String = _typeTemplate,
Array = _typeTemplate,
Object = _typeTemplate,
Enum = _typeTemplate,
Number = _typeTemplate,
Date = _typeTemplate,
Boolean = _typeTemplate,
@ -32,7 +31,7 @@
Url = _typeTemplate,
};
public StringMessage String { get; set; } = new()
public CompareMessage String { get; set; } = new()
{
Len = "'{0}' must be exactly {1} characters",
Min = "'{0}' must be at least {1} characters",
@ -40,7 +39,7 @@
Range = "'{0}' must be between {1} and {2} characters",
};
public NumberMessage Number { get; set; } = new()
public CompareMessage Number { get; set; } = new()
{
Len = "'{0}' must equal {1}",
Min = "'{0}' cannot be less than {1}",
@ -48,7 +47,7 @@
Range = "'{0}' must be between {1} and {2}",
};
public ArrayMessage Array { get; set; } = new()
public CompareMessage Array { get; set; } = new()
{
Len = "'{0}' must be exactly {1} in length",
Min = "'{0}' cannot be less than {1} in length",
@ -73,7 +72,6 @@
FormFieldType.Float => Types.Float,
FormFieldType.Array => Types.Array,
FormFieldType.Object => Types.Object,
FormFieldType.Enum => Types.Enum,
FormFieldType.Date => Types.Date,
FormFieldType.Url => Types.Url,
FormFieldType.Email => Types.Email,
@ -87,7 +85,6 @@
public string String { get; set; }
public string Array { get; set; }
public string Object { get; set; }
public string Enum { get; set; }
public string Number { get; set; }
public string Date { get; set; }
public string Boolean { get; set; }
@ -98,7 +95,7 @@
public string Url { get; set; }
}
public class StringMessage
public class CompareMessage
{
public string Len { get; set; }
public string Min { get; set; }
@ -106,24 +103,6 @@
public string Range { get; set; }
}
public class NumberMessage
{
public string Len { get; set; }
public string Min { get; set; }
public string Max { get; set; }
public string Range { get; set; }
}
public class ArrayMessage
{
public string Len { get; set; }
public string Min { get; set; }
public string Max { get; set; }
public string Range { get; set; }
}
public class PatternMessage
{
public string Mismatch { get; set; }

Просмотреть файл

@ -7,8 +7,6 @@ namespace AntDesign.Forms
{
public interface IControlValueAccessor
{
internal FieldIdentifier FieldIdentifier { get; }
void OnValidated(string[] validationMessages);
internal void Reset();
}
}

Просмотреть файл

@ -30,7 +30,7 @@ namespace AntDesign
_enumType = THelper.GetUnderlyingType<T>();
_aggregateFunction = BuildAggregateFunction();
_valueList = Enum.GetValues(_enumType).Cast<T>();
_valueLabelList = _valueList.Select(value => (value, GetDisplayName(value)));
_valueLabelList = _valueList.Select(value => (value, EnumHelper.GetDisplayName(_enumType, value)));
_isFlags = _enumType.GetCustomAttribute<FlagsAttribute>() != null;
_hasFlagFunction = BuildHasFlagFunction();
}
@ -68,11 +68,9 @@ namespace AntDesign
return _valueLabelList;
}
public static string GetDisplayName(T enumValue)
public static string GetDisplayName<TEnum>(TEnum item)
{
var enumName = Enum.GetName(_enumType, enumValue);
var fieldInfo = _enumType.GetField(enumName);
return fieldInfo.GetCustomAttribute<DisplayAttribute>(true)?.GetName() ?? enumName;
return EnumHelper.GetDisplayName(_enumType, item);
}
private static Func<T, T, T> BuildAggregateFunction()
@ -111,4 +109,14 @@ namespace AntDesign
}
}
}
internal static class EnumHelper
{
public static string GetDisplayName(Type enumType, object enumValue)
{
var enumName = Enum.GetName(enumType, enumValue);
var fieldInfo = enumType.GetField(enumName);
return fieldInfo.GetCustomAttribute<DisplayAttribute>(true)?.GetName() ?? enumName;
}
}
}

Просмотреть файл

@ -8,7 +8,7 @@ using System.Reflection;
namespace AntDesign
{
public static class THelper
internal static class THelper
{
public static T ChangeType<T>(object value)
{
@ -98,9 +98,22 @@ namespace AntDesign
or TypeCode.UInt32
or TypeCode.UInt64;
}
public static bool IsDateType(this Type type)
{
return type != null && (type == typeof(DateTime)
|| type == typeof(DateTimeOffset)
#if NET6_0_OR_GREATER
|| type == typeof(TimeOnly)
|| type == typeof(DateOnly)
#endif
);
}
public static bool IsArrayOrList(this Type that) => that != null && (that.IsArray || typeof(IList).IsAssignableFrom(that));
public static bool IsEnumerable(this Type that) => that != null && (that.IsArray || typeof(IEnumerable).IsAssignableFrom(that));
public static bool IsUserDefinedClass(this Type thta) =>
thta.IsClass && thta.Namespace != null && !thta.Namespace.StartsWith("System");
}

Просмотреть файл

@ -393,7 +393,7 @@ namespace AntDesign
bool UseRulesValidator => UseLocaleValidateMessage || ValidateMode != FormValidateMode.Default;
public void BuildEditContext()
private void BuildEditContext()
{
if (_editContext == null)
return;

Просмотреть файл

@ -186,6 +186,11 @@ namespace AntDesign
private string DisplayName => Label ?? _propertyReflector?.DisplayName;
private string _name;
private Action _nameChanged;
private Type _valueUnderlyingType;
private FieldIdentifier _fieldIdentifier;
private Func<object, object> _fieldValueGetter;
@ -195,10 +200,8 @@ namespace AntDesign
private FormValidateStatus? _originalValidateStatus;
private Action _vaildateStatusChanged;
private Action _nameChanged;
private Action<string[]> _onValidated;
private string _name;
private IEnumerable<FormValidationRule> _rules;
RenderFragment IFormItem.FeedbackIcon => IsShowIcon ? builder =>
@ -270,6 +273,11 @@ namespace AntDesign
FormValidateMode.Rules => Rules ?? [],
_ => [.. GetRulesFromAttributes(), .. Rules ?? []]
};
if (Required && !_rules.Any(rule => rule.Required == true || rule.ValidationAttribute is RequiredAttribute))
{
_rules = [.. _rules, new FormValidationRule { Required = true }];
}
}
protected override void OnParametersSet()
@ -404,6 +412,7 @@ namespace AntDesign
_vaildateStatusChanged = control.UpdateStyles;
_nameChanged = control.OnNameChanged;
_onValidated = control.OnValidated;
_valueUnderlyingType = control.ValueUnderlyingType;
if (control.FieldIdentifier.Model == null)
{
@ -459,6 +468,11 @@ namespace AntDesign
return [];
}
if (IsRequired)
{
_rules ??= [];
}
if (_rules?.Any() != true)
{
return [];
@ -480,6 +494,7 @@ namespace AntDesign
Value = propertyValue,
FieldName = _fieldIdentifier.FieldName,
DisplayName = DisplayName,
FieldType = _valueUnderlyingType,
ValidateMessages = validateMessages,
};
@ -524,8 +539,7 @@ namespace AntDesign
foreach (var attribute in attributes)
{
yield return new FormValidationRule { ValidationAttribute = attribute };
yield return new FormValidationRule { ValidationAttribute = attribute, Enum = _valueUnderlyingType.IsEnum ? _valueUnderlyingType : null };
}
}
}

Просмотреть файл

@ -1,6 +1,6 @@
using System;
using System.ComponentModel.DataAnnotations;
using System.Numerics;
using System.Linq;
namespace AntDesign.Internal.Form.Validate
{
@ -43,25 +43,29 @@ namespace AntDesign.Internal.Form.Validate
var templates = validationContext.ValidateMessages;
var compareMessage = validationContext.FieldType switch
{
Type t when t == typeof(string) => templates.String,
Type t when THelper.IsNumericType(t) => templates.Number,
Type t when THelper.IsEnumerable(t) => templates.Array,
_ => templates.String
};
attribute.ErrorMessage = attribute switch
{
// if user has set the ErrorMessage, we will use it directly
{ ErrorMessage.Length: > 0 } => attribute.ErrorMessage,
RequiredAttribute => ReplaceLabel(templates.Required),
RangeAttribute => ReplaceLength(validationContext.Value is string ? templates.String.Range : templates.Number.Range, max: 2),
MinLengthAttribute => ReplaceLength(validationContext.Value is string ? templates.String.Min : templates.Number.Min),
MaxLengthAttribute => ReplaceLength(validationContext.Value is string ? templates.String.Max : templates.Number.Max),
RangeAttribute => ReplaceLength(compareMessage.Range, max: 2),
MinLengthAttribute => ReplaceLength(compareMessage.Min),
MaxLengthAttribute => ReplaceLength(compareMessage.Max),
StringLengthAttribute => ReplaceLength(templates.String.Range, max: 2),
_ => attribute.ErrorMessage,
};
if (attribute is RangeAttribute or MinLengthAttribute or MaxLengthAttribute)
{
validationContext.Value ??= 0;
}
return IsValid(validationContext.Rule.ValidationAttribute, validationContext, out result);
}
private static bool RequiredIsValid(FormValidationContext validationContext, out ValidationResult result)
{
if (validationContext.Rule.Required == true)
@ -86,21 +90,22 @@ namespace AntDesign.Internal.Form.Validate
{
result = null;
var rule = validationContext.Rule;
var fieldType = validationContext.FieldType;
if (rule.Len != null)
{
ValidationAttribute attribute = null;
if (rule.Type == FormFieldType.String)
if (fieldType == typeof(string))
{
attribute = new StringLengthAttribute((int)rule.Len);
attribute.ErrorMessage = validationContext.ValidateMessages.String.Len;
}
if (rule.Type.IsIn(FormFieldType.Number, FormFieldType.Integer, FormFieldType.Float))
else if (THelper.IsNumericType(fieldType))
{
attribute = new NumberAttribute((decimal)rule.Len);
attribute.ErrorMessage = validationContext.ValidateMessages.Number.Len;
}
if (rule.Type == FormFieldType.Array)
else if (THelper.IsEnumerable(fieldType))
{
attribute = new ArrayLengthAttribute((int)rule.Len);
attribute.ErrorMessage = validationContext.ValidateMessages.Array.Len;
@ -127,24 +132,22 @@ namespace AntDesign.Internal.Form.Validate
{
result = null;
var rule = validationContext.Rule;
var fieldType = validationContext.FieldType;
if (rule.Min != null)
{
ValidationAttribute attribute = null;
if (rule.Type.IsIn(FormFieldType.String))
if (fieldType == typeof(string))
{
attribute = new MinLengthAttribute((int)rule.Min);
attribute.ErrorMessage = validationContext.ValidateMessages.String.Min;
}
if (rule.Type.IsIn(FormFieldType.Array))
else if (THelper.IsEnumerable(fieldType))
{
attribute = new MinLengthAttribute((int)rule.Min);
attribute.ErrorMessage = validationContext.ValidateMessages.Array.Min;
}
if (rule.Type.IsIn(FormFieldType.Number, FormFieldType.Integer, FormFieldType.Float))
else if (THelper.IsNumericType(fieldType))
{
attribute = new NumberMinAttribute((decimal)rule.Min);
attribute.ErrorMessage = validationContext.ValidateMessages.Number.Min;
@ -172,24 +175,23 @@ namespace AntDesign.Internal.Form.Validate
{
result = null;
var rule = validationContext.Rule;
var fieldType = validationContext.FieldType;
if (rule.Max != null)
{
ValidationAttribute attribute = null;
if (rule.Type.IsIn(FormFieldType.String))
if (fieldType == typeof(string))
{
attribute = new MaxLengthAttribute((int)rule.Max);
attribute.ErrorMessage = validationContext.ValidateMessages.String.Max;
}
if (rule.Type.IsIn(FormFieldType.Array))
else if (THelper.IsEnumerable(fieldType))
{
attribute = new MaxLengthAttribute((int)rule.Max);
attribute.ErrorMessage = validationContext.ValidateMessages.Array.Max;
}
if (rule.Type.IsIn(FormFieldType.Number, FormFieldType.Integer, FormFieldType.Float))
else if (THelper.IsNumericType(fieldType))
{
attribute = new NumberMaxAttribute((decimal)rule.Max);
attribute.ErrorMessage = validationContext.ValidateMessages.Number.Max;
@ -217,24 +219,23 @@ namespace AntDesign.Internal.Form.Validate
{
result = null;
var rule = validationContext.Rule;
var fieldType = validationContext.FieldType;
if (rule.Range != null)
{
ValidationAttribute attribute = null;
if (rule.Type.IsIn(FormFieldType.String))
if (fieldType == typeof(string))
{
attribute = new StringRangeAttribute((int)rule.Range.Value.Min, (int)rule.Range.Value.Max);
attribute.ErrorMessage = validationContext.ValidateMessages.String.Range;
}
if (rule.Type.IsIn(FormFieldType.Array))
else if (THelper.IsEnumerable(fieldType))
{
attribute = new ArrayRangeAttribute((int)rule.Range.Value.Min, (int)rule.Range.Value.Max);
attribute.ErrorMessage = validationContext.ValidateMessages.Array.Range;
}
if (rule.Type.IsIn(FormFieldType.Number, FormFieldType.Integer, FormFieldType.Float))
else if (THelper.IsNumericType(fieldType))
{
attribute = new RangeAttribute(rule.Range.Value.Min, rule.Range.Value.Max);
attribute.ErrorMessage = validationContext.ValidateMessages.Number.Range;
@ -300,11 +301,11 @@ namespace AntDesign.Internal.Form.Validate
private static bool TypeIsValid(FormValidationContext validationContext, out ValidationResult result)
{
result = null;
var rule = validationContext.Rule;
if (rule.Type == null)
{
result = null;
return true;
}
@ -318,14 +319,12 @@ namespace AntDesign.Internal.Form.Validate
return false;
}
result = null;
return true;
}
private static bool DefaultFieldIsValid(FormValidationContext validationContext, out ValidationResult result)
{
if (validationContext.Rule.Type == FormFieldType.Array && validationContext.Rule.DefaultField != null)
if (THelper.IsEnumerable(validationContext.FieldType) && validationContext.Rule.DefaultField != null)
{
Array values = validationContext.Value as Array;
@ -334,6 +333,7 @@ namespace AntDesign.Internal.Form.Validate
ValidateMessages = validationContext.ValidateMessages,
Rule = validationContext.Rule.DefaultField,
FieldName = validationContext.FieldName,
FieldType = validationContext.FieldType,
};
int index = 0;
@ -373,6 +373,7 @@ namespace AntDesign.Internal.Form.Validate
ValidateMessages = validationContext.ValidateMessages,
DisplayName = validationContext.DisplayName,
FieldName = validationContext.FieldName,
FieldType = validationContext.FieldType,
};
Array arrValues = validationContext.Value as Array;
@ -392,6 +393,7 @@ namespace AntDesign.Internal.Form.Validate
context.Value = arrValues.GetValue(index);
context.DisplayName = $"{validationContext.DisplayName}[{index}]";
context.FieldType = context.Value.GetType();
}
else
{
@ -399,6 +401,7 @@ namespace AntDesign.Internal.Form.Validate
if (propertyValue != null)
{
context.Value = propertyValue.GetValue(validationContext.Value);
context.FieldType = propertyValue.GetType();
}
else
{
@ -410,6 +413,7 @@ namespace AntDesign.Internal.Form.Validate
continue;
}
context.FieldType = fieldValue.GetType();
context.Value = fieldValue.GetValue(validationContext.Value);
}
@ -435,19 +439,25 @@ namespace AntDesign.Internal.Form.Validate
var rule = validationContext.Rule;
object[] values = [];
string[] labels = null;
if (rule.Type != FormFieldType.Array && rule.OneOf != null)
if (rule.Type != FormFieldType.Array)
{
values = rule.OneOf;
}
else if (rule.Enum != null && rule.Enum.IsEnum)
{
var enumValues = rule.Enum.GetEnumValues();
if (rule.OneOf != null)
{
values = rule.OneOf;
}
else if (rule.Enum != null)
{
var enumValues = Enum.GetValues(rule.Enum);
values = enumValues.Cast<object>().ToArray();
labels = values.Select(v => EnumHelper.GetDisplayName(rule.Enum, v)).ToArray();
}
}
if (values.Length > 0)
{
var attribute = new OneOfAttribute(values);
var attribute = new OneOfAttribute(values, labels);
attribute.ErrorMessage = ReplaceEnum(validationContext.ValidateMessages.Enum);
if (!IsValid(attribute, validationContext, out ValidationResult validationResult))

Просмотреть файл

@ -9,18 +9,27 @@ namespace AntDesign.Internal.Form.Validate
{
internal object[] Values { get; set; }
internal OneOfAttribute(object[] values)
internal string EnumOptions { get; set; }
internal OneOfAttribute(object[] values, string[] enumOptions = null)
{
Values = values;
EnumOptions = enumOptions != null ? string.Join(",", enumOptions) : null;
}
public override string FormatErrorMessage(string name)
{
return string.Format(CultureInfo.CurrentCulture, ErrorMessageString, name, JsonSerializer.Serialize(Values));
var options = EnumOptions ?? JsonSerializer.Serialize(Values).Trim('[', ']');
return string.Format(CultureInfo.CurrentCulture, ErrorMessageString, name, options);
}
public override bool IsValid(object value)
{
if (value == null)
{
return true;
}
if (value is Array)
{
return false;

Просмотреть файл

@ -35,7 +35,6 @@ namespace AntDesign.Internal.Form.Validate
FormFieldType.Regexp => IsRegexp(value),
FormFieldType.Array => value is Array,
FormFieldType.Object => value is object,
FormFieldType.Enum => value is Enum,
FormFieldType.Date => value is DateTime,
FormFieldType.Url => new UrlAttribute().IsValid(value),
FormFieldType.Email => new EmailAddressAttribute().IsValid(value),

Просмотреть файл

@ -10,7 +10,6 @@
Float,
Array,
Object,
Enum,
Date,
Url,
Email,

Просмотреть файл

@ -1,4 +1,6 @@
namespace AntDesign
using System;
namespace AntDesign
{
public class FormValidationContext
{
@ -7,6 +9,6 @@
public object Value { get; set; }
public string FieldName { get; set; }
public string DisplayName { get; set; }
public Type FieldType { get; set; }
}
}

Просмотреть файл

@ -270,9 +270,6 @@ namespace AntDesign.Tests.Form.Validation
new object[] { FormFieldType.Float, 100, false },
new object[] { FormFieldType.Float, "100", false },
new object[] { FormFieldType.Enum, FormValidateHelperEnum.None, true },
new object[] { FormFieldType.Enum, 100, false },
new object[] { FormFieldType.Regexp, "\\d", true },
new object[] { FormFieldType.Regexp, "^????---))%%%$$3#@^^", false },
@ -406,11 +403,10 @@ namespace AntDesign.Tests.Form.Validation
new object[] {
new FormValidationRule { Type= FormFieldType.Integer, OneOf = new object[] { 1, 2 } },
0,
string.Format($"{_defValidateMsgs.Enum}{_customSuffix}", _displayName, JsonSerializer.Serialize(new object[] { 1, 2 }))
string.Format($"{_defValidateMsgs.Enum}{_customSuffix}", _displayName, JsonSerializer.Serialize(new object[] { 1, 2 }).Trim('[', ']'))
},
new object[] { new FormValidationRule { Type = FormFieldType.String }, 123, string.Format($"{_defValidateMsgs.Types.String }{_customSuffix}", _displayName, FormFieldType.String) },
new object[] { new FormValidationRule { Type = FormFieldType.Array }, 123, string.Format($"{_defValidateMsgs.Types.Array }{_customSuffix}", _displayName, FormFieldType.Array) },
new object[] { new FormValidationRule { Type = FormFieldType.Enum }, 123, string.Format($"{_defValidateMsgs.Types.Enum }{_customSuffix}", _displayName, FormFieldType.Enum) },
new object[] { new FormValidationRule { Type = FormFieldType.Number }, "str", string.Format($"{_defValidateMsgs.Types.Number }{_customSuffix}", _displayName, FormFieldType.Number) },
new object[] { new FormValidationRule { Type = FormFieldType.Date }, 123, string.Format($"{_defValidateMsgs.Types.Date }{_customSuffix}", _displayName, FormFieldType.Date) },
new object[] { new FormValidationRule { Type = FormFieldType.Boolean }, "str", string.Format($"{_defValidateMsgs.Types.Boolean}{_customSuffix}", _displayName, FormFieldType.Boolean) },
@ -433,7 +429,6 @@ namespace AntDesign.Tests.Form.Validation
customValidateMessage.Types.String = $"{customValidateMessage.Types.String}{suffix}";
customValidateMessage.Types.Array = $"{customValidateMessage.Types.Array}{suffix}";
customValidateMessage.Types.Object = $"{customValidateMessage.Types.Object}{suffix}";
customValidateMessage.Types.Enum = $"{customValidateMessage.Types.Enum}{suffix}";
customValidateMessage.Types.Number = $"{customValidateMessage.Types.Number}{suffix}";
customValidateMessage.Types.Date = $"{customValidateMessage.Types.Date}{suffix}";
customValidateMessage.Types.Boolean = $"{customValidateMessage.Types.Boolean}{suffix}";
@ -472,6 +467,13 @@ namespace AntDesign.Tests.Form.Validation
Value = value,
FieldName = _fieldName,
DisplayName = _displayName,
FieldType = rule.Type switch
{
FormFieldType.String => typeof(string),
FormFieldType.Number => typeof(int),
FormFieldType.Array => typeof(string[]),
_ => typeof(object)
}
};
return FormValidateHelper.GetValidationResult(validationContext);

Просмотреть файл

@ -68,6 +68,7 @@
);
//Act
cut.InvokeAsync(() => cut.Instance.Validate());
//Assert
cut.Invoking(c => cut.Find("div.ant-form-item-explain-error"))
.Should().Throw<ElementNotFoundException>();