Skip to content

Commit d8b0458

Browse files
AiYuZhenArgoZhang
andauthored
feat(MetadataTypeAttribute): support IValidatableObject/IValidateCollection (#4323)
* 增增 MetadataTypeAttribute 方式,以支持 IValidatableObject、IValidateCollection 接口验证方式。 增加简单示例。 * 增强 MetadataTypeAttribute 方式,以支持 IValidatableObject、IValidateCollection 接口验证方式。 增加简单示例。 修复文档测试错误。 * 修复代码覆盖率。 * 添加测试代码。 * 完善代码覆盖率。 * 调整示例。 * refactor: 代码格式化与规范化 * refactor: 移除不需要的命名空间 * doc: 更新示例代码 * refactor: 增加扩展方法提高代码复用率 * doc: 更新示例 * doc: 更新 MetadataType 示例 * refactor: 更新验证逻辑 * test: 更新单元测试 * chore: bump version 8.9.3-beta01 --------- Co-authored-by: Argo Zhang <argo@live.ca>
1 parent cf67c85 commit d8b0458

File tree

8 files changed

+163
-15
lines changed

8 files changed

+163
-15
lines changed

src/BootstrapBlazor.Server/Components/Samples/ValidateForms.razor

+16
Original file line numberDiff line numberDiff line change
@@ -400,6 +400,22 @@ private Task OnClickValidate()
400400
<ConsoleLogger @ref="Logger7" />
401401
</DemoBlock>
402402

403+
<DemoBlock Title="@Localizer["ValidateMetadataTypeTitle"]" Introduction="@Localizer["ValidateMetadataTypeIntro"]" Name="MetadataType">
404+
<ValidateForm Model="@_mockModel">
405+
<div class="row g-3">
406+
<div class="col-12 col-sm-6">
407+
<BootstrapInput @bind-Value="@_mockModel.Email" />
408+
</div>
409+
<div class="col-12 col-sm-6">
410+
<BootstrapInput @bind-Value="@_mockModel.ConfirmEmail" />
411+
</div>
412+
<div class="col-12">
413+
<Button ButtonType="@ButtonType.Submit" Text="@Localizer["ValidateFormsSubmitButtonText"]" />
414+
</div>
415+
</div>
416+
</ValidateForm>
417+
</DemoBlock>
418+
403419
<AttributeTable Items="@GetAttributes()" />
404420

405421
<MethodTable Items="@GetMethods()" />

src/BootstrapBlazor.Server/Components/Samples/ValidateForms.razor.cs

+43-4
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ private void OnFiledChanged(string field, object? value)
3636
}
3737

3838
/// <summary>
39-
/// OnInitializedAsync 方法
39+
/// <inheritdoc/>
4040
/// </summary>
4141
/// <returns></returns>
4242
protected override async Task OnInitializedAsync()
@@ -132,19 +132,58 @@ private Task OnInvalidSubmitAddress(EditContext context)
132132

133133
private ConcurrentDictionary<FieldIdentifier, object?> GetValueChangedFieldCollection() => ComplexForm?.ValueChangedFields ?? new ConcurrentDictionary<FieldIdentifier, object?>();
134134

