Skip to content

Commit

Permalink
feat(ValidateForm): add IValidatableObject support (#4260)
Browse files Browse the repository at this point in the history
* 增加IValidatableObject支持

* 增加单元测试

* 补充demo模型

* 调整单元测试

* refactor: 更改模型名称

* chore: 更新字典

* chore: 移除不使用的命名空间

* doc: 更新示例

* fix: 修复字段更改时验证失效问题

* test: 更新单元测试

* doc: 格式化文档

* doc: 更新注释

* feat: 增加 IValidataResult 接口

* doc: 增加 IValidataResult 接口示例

* feat: 更新示例

* feat: 增加 ResetMemberNames 属性用于验证联动

* refactor: 调整时长防止脚本报错

* feat: 更新组件验证逻辑

* test: 更新单元测试

* refactor: 增加联动逻辑

* refactor: 精简代码

* feat: 增加验证失败联动逻辑

* refactor: 更改为 IValidateCollection 接口

* chore: 更新组件验证联动逻辑

* doc: 更新示例

* doc: 更新示例

* doc: 撤销更新准备还原类验证逻辑

* feat: 增加 IValidatableObject 逻辑支持

* refactor: 增加异常保护

* doc: 更新示例

* test: 更新单元测试

---------

Co-authored-by: Argo-AscioTech <[email protected]>
  • Loading branch information
izanhzh and ArgoZhang authored Sep 11, 2024
1 parent 9425792 commit 0718f9c
Show file tree
Hide file tree
Showing 15 changed files with 393 additions and 10 deletions.
2 changes: 2 additions & 0 deletions exclusion.dic
Original file line number Diff line number Diff line change
Expand Up @@ -99,3 +99,5 @@ autoplay
youtube
vimeo
scrlang
Validata
Validatable
83 changes: 83 additions & 0 deletions src/BootstrapBlazor.Server/Components/Samples/ValidateForms.razor
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,89 @@ private Task OnClickValidate()
</ValidateForm>
</DemoBlock>

@* <DemoBlock Title="@Localizer["ValidateFormIValidateCollectionTitle"]" Introduction="@Localizer["ValidateFormIValidateCollectionIntro"]" Name="IValidateCollectionObject">
<section ignore>
<p>@((MarkupString)Localizer["ValidateFormIValidateCollectionDescription"].Value)</p>
<Pre class="mb-3">public class CustomValidataModel : IValidatableObject
{
public string? Name { get; set; }

public string? Telephone1 { get; set; }

public string? Telephone2 { get; set; }

public IEnumerable&lt;ValidationResult&gt; Validate(ValidationContext validationContext)
{
if (string.Equals(Telephone1, Telephone2, StringComparison.InvariantCultureIgnoreCase))
{
var localizer = validationContext.GetRequiredService&lt;IStringLocalizer&lt;CustomValidataModel&gt;&gt;();
yield return new ValidationResult(localizer["CanNotBeTheSame"], [nameof(Telephone1), nameof(Telephone2)]);
}
if (string.IsNullOrEmpty(Name))
{
yield return new ValidationResult("Name is required", [nameof(Name)]);
}
}
}</Pre>
<p>@((MarkupString)Localizer["ValidateFormIValidatableObjectDescription"].Value)</p>
</section>
<ValidateForm Model="@ValidateCollectionModel" OnInvalidSubmit="OnInvalidValidateCollection">
<div class="row g-3">
<div class="col-12 col-sm-6">
<BootstrapInput @bind-Value="@ValidateCollectionModel.Telephone1" />
</div>
<div class="col-12 col-sm-6">
<BootstrapInput @bind-Value="@ValidateCollectionModel.Telephone2" />
</div>
<div class="col-12">
<Button ButtonType="@ButtonType.Submit" Text="@Localizer["ValidateFormsSubmitButtonText"]" />
</div>
</div>
</ValidateForm>
<ConsoleLogger @ref="Logger7" />
</DemoBlock> *@

<DemoBlock Title="@Localizer["ValidateFormIValidatableObjectTitle"]" Introduction="@Localizer["ValidateFormIValidatableObjectIntro"]" Name="IValidatableObject">
<section ignore>
<p>@((MarkupString)Localizer["ValidateFormIValidatableObjectDescription"].Value)</p>
<Pre class="mb-3">public class CustomValidataModel : IValidatableObject
{
public string? Name { get; set; }

public string? Telephone1 { get; set; }

public string? Telephone2 { get; set; }

public IEnumerable&lt;ValidationResult&gt; Validate(ValidationContext validationContext)
{
if (string.Equals(Telephone1, Telephone2, StringComparison.InvariantCultureIgnoreCase))
{
var localizer = validationContext.GetRequiredService&lt;IStringLocalizer&lt;CustomValidataModel&gt;&gt;();
yield return new ValidationResult(localizer["CanNotBeTheSame"], [nameof(Telephone1), nameof(Telephone2)]);
}
if (string.IsNullOrEmpty(Name))
{
yield return new ValidationResult("Name is required", [nameof(Name)]);
}
}
}</Pre>
</section>
<ValidateForm Model="@ValidataModel" OnInvalidSubmit="OnInvalidValidatableObject">
<div class="row g-3">
<div class="col-12 col-sm-6">
<BootstrapInput @bind-Value="@ValidataModel.Telephone1" />
</div>
<div class="col-12 col-sm-6">
<BootstrapInput @bind-Value="@ValidataModel.Telephone2" />
</div>
<div class="col-12">
<Button ButtonType="@ButtonType.Submit" Text="@Localizer["ValidateFormsSubmitButtonText"]" />
</div>
</div>
</ValidateForm>
<ConsoleLogger @ref="Logger8" />
</DemoBlock>

<AttributeTable Items="@GetAttributes()" />

<MethodTable Items="@GetMethods()" />
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ protected override async Task OnInitializedAsync()
Model8 = new Foo { Name = "Name", Education = EnumEducation.Primary, DateTime = DateTime.Now };
Model9 = new Foo { Name = "Name", Education = EnumEducation.Primary, DateTime = DateTime.Now };
Model10 = new Foo { Name = "Name", Education = EnumEducation.Primary, DateTime = DateTime.Now };
ValidateCollectionModel = new CustomValidateCollectionModel { Telephone1 = "123456789", Telephone2 = "123456789" };
ValidataModel = new CustomValidataModel { Name = "", Telephone1 = "123456789", Telephone2 = "123456789" };

// 初始化参数
Hobbies2 = Foo.GenerateHobbies(LocalizerFoo);
Expand Down Expand Up @@ -216,6 +218,30 @@ private Task OnValidator()
[NotNull]
private Foo? Model10 { get; set; }

[NotNull]
private CustomValidateCollectionModel? ValidateCollectionModel { get; set; }

[NotNull]
private CustomValidataModel? ValidataModel { get; set; }

[NotNull]
private ConsoleLogger? Logger7 { get; set; }

private Task OnInvalidValidateCollection(EditContext context)
{
Logger7.Log(Localizer["OnInvalidSubmitCallBackLog"]);
return Task.CompletedTask;
}

[NotNull]
private ConsoleLogger? Logger8 { get; set; }

private Task OnInvalidValidatableObject(EditContext context)
{
Logger8.Log(Localizer["OnInvalidSubmitCallBackLog"]);
return Task.CompletedTask;
}

#region 参数说明
private AttributeItem[] GetAttributes() =>
[
Expand Down
45 changes: 45 additions & 0 deletions src/BootstrapBlazor.Server/Data/CustomValidataModel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// Copyright (c) Argo Zhang ([email protected]). All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
// Website: https://www.blazor.zone or https://argozhang.github.io/

namespace BootstrapBlazor.Server.Data;

/// <summary>
/// 公司模型类
/// </summary>
public class CustomValidataModel : IValidatableObject
{
/// <summary>
/// 名称
/// </summary>
public string? Name { get; set; }

/// <summary>
/// 联系电话1
/// </summary>
public string? Telephone1 { get; set; }

/// <summary>
/// 联系电话2
/// </summary>
public string? Telephone2 { get; set; }

/// <summary>
/// <inheritdoc/>
/// </summary>
/// <param name="validationContext"></param>
/// <returns></returns>
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
if (string.Equals(Telephone1, Telephone2, StringComparison.InvariantCultureIgnoreCase))
{
var localizer = validationContext.GetRequiredService<IStringLocalizer<CustomValidataModel>>();
yield return new ValidationResult(localizer["CanNotBeTheSame"], [nameof(Telephone1), nameof(Telephone2)]);
}

if (string.IsNullOrEmpty(Name))
{
yield return new ValidationResult("Name is required", [nameof(Name)]);
}
}
}
73 changes: 73 additions & 0 deletions src/BootstrapBlazor.Server/Data/CustomValidateCollectionModel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// Copyright (c) Argo Zhang ([email protected]). All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
// Website: https://www.blazor.zone or https://argozhang.github.io/

namespace BootstrapBlazor.Server.Data;

/// <summary>
/// 公司模型类
/// </summary>
public class CustomValidateCollectionModel : IValidateCollection
{
/// <summary>
/// 联系电话1
/// </summary>
[Display(Name = "联系电话1")]
public string? Telephone1 { get; set; }

/// <summary>
/// 联系电话2
/// </summary>
[Display(Name = "联系电话2")]
public string? Telephone2 { get; set; }

private readonly List<string> _validMemberNames = [];

private readonly List<ValidationResult> _invalidMemberNames = [];

/// <summary>
/// <inheritdoc/>
/// </summary>
/// <param name="validationContext"></param>
/// <returns></returns>
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
_validMemberNames.Clear();
_invalidMemberNames.Clear();
if (string.Equals(Telephone1, Telephone2, StringComparison.InvariantCultureIgnoreCase))
{
var localizer = validationContext.GetRequiredService<IStringLocalizer<CustomValidateCollectionModel>>();
var errorMessage = localizer["CanNotBeTheSame"];
if (validationContext.MemberName == nameof(Telephone1))
{
_invalidMemberNames.Add(new ValidationResult(errorMessage, [nameof(Telephone2)]));
}
else if (validationContext.MemberName == nameof(Telephone2))
{
_invalidMemberNames.Add(new ValidationResult(errorMessage, [nameof(Telephone1)]));
}
yield return new ValidationResult(errorMessage, [validationContext.MemberName!]);
}
else if (validationContext.MemberName == nameof(Telephone1))
{
_validMemberNames.Add(nameof(Telephone2));

}
else if (validationContext.MemberName == nameof(Telephone2))
{
_validMemberNames.Add(nameof(Telephone1));
}
}

/// <summary>
/// <inheritdoc/>
/// </summary>
/// <returns></returns>
public List<string> ValidMemberNames() => _validMemberNames;

/// <summary>
/// <inheritdoc/>
/// </summary>
/// <returns></returns>
public List<ValidationResult> InvalidMemberNames() => _invalidMemberNames;
}
11 changes: 10 additions & 1 deletion src/BootstrapBlazor.Server/Locales/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -3508,7 +3508,10 @@
"DisableAutoSubmitFormByEnter": "Disable automatic submit form",
"ValidateFormDisableAutoSubmitFormByEnterTitle": "Disable automatic submit form",
"ValidateFormDisableAutoSubmitFormByEnterIntro": "When the focus is in the input box in the form, pressing <kbd>Enter</kbd> will automatically submit the form by default. You can disable automatic submission by setting <code>DisableAutoSubmitFormByEnter=\"false\"</code>",
"DisableAutoSubmitFormByEnterDesc": "<code>form</code> form is an important element of<code>web</code>. If there is an <code>input</code> element in the form, when this element gets the focus, pressing <kbd>Enter</kbd> will automatically submit the form. This is a feature of the form element, not our component code logic. If this function is not needed, disable it by setting <code>DisableAutoSubmitFormByEnter=\"true\"</code>, In this example, pressing Enter in the text box will not automatically <b>submit</b> and perform data compliance check. You need to click the <b>Submit</b> button to submit the form"
"DisableAutoSubmitFormByEnterDesc": "<code>form</code> form is an important element of<code>web</code>. If there is an <code>input</code> element in the form, when this element gets the focus, pressing <kbd>Enter</kbd> will automatically submit the form. This is a feature of the form element, not our component code logic. If this function is not needed, disable it by setting <code>DisableAutoSubmitFormByEnter=\"true\"</code>, In this example, pressing Enter in the text box will not automatically <b>submit</b> and perform data compliance check. You need to click the <b>Submit</b> button to submit the form",
"ValidateFormIValidatableObjectTitle": "IValidatableObject",
"ValidateFormIValidatableObjectIntro": "The <code>IValidatableObject</code> interface provides more flexible custom validation, making it very suitable for performing complex data validation scenarios, such as validating combinations of multiple properties.",
"ValidateFormIValidatableObjectDescription": "This example uses the <code>IValidatableObject</code> interface to implement validation that <code>Telephone 1</code> and <code>Telephone 2</code> must not be the same, and that <code>Name</code> cannot be empty. In this example, <code>Name</code> is not provided, but since the model implements validation through the <code>IValidatableObject</code> interface, it still triggers the <code>OnInvalidSubmit</code> callback delegate when submitting data."
},
"BootstrapBlazor.Server.Components.Samples.Ajaxs": {
"AjaxTitle": "Ajax call",
Expand Down Expand Up @@ -6779,5 +6782,11 @@
"PlayersYouTubeIntro": "Play <code>YouTube</code> videos by setting the <code>Mode=\"PlayerSources\"</code> parameter <b>Provider=\"youtube\"</b>",
"PlayersVimeoTitle": "Vimeo",
"PlayersVimeoIntro": "Play <code>Vimei</code> videos by setting <code>Mode=\"PlayerSources\"</code> parameter <b>Provider=\"vimeo\"</b>"
},
"BootstrapBlazor.Server.Data.CustomValidataModel": {
"Name": "Name",
"Telephone1": "Telephone 1",
"Telephone2": "Telephone 2",
"CanNotBeTheSame": "Telephone 1 and Telephone 2 can not be the same"
}
}
11 changes: 10 additions & 1 deletion src/BootstrapBlazor.Server/Locales/zh-CN.json
Original file line number Diff line number Diff line change
Expand Up @@ -3508,7 +3508,10 @@
"DisableAutoSubmitFormByEnter": "是否禁用表单自动提交功能",
"ValidateFormDisableAutoSubmitFormByEnterTitle": "禁用自动提交功能",
"ValidateFormDisableAutoSubmitFormByEnterIntro": "当焦点在表单内输入框时,按 <kbd>Enter</kbd> 默认是自动提交表单的,通过设置 <code>DisableAutoSubmitFormByEnter=\"false\"</code> 禁止自动提交",
"DisableAutoSubmitFormByEnterDesc": "<code>form</code> 表单是 <code>web</code> 一个重要的元素,如果表单内有 <code>input</code> 元素当此元素获得焦点时,按下 <kbd>Enter</kbd> 会自动提交表单,这是表单元素的特性,并不是我们组件代码逻辑,如果不需要这个功能时通过设置 <code>DisableAutoSubmitFormByEnter=\"true\"</code> 禁用此功能,此例中文本框内按回车不会 <b>自动提交</b> 并且进行数据合规性检查,需要点击 <b>提交表单</b> 按钮提交表单"
"DisableAutoSubmitFormByEnterDesc": "<code>form</code> 表单是 <code>web</code> 一个重要的元素,如果表单内有 <code>input</code> 元素当此元素获得焦点时,按下 <kbd>Enter</kbd> 会自动提交表单,这是表单元素的特性,并不是我们组件代码逻辑,如果不需要这个功能时通过设置 <code>DisableAutoSubmitFormByEnter=\"true\"</code> 禁用此功能,此例中文本框内按回车不会 <b>自动提交</b> 并且进行数据合规性检查,需要点击 <b>提交表单</b> 按钮提交表单",
"ValidateFormIValidatableObjectTitle": "IValidatableObject 接口类型",
"ValidateFormIValidatableObjectIntro": "<code>IValidatableObject</code> 接口提供了更加灵活的自定义校验,非常适合进行非常复杂的数据校验,例如多属性组合起来进行校验等场景。",
"ValidateFormIValidatableObjectDescription": "本示例使用 <code>IValidatableObject</code> 实现校验 <code>联系电话1</code> 和 <code>联系电话2</code> 不能相同,以及 <code>名称</code> 不能为空,本例中并未设置 <code>名称</code> 为必填项,但是由于模型对 <code>IValidatableObject</code> 接口的实现中进行了校验,所以在提交数据时仍然触发 <code>OnInvalidSubmit</code> 回调委托"
},
"BootstrapBlazor.Server.Components.Samples.Ajaxs": {
"AjaxTitle": "Ajax调用",
Expand Down Expand Up @@ -6779,5 +6782,11 @@
"PlayersYouTubeIntro": "通过设置 <code>Mode=\"PlayerSources\"</code> 参数 <b>Provider=\"youtube\"</b> 播放 <code>YouTube</code> 视频",
"PlayersVimeoTitle": "Vimeo 支持",
"PlayersVimeoIntro": "通过设置 <code>Mode=\"PlayerSources\"</code> 参数 <b>Provider=\"vimeo\"</b> 播放 <code>Vimei</code> 视频"
},
"BootstrapBlazor.Server.Data.CustomValidataModel": {
"Name": "名称",
"Telephone1": "联系电话1",
"Telephone2": "联系电话2",
"CanNotBeTheSame": "联系电话1 和 联系电话2 不能相同"
}
}
30 changes: 30 additions & 0 deletions src/BootstrapBlazor/Components/Validate/IValidateCollection.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// Copyright (c) Argo Zhang ([email protected]). All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
// Website: https://www.blazor.zone or https://argozhang.github.io/

