From a81be3de546384cbc98918b56ce4950ac423a5f0 Mon Sep 17 00:00:00 2001 From: James Yeung Date: Tue, 20 Aug 2024 21:28:24 +0800 Subject: [PATCH] feat(module: form): support nest property validation and add table form demo (#4102) * fix(module: form): support table editor validation * fix error * fix DisplayName --- .../core/Reflection/PropertyReflector.cs | 3 +- components/form/FormItem.razor | 6 +- components/form/FormItem.razor.cs | 99 +++++++++++++++---- .../Form/demo/ComplexFormControl.razor | 4 +- .../Form/demo/CustomizedLabel.razor | 26 ----- .../Components/Form/demo/NestTable.razor | 77 +++++++++++++++ .../Form/demo/complex-form-control.md | 4 +- .../Components/Form/demo/customized-label.md | 14 --- .../Demos/Components/Form/demo/nest-table.md | 14 +++ 9 files changed, 178 insertions(+), 69 deletions(-) delete mode 100644 site/AntDesign.Docs/Demos/Components/Form/demo/CustomizedLabel.razor create mode 100644 site/AntDesign.Docs/Demos/Components/Form/demo/NestTable.razor delete mode 100644 site/AntDesign.Docs/Demos/Components/Form/demo/customized-label.md create mode 100644 site/AntDesign.Docs/Demos/Components/Form/demo/nest-table.md diff --git a/components/core/Reflection/PropertyReflector.cs b/components/core/Reflection/PropertyReflector.cs index 2a2e16ce..7e9be14c 100644 --- a/components/core/Reflection/PropertyReflector.cs +++ b/components/core/Reflection/PropertyReflector.cs @@ -10,6 +10,7 @@ namespace AntDesign.Core.Reflection { internal class PropertyReflector { + public PropertyInfo PropertyInfo { get; set; } public RequiredAttribute RequiredAttribute { get; set; } public ValidationAttribute[] ValidationAttributes { get; set; } @@ -31,7 +32,7 @@ namespace AntDesign.Core.Reflection public PropertyReflector(MemberInfo propertyInfo, PropertyReflector parentReflector = null) { ParentReflector = parentReflector; - + PropertyInfo = propertyInfo as PropertyInfo; ValidationAttributes = propertyInfo?.GetCustomAttributes(true).ToArray(); if (parentReflector?.ValidationAttributes?.Length > 0) { diff --git a/components/form/FormItem.razor b/components/form/FormItem.razor index 2e847836..8140c994 100644 --- a/components/form/FormItem.razor +++ b/components/form/FormItem.razor @@ -27,10 +27,8 @@
- - - @ChildContent - + + @ChildContent
diff --git a/components/form/FormItem.razor.cs b/components/form/FormItem.razor.cs index b806c10a..d88c378d 100644 --- a/components/form/FormItem.razor.cs +++ b/components/form/FormItem.razor.cs @@ -3,10 +3,13 @@ // See the LICENSE file in the project root for more information. using System; +using System.Collections; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; +using System.Linq.Expressions; using System.Reflection; +using AntDesign.Core.Helpers.MemberPath; using AntDesign.Core.Reflection; using AntDesign.Form.Locale; using AntDesign.Forms; @@ -184,7 +187,7 @@ namespace AntDesign private AntLabelAlignType? FormLabelAlign => LabelAlign ?? Form?.LabelAlign; - private string DisplayName => Label ?? _propertyReflector?.DisplayName ?? _propertyReflector?.PropertyName; + private string DisplayName => Label ?? _propertyReflector?.DisplayName; private string _name; private Action _nameChanged; @@ -200,7 +203,7 @@ namespace AntDesign private FormValidateStatus? _originalValidateStatus; private Action _vaildateStatusChanged; - private Action _onValidated; + private Action _onValidated = _ => { }; private IEnumerable _rules; @@ -241,6 +244,8 @@ namespace AntDesign } SetInternalIsRequired(); + + SetEventHandlers(); } private void SetClass() @@ -280,6 +285,26 @@ namespace AntDesign } } + private void SetEventHandlers() + { + if (Form?.ValidateOnChange == true) + { + _validationStateChangedHandler = (s, e) => + { + UpdateValidateMessage(); + }; + CurrentEditContext.OnValidationStateChanged += _validationStateChangedHandler; + } + else + { + _validationRequestedHandler = (s, e) => + { + UpdateValidateMessage(); + }; + CurrentEditContext.OnValidationRequested += _validationRequestedHandler; + } + } + protected override void OnParametersSet() { if (!string.IsNullOrWhiteSpace(Help)) @@ -386,7 +411,7 @@ namespace AntDesign private void UpdateValidateMessage() { - if (_control == null) + if (_fieldIdentifier.Model == null) { return; } @@ -423,23 +448,6 @@ namespace AntDesign _fieldIdentifier = control.FieldIdentifier; _control = control; - if (Form?.ValidateOnChange == true) - { - _validationStateChangedHandler = (s, e) => - { - UpdateValidateMessage(); - }; - CurrentEditContext.OnValidationStateChanged += _validationStateChangedHandler; - } - else - { - _validationRequestedHandler = (s, e) => - { - UpdateValidateMessage(); - }; - CurrentEditContext.OnValidationRequested += _validationRequestedHandler; - } - if (control.PopertyReflector is not null) { _propertyReflector = control.PopertyReflector; @@ -461,8 +469,57 @@ namespace AntDesign StateHasChanged(); } + private void BuildPropertyWithName() + { + var type = Form.Model.GetType(); + var dataIndex = Name; + if (typeof(IDictionary).IsAssignableFrom(type)) + { + dataIndex = $"['{dataIndex}']"; + } + + LambdaExpression exp = PathHelper.GetLambda(dataIndex, type); + + if (exp.Body is UnaryExpression unary) + { + if (unary.Operand is MemberExpression member) + { + var perpertyInfo = member.Member as PropertyInfo; + _propertyReflector = new PropertyReflector(perpertyInfo); + } + } + else if (exp.Body is MemberExpression member) + { + var perpertyInfo = member.Member as PropertyInfo; + _propertyReflector = new PropertyReflector(perpertyInfo); + } + else + { + var getValueDelegate = PathHelper.GetDelegate(dataIndex, type); + _propertyReflector = new PropertyReflector + { + GetValueDelegate = getValueDelegate.Invoke, + PropertyName = Name, + DisplayName = Name, + ValidationAttributes = [] + }; + } + _fieldValueGetter = _propertyReflector?.GetValueDelegate; + _valueUnderlyingType = THelper.GetUnderlyingType(_propertyReflector.PropertyInfo.PropertyType); + _fieldIdentifier = new FieldIdentifier(Form.Model, Name); + SetRules(); + } + ValidationResult[] IFormItem.ValidateFieldWithRules() { + if (_propertyReflector is null) + { + if (!string.IsNullOrWhiteSpace(Name)) + { + BuildPropertyWithName(); + } + } + if (_propertyReflector is null) { return []; @@ -493,7 +550,7 @@ namespace AntDesign Rule = rule, Value = propertyValue, FieldName = _fieldIdentifier.FieldName, - DisplayName = DisplayName, + DisplayName = DisplayName ?? _propertyReflector.PropertyName, FieldType = _valueUnderlyingType, ValidateMessages = validateMessages, Model = Form.Model diff --git a/site/AntDesign.Docs/Demos/Components/Form/demo/ComplexFormControl.razor b/site/AntDesign.Docs/Demos/Components/Form/demo/ComplexFormControl.razor index 39207cd9..8d961650 100644 --- a/site/AntDesign.Docs/Demos/Components/Form/demo/ComplexFormControl.razor +++ b/site/AntDesign.Docs/Demos/Components/Form/demo/ComplexFormControl.razor @@ -43,9 +43,9 @@ { public class Address { - [Display(Name = "省份")] + [Display(Name = "Pr.")] public string Province { get; set; } - [Display(Name = "街道")] + [Display(Name = "St.")] public string Street { get; set; } } diff --git a/site/AntDesign.Docs/Demos/Components/Form/demo/CustomizedLabel.razor b/site/AntDesign.Docs/Demos/Components/Form/demo/CustomizedLabel.razor deleted file mode 100644 index 65f778b6..00000000 --- a/site/AntDesign.Docs/Demos/Components/Form/demo/CustomizedLabel.razor +++ /dev/null @@ -1,26 +0,0 @@ -@using System.ComponentModel.DataAnnotations; - -
- - - - - - - - -
- -@code{ - public class Model - { - [Required] - public string Username { get; set; } - } - - private Model model = new Model(); -} \ No newline at end of file diff --git a/site/AntDesign.Docs/Demos/Components/Form/demo/NestTable.razor b/site/AntDesign.Docs/Demos/Components/Form/demo/NestTable.razor new file mode 100644 index 00000000..daf392d2 --- /dev/null +++ b/site/AntDesign.Docs/Demos/Components/Form/demo/NestTable.razor @@ -0,0 +1,77 @@ +@using System.ComponentModel.DataAnnotations; + +
+ + + + + + + + + + + + + + + + +
+
+ + + +
+ + + +@code +{ + private Model model = new Model(); + + public class Model + { + [Required] + public string ClassName { get; set; } + + public List Students { get; set; } = []; + } + + public record Student() + { + public string Id { get; set; } + public string Age { get; set; } + public string Name { get; set; } + + public string Address { get; set; } + } + + void addRow() + { + model.Students.Add(new()); + } + + private void OnFinish(EditContext editContext) + { + Console.WriteLine($"Success:{JsonSerializer.Serialize(model)}"); + } + + private void OnFinishFailed(EditContext editContext) + { + Console.WriteLine($"Failed:{JsonSerializer.Serialize(model)}"); + } +} \ No newline at end of file diff --git a/site/AntDesign.Docs/Demos/Components/Form/demo/complex-form-control.md b/site/AntDesign.Docs/Demos/Components/Form/demo/complex-form-control.md index c5f7adc0..ce9f6f60 100644 --- a/site/AntDesign.Docs/Demos/Components/Form/demo/complex-form-control.md +++ b/site/AntDesign.Docs/Demos/Components/Form/demo/complex-form-control.md @@ -15,6 +15,7 @@ title: - `Address`:有两个控件,在 `FormItem` 内使用两个 `` 分别绑定对应控件(一个FormItem下只能出现一个使用了@bing-Value的控件),对FormItem使用NoStyle,则FormItem的Grid布局会被忽略,就算主动使用了LabelCol或WrapperCol也不会产生效果。 这个场景还展示了复杂类型的表单验证,Address 属性是一个包含了两个属性的类型,当级联表单项绑定了它的属性时,会继承这个属性的所有 Attributes,但由于表单未绑定这个属性,外部表单无法获取 Required 和 Label。 + - `BirthDate`:有两个内联控件,错误信息展示各自控件下,使用两个 `` 分别绑定对应控件,并修改 `style` 使其内联布局。 更复杂的封装复用方式可以参考下面的 `自定义表单控件` 。 @@ -28,7 +29,8 @@ Three typical scenarios are shown here. - `Username`: there is a description text or other component behind the input box, within `FormItem` only the component that uses @bind-Value will be bound to the FormItem, other components can be added at will. - `Address`: there are two controls, use two `` within the `FormItem` to bind the corresponding controls separately (only one control with @bind-Value can appear under a FormItem), use NoStyle for the FormItem, then The Grid layout of the FormItem will be ignored, even if LabelCol or WrapperCol is actively used. - This scenario also demonstrates the validation of a complex type of form, Address is a class structure containing two properties and by attaching ValidateComplexType the form can be validated against all its properties. Details can be found in the Blazor documentation: [Nested Models, Collection Types and Complex Types](https://docs.microsoft.com/zh-cn/aspnet/core/blazor/forms-validation?WT.mc_id=DT-MVP-5003987) +The scenario also shows complex types of form validation. The Address property is a type that contains two properties. When a cascading form entry binds its property, it inherits all attributes of that property, but because the form does not bind this property, the external form cannot obtain Required and Label. + - `BirthDate`: there are two inline controls with error messages displayed under each control, using two ``s to bind the corresponding controls separately, and modifying the `style` to make the layout inline. For a more complex way of wrapping and reusing controls see `Custom Form Controls` below. \ No newline at end of file diff --git a/site/AntDesign.Docs/Demos/Components/Form/demo/customized-label.md b/site/AntDesign.Docs/Demos/Components/Form/demo/customized-label.md deleted file mode 100644 index 43629995..00000000 --- a/site/AntDesign.Docs/Demos/Components/Form/demo/customized-label.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -order: 6.2 -title: - zh-CN: 自定义标签 - en-US: Customized Label ---- - -## zh-CN - -标签可以通过LabelTemplate来自定义。 - -## en-US - -Label can be customized by using LabelTemplate. diff --git a/site/AntDesign.Docs/Demos/Components/Form/demo/nest-table.md b/site/AntDesign.Docs/Demos/Components/Form/demo/nest-table.md new file mode 100644 index 00000000..fed98a1e --- /dev/null +++ b/site/AntDesign.Docs/Demos/Components/Form/demo/nest-table.md @@ -0,0 +1,14 @@ +--- +order: 6.2 +title: + zh-CN: 表格编辑与验证 + en-US: Table Edit & Validate +--- + +## zh-CN + +支持使用表格作为表单组件,并验证单元格编辑。 + +## en-US + +Support using table as form component and validate cell edit. \ No newline at end of file