135-
private class ComplexFoo : Foo
135+
private readonly MockModel _mockModel = new() { Email = "argo@live.ca", ConfirmEmail = "argo@163.com" };
136+
137+
[MetadataType(typeof(MockModelMetadataType))]
138+
class MockModel
139+
{
140+
public string? Email { get; set; }
141+
142+
public string? ConfirmEmail { get; set; }
143+
}
144+
145+
class MockModelMetadataType : IValidateCollection
146+
{
147+
private readonly List<string> _validMemberNames = [];
148+
private readonly List<ValidationResult> _invalidMemberNames = [];
149+
150+
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
151+
{
152+
_validMemberNames.Clear();
153+
_invalidMemberNames.Clear();
154+
if (validationContext.ObjectInstance is MockModel model)
155+
{
156+
if (!string.IsNullOrEmpty(model.Email) && !string.IsNullOrEmpty(model.ConfirmEmail)
157+
&& !model.Email.Equals(model.ConfirmEmail, StringComparison.OrdinalIgnoreCase))
158+
{
159+
_invalidMemberNames.Add(new ValidationResult("两个值必须一致。", [nameof(model.Email), nameof(model.ConfirmEmail)]));
160+
}
161+
else
162+
{
163+
_validMemberNames.AddRange([nameof(model.Email), nameof(model.ConfirmEmail)]);
164+
}
165+
}
166+
return InvalidMemberNames();
167+
}
168+
169+
public List<string> ValidMemberNames() => _validMemberNames;
170+
171+
public List<ValidationResult> InvalidMemberNames() => _invalidMemberNames;
172+
}
173+
174+
class ComplexFoo : Foo
136175
{
137176
[NotNull]
138177
public Dummy1? Dummy { get; set; }
139178
}
140179

141-
private class Dummy1
180+
class Dummy1
142181
{
143182
[NotNull]
144183
public Dummy2? Dummy2 { get; set; }
145184
}
146185