namespace BootstrapBlazor.Components;

/// <summary>
/// IValidateCollection 多个验证结果接口 支持组件间联动验证
/// </summary>
public interface IValidateCollection
{
/// <summary>
/// 验证方法
/// </summary>
/// <param name="validationContext"></param>
/// <returns></returns>
IEnumerable<ValidationResult> Validate(ValidationContext validationContext);

/// <summary>
/// 返回合法成员集合
/// </summary>
/// <returns></returns>
List<string> ValidMemberNames();

/// <summary>
/// 返回非法成员集合
/// </summary>
/// <returns></returns>
List<ValidationResult> InvalidMemberNames();
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
namespace BootstrapBlazor.Components;

/// <summary>
/// IValidComponent 接口
/// IValidateComponent 接口
/// </summary>
public interface IValidateComponent
{
Expand Down
19 changes: 19 additions & 0 deletions src/BootstrapBlazor/Components/Validate/ValidateBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,25 @@ protected override void OnParametersSet()
{
Rules.Add(new RequiredValidator() { ErrorMessage = RequiredErrorMessage ?? GetDefaultRequiredErrorMessage() });
}

//if (ValidateForm != null)
//{
// var fieldName = FieldIdentifier?.FieldName;
// if (!string.IsNullOrEmpty(fieldName))
// {
// var item = ValidateForm.InvalidMemberNames.Find(i => i.MemberNames.Any(m => m == fieldName));
// if (item != null)
// {
// ValidateForm.InvalidMemberNames.Remove(item);
// IsValid = false;
// ErrorMessage = item.ErrorMessage;
// }
// else if (ValidateForm.ValidMemberNames.Remove(fieldName))
// {
// IsValid = true;
// }
// }
//}
}

/// <summary>
Expand Down
Loading

0 comments on commit 0718f9c

Please sign in to comment.