feat(module: form): support nest property validation and add table form demo (#4102)

* fix(module: form): support table editor validation

* fix error

* fix DisplayName
This commit is contained in:
James Yeung 2024-08-20 21:28:24 +08:00 committed by GitHub
parent 3faebe2456
commit a81be3de54
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 178 additions and 69 deletions

View File

@ -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<ValidationAttribute>(true).ToArray();
if (parentReflector?.ValidationAttributes?.Length > 0)
{

View File

@ -27,10 +27,8 @@
<AntDesign.Col @attributes="GetWrapperColAttributes()" Class=@($"{_prefixCls}-control")>
<div class=@($"{_prefixCls}-control-input")>
<div class=@($"{_prefixCls}-control-input-content")>
<CascadingValue Value="null" Name="Form" TValue="IForm" IsFixed="@true">
<CascadingValue Value="this" Name="FormItem" TValue="IFormItem" IsFixed="@true">
@ChildContent
</CascadingValue>
<CascadingValue Value="this" Name="FormItem" TValue="IFormItem" IsFixed="@true">
@ChildContent
</CascadingValue>
</div>
</div>

View File

@ -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<string[]> _onValidated;
private Action<string[]> _onValidated = _ => { };
private IEnumerable<FormValidationRule> _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<object>(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<object>(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

View File

@ -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; }
}

View File

@ -1,26 +0,0 @@
@using System.ComponentModel.DataAnnotations;
<Form Model="@model"
LabelColSpan="8"
WrapperColSpan="16">
<FormItem>
<LabelTemplate>
<label class="ant-form-item-required" for="username">
<Tooltip Title="Enter your username">Username</Tooltip>
</label>
</LabelTemplate>
<ChildContent>
<Input Id="username" @bind-Value="@context.Username" />
</ChildContent>
</FormItem>
</Form>
@code{
public class Model
{
[Required]
public string Username { get; set; }
}
private Model model = new Model();
}

View File

@ -0,0 +1,77 @@
@using System.ComponentModel.DataAnnotations;
<Form Model="@model"
ValidateMode="FormValidateMode.Complex"
OnFinish="OnFinish"
OnFinishFailed="OnFinishFailed"
LabelColSpan="8"
WrapperColSpan="16">
<FormItem Label="Class Name">
<Input @bind-Value="@context.ClassName" />
</FormItem>
<FormItem Label="Students" Name="@nameof(context.Students)" Rules="[new(){ Min=1 }]">
<Button OnClick="addRow" Type="primary" Style="margin-bottom:16px" Size="small">
Add a row
</Button>
<Table DataSource="context.Students" TItem="Student" Context="row" Size="TableSize.Small" HidePagination Bordered>
<PropertyColumn Width="30%" Property="c=>c.Name">
<FormItem Required>
<Input @bind-Value="@row.Name" />
</FormItem>
</PropertyColumn>
<PropertyColumn Property="c=>c.Address">
<FormItem Required>
<Input @bind-Value="@row.Address" />
</FormItem>
</PropertyColumn>
</Table>
</FormItem>
<FormItem WrapperColOffset="8" WrapperColSpan="16">
<Button Type="@ButtonType.Primary" HtmlType="submit">
Submit
</Button>
</FormItem>
</Form>
<style>
.ant-form-item-has-error .ant-table {
border: 1px solid red;
}
</style>
@code
{
private Model model = new Model();
public class Model
{
[Required]
public string ClassName { get; set; }
public List<Student> 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)}");
}
}

View File

@ -15,6 +15,7 @@ title:
- `Address`:有两个控件,在 `FormItem` 内使用两个 `<FormItem NoStyle />` 分别绑定对应控件一个FormItem下只能出现一个使用了@bing-Value的控件对FormItem使用NoStyle则FormItem的Grid布局会被忽略就算主动使用了LabelCol或WrapperCol也不会产生效果。
这个场景还展示了复杂类型的表单验证Address 属性是一个包含了两个属性的类型,当级联表单项绑定了它的属性时,会继承这个属性的所有 Attributes但由于表单未绑定这个属性外部表单无法获取 Required 和 Label。
- `BirthDate`:有两个内联控件,错误信息展示各自控件下,使用两个 `<FormItem />` 分别绑定对应控件,并修改 `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 `<FormItem NoStyle />` 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 `<FormItem />`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.

View File

@ -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.

View File

@ -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.