147-
private class Dummy2
186+
class Dummy2
148187
{
149188
[Required]
150189
public string? Name { get; set; }

src/BootstrapBlazor.Server/Locales/en-US.json

+3-1
Original file line numberDiff line numberDiff line change
@@ -3514,7 +3514,9 @@
35143514
"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.",
35153515
"ValidateFormIValidateCollectionTitle": "IValidateCollection",
35163516
"ValidateFormIValidateCollectionIntro": "The <code>IValidateCollection</code> interface provides more flexible custom <code>linked</code> validation, which is very suitable for very complex data validation, such as combining multiple attributes for validation.",
3517-
"ValidateFormIValidateCollectionDescription": "This example uses <code>IValidateCollection</code> to verify that <code>Telephone 1</code> and <code>Telephone 2</code> cannot be the same. If any cell is changed so that the phone numbers are the same, both text boxes will prompt."
3517+
"ValidateFormIValidateCollectionDescription": "This example uses <code>IValidateCollection</code> to verify that <code>Telephone 1</code> and <code>Telephone 2</code> cannot be the same. If any cell is changed so that the phone numbers are the same, both text boxes will prompt.",
3518+
"ValidateMetadataTypeTitle": "MetadataType IValidateCollection",
3519+
"ValidateMetadataTypeIntro": "The model uses <code>[MetadataType(typeof(MockModelMetadataType))]</code> to specify the metadata type <code>MockModelMetadataType</code> for model validation. If the specified model inherits the <code>IValidateCollection</code> interface, its <code>Validate</code> method is called to perform data compliance checks."
35183520
},
35193521
"BootstrapBlazor.Server.Components.Samples.Ajaxs": {
35203522
"AjaxTitle": "Ajax call",

src/BootstrapBlazor.Server/Locales/zh-CN.json

+3-1
Original file line numberDiff line numberDiff line change
@@ -3514,7 +3514,9 @@
35143514
"ValidateFormIValidatableObjectDescription": "本示例使用 <code>IValidatableObject</code> 实现校验 <code>联系电话1</code> 和 <code>联系电话2</code> 不能相同,以及 <code>名称</code> 不能为空,本例中并未设置 <code>名称</code> 为必填项,但是由于模型对 <code>IValidatableObject</code> 接口的实现中进行了校验,所以在提交数据时仍然触发 <code>OnInvalidSubmit</code> 回调委托",
35153515
"ValidateFormIValidateCollectionTitle": "IValidateCollection 接口类型",
35163516
"ValidateFormIValidateCollectionIntro": "<code>IValidateCollection</code> 接口提供了更加灵活的自定义 <code>联动</code> 校验,非常适合进行非常复杂的数据校验,例如多属性组合起来进行校验等场景。",
3517-
"ValidateFormIValidateCollectionDescription": "本示例使用 <code>IValidateCollection</code> 实现校验 <code>联系电话1</code> 和 <code>联系电话2</code> 不能相同校验,更改任意单元格使电话号码相同时两个文本框均进行提示"
3517+
"ValidateFormIValidateCollectionDescription": "本示例使用 <code>IValidateCollection</code> 实现校验 <code>联系电话1</code> 和 <code>联系电话2</code> 不能相同校验,更改任意单元格使电话号码相同时两个文本框均进行提示",
3518+
"ValidateMetadataTypeTitle": "MetadataType IValidateCollection",
3519+
"ValidateMetadataTypeIntro": "模型通过 <code>[MetadataType(typeof(MockModelMetadataType))]</code> 指定元数据类型 <code>MockModelMetadataType</code> 进行模型验证,如果指定模型继承 <code>IValidateCollection</code> 接口时,调用其 <code>Validate</code> 方法进行数据合规性检查"
35183520
},
35193521
"BootstrapBlazor.Server.Components.Samples.Ajaxs": {
35203522
"AjaxTitle": "Ajax调用",

src/BootstrapBlazor/BootstrapBlazor.csproj

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<Project Sdk="Microsoft.NET.Sdk.Razor">
22

33
<PropertyGroup>
4-
<Version>8.9.2</Version>
4+
<Version>8.9.3-beta01</Version>
55
</PropertyGroup>
66

77
<ItemGroup Condition="'$(TargetFramework)' == 'net5.0'">

src/BootstrapBlazor/Components/ValidateForm/ValidateForm.razor.cs

+25-7
Original file line numberDiff line numberDiff line change
@@ -295,9 +295,18 @@ internal async Task ValidateObject(ValidationContext context, List<ValidationRes
295295
// 验证 IValidatableObject
296296
if (results.Count == 0)
297297
{
298-
if (context.ObjectInstance is IValidatableObject validatableObject)
298+
IValidatableObject? validate;
299+
if (context.ObjectInstance is IValidatableObject v)
299300
{
300-
var messages = validatableObject.Validate(context);
301+
validate = v;
302+
}
303+
else
304+
{
305+
validate = context.GetInstanceFromMetadataType<IValidatableObject>();
306+
}
307+
if (validate != null)
308+
{
309+
var messages = validate.Validate(context);
301310
if (messages.Any())
302311
{
303312
foreach (var key in _validatorCache.Keys)
@@ -506,11 +515,20 @@ private async Task ValidateAsync(IValidateComponent validator, ValidationContext
506515
if (messages.Count == 0)
507516
{
508517
// 联动字段验证 IValidateCollection
509-
if (context.ObjectInstance is IValidateCollection validateCollection)
518+
IValidateCollection? validate;
519+
if (context.ObjectInstance is IValidateCollection v)
520+
{
521+
validate = v;
522+
}
523+
else
524+
{
525+
validate = context.GetInstanceFromMetadataType<IValidateCollection>();
526+
}
527+
if (validate != null)
510528
{
511-
messages.AddRange(validateCollection.Validate(context));
512-
ValidMemberNames.AddRange(validateCollection.ValidMemberNames());
513-
InvalidMemberNames.AddRange(validateCollection.InvalidMemberNames());
529+
messages.AddRange(validate.Validate(context));
530+
ValidMemberNames.AddRange(validate.ValidMemberNames());
531+
InvalidMemberNames.AddRange(validate.InvalidMemberNames());
514532
}
515533
}
516534
}
@@ -619,7 +637,7 @@ public void NotifyFieldChanged(in FieldIdentifier fieldIdentifier, object? value
619637
OnFieldValueChanged?.Invoke(fieldIdentifier.FieldName, value);
620638
}
621639

622-
private List<string> _invalidComponents = [];
640+
private readonly List<string> _invalidComponents = [];
623641

624642
internal void AddValidationComponent(string id)
625643
{
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// Copyright (c) Argo Zhang (argo@163.com). All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
// Website: https://www.blazor.zone or https://argozhang.github.io/
4+
5+
using Microsoft.Extensions.DependencyInjection;
6+
using System.Reflection;
7+
8+
namespace BootstrapBlazor.Components;
9+
10+
/// <summary>
11+
/// ValidationContext 扩展方法
12+
/// </summary>
13+
public static class ValidationContextExtensions
14+
{
15+
/// <summary>
16+
/// 从 <see cref="MetadataTypeAttribute"/> 中获取指定类型实例
17+
/// </summary>
18+
/// <typeparam name="T">验证接口类型</typeparam>
19+
/// <param name="context"></param>
20+
/// <returns>没有实现 <typeparamref name="T"/> 接口,则返回 <see langword="null"/></returns>
21+
public static T? GetInstanceFromMetadataType<T>(this ValidationContext context) where T : class
22+
{
23+
T? ret = default;
24+
var attribute = context.ObjectInstance.GetType().GetCustomAttribute<MetadataTypeAttribute>();
25+
if (attribute != null && attribute.MetadataClassType.GetInterfaces().Any(x => x.Equals(typeof(T))))
26+
{
27+
//此处是否需要缓存?
28+
ret = ActivatorUtilities.CreateInstance(context, attribute.MetadataClassType) as T;
29+
}
30+
return ret;
31+
}
32+
}

test/UnitTest/Components/ValidateFormTest.cs

+40-1
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,30 @@ public void MetadataTypeAttribute_Ok()
237237
cut.InvokeAsync(() => form.Submit());
238238
}
239239

240+
[Fact]
241+
public void MetadataTypeIValidatableObject_Ok()
242+
{
243+
var foo = new Dummy() { Password1 = "password", Password2 = "Password2" };
244+
var cut = Context.RenderComponent<ValidateForm>(pb =>
245+
{
246+
pb.Add(a => a.Model, foo);
247+
pb.AddChildContent<MockInput<string>>(pb =>
248+
{
249+
pb.Add(a => a.Value, foo.Password1);
250+
pb.Add(a => a.ValueExpression, Utility.GenerateValueExpression(foo, "Password1", typeof(string)));
251+
});
252+
pb.AddChildContent<MockInput<string>>(pb =>
253+
{
254+
pb.Add(a => a.Value, foo.Password2);
255+
pb.Add(a => a.ValueExpression, Utility.GenerateValueExpression(foo, "Password2", typeof(string)));
256+
});
257+
});
258+
var form = cut.Find("form");
259+
cut.InvokeAsync(() => form.Submit());
260+
var message = cut.FindComponent<MockInput<string>>().Instance.GetErrorMessage();
261+
Assert.Equal("两次密码必须一致。", message);
262+
}
263+
240264
[Fact]
241265
public void Validate_Class_Ok()
242266
{
@@ -672,12 +696,27 @@ private class Dummy
672696

673697
[Required]
674698
public string? File { get; set; }
699+
700+
public string? Password1 { get; set; }
701+
public string? Password2 { get; set; }
675702
}
676703

677-
private class DummyMetadata
704+
private class DummyMetadata : IValidatableObject
678705
{
679706
[Required]
680707
public DateTime? Value { get; set; }
708+
709+
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
710+
{
711+
var result = new List<ValidationResult>();
712+
if (validationContext.ObjectInstance is Dummy dy)
713+
{
714+
if (!string.Equals(dy.Password1, dy.Password2, StringComparison.InvariantCultureIgnoreCase))
715+
result.Add(new ValidationResult("两次密码必须一致。",
716+
[nameof(Dummy.Password1), nameof(Dummy.Password2)]));
717+
}
718+
return result;
719+
}
681720
}
682721

683722
private class MockFoo

0 commit comments

Comments
 (0)