From 83704eb2c1fc86d3e6e46253f59d010736d21786 Mon Sep 17 00:00:00 2001 From: Argo Zhang Date: Tue, 1 Sep 2026 14:30:13 +0800 Subject: [PATCH 01/23] feat: support .NET 11 preview Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- Version.props | 4 ++-- global.json | 7 +++++++ src/BootstrapBlazor/Directory.Build.props | 12 +++++++++++- 3 files changed, 20 insertions(+), 3 deletions(-) create mode 100644 global.json diff --git a/Version.props b/Version.props index a39d9af4200..ba81f4b13c8 100644 --- a/Version.props +++ b/Version.props @@ -1,8 +1,8 @@ - net10.0 - net6.0;net7.0;net8.0;net9.0;net10.0 + net11.0 + net6.0;net7.0;net8.0;net9.0;net10.0;net11.0 diff --git a/global.json b/global.json new file mode 100644 index 00000000000..1752ec8cef2 --- /dev/null +++ b/global.json @@ -0,0 +1,7 @@ +{ + "sdk": { + "version": "11.0.100-preview.7.26381.103", + "rollForward": "latestPatch", + "allowPrerelease": true + } +} diff --git a/src/BootstrapBlazor/Directory.Build.props b/src/BootstrapBlazor/Directory.Build.props index 789d2c09dad..fb5000d7c6a 100644 --- a/src/BootstrapBlazor/Directory.Build.props +++ b/src/BootstrapBlazor/Directory.Build.props @@ -27,6 +27,7 @@ 8.0.* 9.0.* 10.0.0 + 11.0.0-preview* @@ -75,7 +76,16 @@ - + + + + + + + + + + From 6f79cfa4ecb560812ce368108c97bb8bbf435823 Mon Sep 17 00:00:00 2001 From: Argo Zhang Date: Tue, 1 Sep 2026 14:31:09 +0800 Subject: [PATCH 02/23] =?UTF-8?q?refactor:=20=E7=A7=BB=E9=99=A4=E5=86=97?= =?UTF-8?q?=E4=BD=99=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Components/DateTimePicker/DatePickerBody.razor.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/BootstrapBlazor/Components/DateTimePicker/DatePickerBody.razor.cs b/src/BootstrapBlazor/Components/DateTimePicker/DatePickerBody.razor.cs index 8405e12cd71..8580c2f0c22 100644 --- a/src/BootstrapBlazor/Components/DateTimePicker/DatePickerBody.razor.cs +++ b/src/BootstrapBlazor/Components/DateTimePicker/DatePickerBody.razor.cs @@ -796,7 +796,6 @@ private Task OnCloseTime() private void OnShowTimePicker() { _showTimePicker = true; - StateHasChanged(); } private bool HasSeconds => TimeFormat.Contains('s'); From c6b95153c9618b181b5993bd4af3b1f9efdbc7bc Mon Sep 17 00:00:00 2001 From: Argo Zhang Date: Tue, 1 Sep 2026 14:32:06 +0800 Subject: [PATCH 03/23] =?UTF-8?q?refactor(DateTimeRange):=20=E7=A7=BB?= =?UTF-8?q?=E9=99=A4=E5=86=97=E4=BD=99=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Components/DateTimeRange/DateTimeRange.razor.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/BootstrapBlazor/Components/DateTimeRange/DateTimeRange.razor.cs b/src/BootstrapBlazor/Components/DateTimeRange/DateTimeRange.razor.cs index 4e2987c719d..72399af84aa 100644 --- a/src/BootstrapBlazor/Components/DateTimeRange/DateTimeRange.razor.cs +++ b/src/BootstrapBlazor/Components/DateTimeRange/DateTimeRange.razor.cs @@ -554,10 +554,6 @@ private async Task UpdateValue(DateTime d) { await ClickConfirmButton(); } - else - { - StateHasChanged(); - } } /// From 7f9feb467d0830b9667974d79e6830c8ec209b56 Mon Sep 17 00:00:00 2001 From: Argo Zhang Date: Tue, 1 Sep 2026 14:32:54 +0800 Subject: [PATCH 04/23] =?UTF-8?q?refactor(FilterProvider):=20=E7=A7=BB?= =?UTF-8?q?=E9=99=A4=E5=86=97=E4=BD=99=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/BootstrapBlazor/Components/Filters/FilterProvider.razor.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/BootstrapBlazor/Components/Filters/FilterProvider.razor.cs b/src/BootstrapBlazor/Components/Filters/FilterProvider.razor.cs index 74ce7e4f906..25b49c6c3b6 100644 --- a/src/BootstrapBlazor/Components/Filters/FilterProvider.razor.cs +++ b/src/BootstrapBlazor/Components/Filters/FilterProvider.razor.cs @@ -122,7 +122,6 @@ private async Task OnClickReset() { await TableColumnFilter.Reset(); } - StateHasChanged(); } /// From a7133a2a8cbfb72cc34f1ebcc787786aee515593 Mon Sep 17 00:00:00 2001 From: Argo Zhang Date: Tue, 1 Sep 2026 14:33:36 +0800 Subject: [PATCH 05/23] =?UTF-8?q?refactor(Layout):=20=E7=A7=BB=E9=99=A4?= =?UTF-8?q?=E5=86=97=E4=BD=99=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/BootstrapBlazor/Components/Layout/Layout.razor.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/BootstrapBlazor/Components/Layout/Layout.razor.cs b/src/BootstrapBlazor/Components/Layout/Layout.razor.cs index 61ba9926700..14ed62408b8 100644 --- a/src/BootstrapBlazor/Components/Layout/Layout.razor.cs +++ b/src/BootstrapBlazor/Components/Layout/Layout.razor.cs @@ -799,7 +799,6 @@ private void OnMouseLeaveSidebar() if (_isSidebarExpand) { _isSidebarExpand = false; - StateHasChanged(); } } From 6dc4dbcab0d9fcc5fdefcc4dcd202cb9b49d0f31 Mon Sep 17 00:00:00 2001 From: Argo Zhang Date: Tue, 1 Sep 2026 14:34:24 +0800 Subject: [PATCH 06/23] =?UTF-8?q?refactor(Layout):=20=E7=A7=BB=E9=99=A4?= =?UTF-8?q?=E5=86=97=E4=BD=99=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/BootstrapBlazor/Components/Layout/Layout.razor.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/BootstrapBlazor/Components/Layout/Layout.razor.cs b/src/BootstrapBlazor/Components/Layout/Layout.razor.cs index 14ed62408b8..3bd5db33ff5 100644 --- a/src/BootstrapBlazor/Components/Layout/Layout.razor.cs +++ b/src/BootstrapBlazor/Components/Layout/Layout.razor.cs @@ -786,7 +786,6 @@ private void OnClickSidebar() if (IsCollapsed && !IsExpandOnHover && !_isSidebarExpand) { _isSidebarExpand = true; - StateHasChanged(); } } From 671b800a4d901d83c2d3ba4048fbfa30e298d407 Mon Sep 17 00:00:00 2001 From: Argo Zhang Date: Tue, 1 Sep 2026 14:36:28 +0800 Subject: [PATCH 07/23] =?UTF-8?q?refactor(Dropzone):=20=E7=A7=BB=E9=99=A4?= =?UTF-8?q?=E5=86=97=E4=BD=99=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/BootstrapBlazor/Components/DragDrap/Dropzone.razor.cs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/BootstrapBlazor/Components/DragDrap/Dropzone.razor.cs b/src/BootstrapBlazor/Components/DragDrap/Dropzone.razor.cs index 63aea6e087e..69a406754a4 100644 --- a/src/BootstrapBlazor/Components/DragDrap/Dropzone.razor.cs +++ b/src/BootstrapBlazor/Components/DragDrap/Dropzone.razor.cs @@ -277,7 +277,6 @@ private void OnDragEnd() //DragDropService.Commit(); } } - StateHasChanged(); } Items.Remove(default!); } @@ -305,14 +304,11 @@ private void OnDragEnter(TItem? item) } DragDropService.DragTargetItem = item; - - StateHasChanged(); } private void OnDragLeave() { DragDropService.DragTargetItem = default; - StateHasChanged(); } private void OnDrop() @@ -357,7 +353,6 @@ private void OnDrop() } } - StateHasChanged(); OnItemDrop.InvokeAsync(activeItem); } From e6130f1bee0afc5374ae7d9daca2edbed1f17932 Mon Sep 17 00:00:00 2001 From: Argo Zhang Date: Tue, 1 Sep 2026 15:21:28 +0800 Subject: [PATCH 08/23] =?UTF-8?q?refactor:=20=E9=87=8D=E6=9E=84=20Validate?= =?UTF-8?q?Async=20=E6=96=B9=E6=B3=95=E5=B9=B6=E4=B8=94=E6=9B=B4=E6=96=B0?= =?UTF-8?q?=E5=8D=95=E5=85=83=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../BootstrapBlazorDataAnnotationsValidator.cs | 16 ++++++++-------- .../ValidateForm/ValidateForm.razor.cs | 12 ++++-------- test/UnitTest/Components/DateTimePickerTest.cs | 2 +- test/UnitTest/Components/DateTimeRangeTest.cs | 4 ++-- test/UnitTest/Components/InputNumberTest.cs | 6 +++--- 5 files changed, 18 insertions(+), 22 deletions(-) diff --git a/src/BootstrapBlazor/Components/ValidateForm/BootstrapBlazorDataAnnotationsValidator.cs b/src/BootstrapBlazor/Components/ValidateForm/BootstrapBlazorDataAnnotationsValidator.cs index cad577984e6..c801213bec6 100644 --- a/src/BootstrapBlazor/Components/ValidateForm/BootstrapBlazorDataAnnotationsValidator.cs +++ b/src/BootstrapBlazor/Components/ValidateForm/BootstrapBlazorDataAnnotationsValidator.cs @@ -51,10 +51,13 @@ protected override void OnInitialized() AddEditContextDataAnnotationsValidation(); } +#if NET11_0_OR_GREATER + internal Task ValidateAsync(CancellationToken cancellationToken = default) => CurrentEditContext.ValidateAsync(cancellationToken); +#else private TaskCompletionSource? _tcs; /// /// 手动验证表单方法 - /// 手动验证表单方法 + /// Manually validate the form /// internal async Task ValidateAsync() { @@ -63,13 +66,7 @@ internal async Task ValidateAsync() var valid = await _tcs.Task; return ret && valid; } - - /// - /// 手动验证表单方法 - /// 手动验证表单方法 - /// - [ExcludeFromCodeCoverage] - internal bool Validate() => CurrentEditContext.Validate(); +#endif private void AddEditContextDataAnnotationsValidation() { @@ -112,10 +109,13 @@ private async Task ValidateModel(EditContext editContext, ValidationMessageStore } editContext.NotifyValidationStateChanged(); +#if NET11_0_OR_GREATER +#else if (_tcs != null) { _tcs.TrySetResult(validationResults.Count == 0); } +#endif } private async Task ValidateField(EditContext editContext, ValidationMessageStore messages, FieldIdentifier field, IServiceProvider provider) diff --git a/src/BootstrapBlazor/Components/ValidateForm/ValidateForm.razor.cs b/src/BootstrapBlazor/Components/ValidateForm/ValidateForm.razor.cs index 56d7b84ba19..a19f2e39eb4 100644 --- a/src/BootstrapBlazor/Components/ValidateForm/ValidateForm.razor.cs +++ b/src/BootstrapBlazor/Components/ValidateForm/ValidateForm.razor.cs @@ -662,19 +662,15 @@ private async Task OnInvalidSubmitForm(EditContext context) private EditContext? _formlessEditContext; - /// - /// 同步验证方法 用于代码调用触发表单验证(不支持某些组件的异步验证) - /// Synchronous validation method used to trigger form validation via code (does not support asynchronous validation for some components) - /// - [Obsolete("已弃用,请使用 ValidateAsync 方法。Deprecated. Please use the ValidateAsync method.")] - [ExcludeFromCodeCoverage] - public bool Validate() => Validator.Validate(); - /// /// 异步验证方法 用于代码调用触发表单验证(支持异步验证) /// Asynchronous validation method used to trigger form validation via code (supports asynchronous validation) /// +#if NET11_0_OR_GREATER + public Task ValidateAsync(CancellationToken cancellationToken = default) => Validator.ValidateAsync(cancellationToken); +#else public Task ValidateAsync() => Validator.ValidateAsync(); +#endif /// /// 通知属性改变方法 diff --git a/test/UnitTest/Components/DateTimePickerTest.cs b/test/UnitTest/Components/DateTimePickerTest.cs index e8dbab390f6..f33f9d0f743 100644 --- a/test/UnitTest/Components/DateTimePickerTest.cs +++ b/test/UnitTest/Components/DateTimePickerTest.cs @@ -1320,7 +1320,7 @@ await cut.InvokeAsync(() => Assert.Equal("02/15/2024", model.DateTime.Value.ToString("MM/dd/yyyy")); // 录入非法数值 Validate 通过 - var valid = await cut.InvokeAsync(cut.Instance.ValidateAsync); + var valid = await cut.InvokeAsync(() => cut.Instance.ValidateAsync()); Assert.True(valid); } diff --git a/test/UnitTest/Components/DateTimeRangeTest.cs b/test/UnitTest/Components/DateTimeRangeTest.cs index 3fb41e13d5a..31721f44442 100644 --- a/test/UnitTest/Components/DateTimeRangeTest.cs +++ b/test/UnitTest/Components/DateTimeRangeTest.cs @@ -479,7 +479,7 @@ public async Task InValidateForm_Ok() cut.Contains("class=\"form-label\""); // 验证 - var validate = await cut.InvokeAsync(cut.Instance.ValidateAsync); + var validate = await cut.InvokeAsync(() => cut.Instance.ValidateAsync()); Assert.False(validate); var range = cut.FindComponent(); @@ -503,7 +503,7 @@ public async Task InValidateForm_Ok() { pb.Add(a => a.Model, foo); }); - validate = await cut.InvokeAsync(cut.Instance.ValidateAsync); + validate = await cut.InvokeAsync(() => cut.Instance.ValidateAsync()); Assert.True(validate); } diff --git a/test/UnitTest/Components/InputNumberTest.cs b/test/UnitTest/Components/InputNumberTest.cs index 99b16776498..cf4f898cde5 100644 --- a/test/UnitTest/Components/InputNumberTest.cs +++ b/test/UnitTest/Components/InputNumberTest.cs @@ -413,7 +413,7 @@ await cut.InvokeAsync(() => }); Assert.Equal(1, model.Count); - var valid = await cut.InvokeAsync(cut.Instance.ValidateAsync); + var valid = await cut.InvokeAsync(() => cut.Instance.ValidateAsync()); Assert.False(valid); await cut.InvokeAsync(() => @@ -421,7 +421,7 @@ await cut.InvokeAsync(() => input.Change("t2"); }); Assert.Equal(1, model.Count); - valid = await cut.InvokeAsync(cut.Instance.ValidateAsync); + valid = await cut.InvokeAsync(() => cut.Instance.ValidateAsync()); Assert.False(valid); await cut.InvokeAsync(() => @@ -430,7 +430,7 @@ await cut.InvokeAsync(() => }); Assert.Equal(2, model.Count); - valid = await cut.InvokeAsync(cut.Instance.ValidateAsync); + valid = await cut.InvokeAsync(() => cut.Instance.ValidateAsync()); Assert.True(valid); } From 5d9ddc0591a2ef5132b8b83a2979d4ab995bd313 Mon Sep 17 00:00:00 2001 From: Argo Zhang Date: Tue, 1 Sep 2026 15:21:46 +0800 Subject: [PATCH 09/23] =?UTF-8?q?chore:=20=E6=9B=B4=E6=96=B0=20github=20ac?= =?UTF-8?q?tion=20=E8=BF=90=E8=A1=8C=E6=97=B6=E6=A1=86=E6=9E=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/auto-pull-request-checks.yml | 2 +- .github/workflows/build.yml | 2 +- .github/workflows/pack.yml | 2 +- .github/workflows/publish.yml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/auto-pull-request-checks.yml b/.github/workflows/auto-pull-request-checks.yml index 9884c1c6be1..a3f8a4f57f6 100644 --- a/.github/workflows/auto-pull-request-checks.yml +++ b/.github/workflows/auto-pull-request-checks.yml @@ -17,7 +17,7 @@ jobs: - name: Setup .NET SDK uses: actions/setup-dotnet@v4 with: - dotnet-version: 10.0.x + global-json-file: global.json - name: Cache NuGet packages uses: actions/cache@v4 diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d6060be9b53..05ef1b0e101 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -16,7 +16,7 @@ jobs: - name: Setup .NET Core SDK uses: actions/setup-dotnet@v4 with: - dotnet-version: 10.0.x + global-json-file: global.json - name: Cache NuGet packages uses: actions/cache@v4 diff --git a/.github/workflows/pack.yml b/.github/workflows/pack.yml index b20fdb6f686..5acf38d3fd0 100644 --- a/.github/workflows/pack.yml +++ b/.github/workflows/pack.yml @@ -20,7 +20,7 @@ jobs: - name: Setup .NET Core SDK uses: actions/setup-dotnet@v4 with: - dotnet-version: 10.0.x + global-json-file: global.json - name: Cache NuGet packages uses: actions/cache@v4 diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index c2789f8cc91..874a103eb6e 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -15,7 +15,7 @@ jobs: - name: Setup .NET Core SDK uses: actions/setup-dotnet@v4 with: - dotnet-version: 10.0.x + global-json-file: global.json - name: Publish wasm run: | From 3eb1209ab4199dfe10c879e19d18979268447dc6 Mon Sep 17 00:00:00 2001 From: Argo Zhang Date: Tue, 1 Sep 2026 15:24:01 +0800 Subject: [PATCH 10/23] =?UTF-8?q?chore:=20=E6=9B=B4=E6=96=B0=20Dockfile=20?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/BootstrapBlazor.Server/Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/BootstrapBlazor.Server/Dockerfile b/src/BootstrapBlazor.Server/Dockerfile index 881334f77dc..645f4a4b5a3 100644 --- a/src/BootstrapBlazor.Server/Dockerfile +++ b/src/BootstrapBlazor.Server/Dockerfile @@ -1,7 +1,7 @@ #Depending on the operating system of the host machines(s) that will build or run the containers, the image specified in the FROM statement may need to be changed. #For more information, please see https://aka.ms/containercompat -FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base +FROM mcr.microsoft.com/dotnet/aspnet:11.0-preview AS base WORKDIR /app EXPOSE 8080 @@ -11,7 +11,7 @@ RUN apt install -y ./google-chrome-stable_current_amd64.deb RUN rm ./google-chrome-stable_current_amd64.deb RUN apt install -y fonts-wqy-microhei -FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build +FROM mcr.microsoft.com/dotnet/sdk:11.0-preview AS build WORKDIR / COPY . . From 05c150e4e6d2448cfc6368f2e7716ba05c2d8d4e Mon Sep 17 00:00:00 2001 From: Argo Zhang Date: Tue, 1 Sep 2026 15:54:55 +0800 Subject: [PATCH 11/23] =?UTF-8?q?test:=20=E8=A1=A8=E5=8D=95=E6=8F=90?= =?UTF-8?q?=E4=BA=A4=E8=B7=9F=E6=94=B9=E4=B8=BA=E5=BC=82=E6=AD=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/UnitTest/Components/ValidateTest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/UnitTest/Components/ValidateTest.cs b/test/UnitTest/Components/ValidateTest.cs index 79dba9043d7..06005f3b34d 100644 --- a/test/UnitTest/Components/ValidateTest.cs +++ b/test/UnitTest/Components/ValidateTest.cs @@ -407,7 +407,7 @@ public async Task AsyncRules_Ok() var c = cut.Find("input"); await cut.InvokeAsync(() => c.Change("Test")); await Task.Delay(300, CancellationToken.None); - await cut.InvokeAsync(() => form.Submit()); + await cut.InvokeAsync(() => form.SubmitAsync()); Assert.True(invalid); } From 6bbf4d21d32acca9d57c1e0b9c169dd661a72586 Mon Sep 17 00:00:00 2001 From: Argo Zhang Date: Tue, 1 Sep 2026 15:55:17 +0800 Subject: [PATCH 12/23] =?UTF-8?q?test:=20=E5=8F=B3=E9=94=AE=E8=8F=9C?= =?UTF-8?q?=E5=8D=95=E6=9B=B4=E6=94=B9=E4=B8=BA=E5=BC=82=E6=AD=A5=E8=AF=B7?= =?UTF-8?q?=E6=B1=82=E6=96=B9=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/UnitTest/Components/ContextMenuTest.cs | 6 +++--- test/UnitTest/Components/LayoutTest.cs | 4 ++-- test/UnitTest/Components/TabTest.cs | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/test/UnitTest/Components/ContextMenuTest.cs b/test/UnitTest/Components/ContextMenuTest.cs index f0e954d7608..60bbe91cc91 100644 --- a/test/UnitTest/Components/ContextMenuTest.cs +++ b/test/UnitTest/Components/ContextMenuTest.cs @@ -52,7 +52,7 @@ public async Task ContextMenu_Ok() }); var row = cut.Find(".context-trigger"); - row.ContextMenu(new MouseEventArgs() { Detail = 0, ScreenX = 10, ScreenY = 10, ClientX = 10, ClientY = 10, Button = 2, Buttons = 2 }); + await row.ContextMenuAsync(new MouseEventArgs() { Detail = 0, ScreenX = 10, ScreenY = 10, ClientX = 10, ClientY = 10, Button = 2, Buttons = 2 }); var menu = cut.FindComponent(); menu.Contains("shadow"); @@ -232,7 +232,7 @@ public async Task ContextMenu_Table(TableRenderMode renderMode) }); var row = renderMode == TableRenderMode.CardView ? cut.Find(".table-row") : cut.Find("tbody tr"); - row.ContextMenu(new MouseEventArgs() { Detail = 0, ScreenX = 10, ScreenY = 10, ClientX = 10, ClientY = 10, Button = 2, Buttons = 2 }); + await row.ContextMenuAsync(new MouseEventArgs() { Detail = 0, ScreenX = 10, ScreenY = 10, ClientX = 10, ClientY = 10, Button = 2, Buttons = 2 }); var menu = cut.FindComponent(); var pi = typeof(ContextMenu).GetField("_contextItem", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); @@ -295,7 +295,7 @@ public async Task ContextMenu_TreeView() }); var row = cut.Find(".tree-content"); - row.ContextMenu(new MouseEventArgs() { Detail = 0, ScreenX = 10, ScreenY = 10, ClientX = 10, ClientY = 10, Button = 2, Buttons = 2 }); + await row.ContextMenuAsync(new MouseEventArgs() { Detail = 0, ScreenX = 10, ScreenY = 10, ClientX = 10, ClientY = 10, Button = 2, Buttons = 2 }); var menu = cut.FindComponent(); var pi = typeof(ContextMenu).GetField("_contextItem", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); diff --git a/test/UnitTest/Components/LayoutTest.cs b/test/UnitTest/Components/LayoutTest.cs index fd9032dbcfa..3fc4f7498fc 100644 --- a/test/UnitTest/Components/LayoutTest.cs +++ b/test/UnitTest/Components/LayoutTest.cs @@ -68,7 +68,7 @@ public async Task TabStyle_Ok() // test context menu onclick event handler var tab = cut.Find(".tabs-item"); - await cut.InvokeAsync(() => tab.ContextMenu()); + await tab.ContextMenuAsync(); var buttons = cut.FindAll(".bb-cm-zone > .dropdown-menu .dropdown-item"); foreach (var button in buttons) @@ -89,7 +89,7 @@ public async Task TabStyle_Ok() return Task.FromResult(true); }); }); - await cut.InvokeAsync(() => tab.ContextMenu()); + await tab.ContextMenuAsync(); Assert.True(show); } diff --git a/test/UnitTest/Components/TabTest.cs b/test/UnitTest/Components/TabTest.cs index 18b2df71b96..0c7230d89f3 100644 --- a/test/UnitTest/Components/TabTest.cs +++ b/test/UnitTest/Components/TabTest.cs @@ -47,7 +47,7 @@ public async Task ContextMenu_Ok() }); var menuItem = cut.Find(".tabs-item"); - await cut.InvokeAsync(() => menuItem.ContextMenu()); + await menuItem.ContextMenuAsync(); var item = cut.Find(".dropdown-menu .dropdown-item"); Assert.NotNull(item); From 1a906764aa00405af117da8c7e4deecc030e96fa Mon Sep 17 00:00:00 2001 From: Argo Zhang Date: Wed, 2 Sep 2026 10:15:38 +0800 Subject: [PATCH 13/23] =?UTF-8?q?Revert=20"refactor:=20=E9=87=8D=E6=9E=84?= =?UTF-8?q?=20ValidateAsync=20=E6=96=B9=E6=B3=95=E5=B9=B6=E4=B8=94?= =?UTF-8?q?=E6=9B=B4=E6=96=B0=E5=8D=95=E5=85=83=E6=B5=8B=E8=AF=95"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit e6130f1bee0afc5374ae7d9daca2edbed1f17932. # Conflicts: # src/BootstrapBlazor/Components/ValidateForm/BootstrapBlazorDataAnnotationsValidator.cs --- .../BootstrapBlazorDataAnnotationsValidator.cs | 14 +++++++------- .../Components/ValidateForm/ValidateForm.razor.cs | 12 ++++++++---- test/UnitTest/Components/DateTimePickerTest.cs | 2 +- test/UnitTest/Components/DateTimeRangeTest.cs | 4 ++-- test/UnitTest/Components/InputNumberTest.cs | 6 +++--- 5 files changed, 21 insertions(+), 17 deletions(-) diff --git a/src/BootstrapBlazor/Components/ValidateForm/BootstrapBlazorDataAnnotationsValidator.cs b/src/BootstrapBlazor/Components/ValidateForm/BootstrapBlazorDataAnnotationsValidator.cs index ae9c8520ec0..e5653ce0fff 100644 --- a/src/BootstrapBlazor/Components/ValidateForm/BootstrapBlazorDataAnnotationsValidator.cs +++ b/src/BootstrapBlazor/Components/ValidateForm/BootstrapBlazorDataAnnotationsValidator.cs @@ -51,9 +51,6 @@ protected override void OnInitialized() AddEditContextDataAnnotationsValidation(); } -#if NET11_0_OR_GREATER - internal Task ValidateAsync(CancellationToken cancellationToken = default) => CurrentEditContext.ValidateAsync(cancellationToken); -#else private TaskCompletionSource? _tcs; /// /// 手动验证表单方法 @@ -66,7 +63,13 @@ internal async Task ValidateAsync() var valid = await _tcs.Task; return ret && valid; } -#endif + + /// + /// 手动验证表单方法 + /// 手动验证表单方法 + /// + [ExcludeFromCodeCoverage] + internal bool Validate() => CurrentEditContext.Validate(); private void AddEditContextDataAnnotationsValidation() { @@ -109,13 +112,10 @@ private async Task ValidateModel(EditContext editContext, ValidationMessageStore } editContext.NotifyValidationStateChanged(); -#if NET11_0_OR_GREATER -#else if (_tcs != null) { _tcs.TrySetResult(validationResults.Count == 0); } -#endif } private async Task ValidateField(EditContext editContext, ValidationMessageStore messages, FieldIdentifier field, IServiceProvider provider) diff --git a/src/BootstrapBlazor/Components/ValidateForm/ValidateForm.razor.cs b/src/BootstrapBlazor/Components/ValidateForm/ValidateForm.razor.cs index a19f2e39eb4..56d7b84ba19 100644 --- a/src/BootstrapBlazor/Components/ValidateForm/ValidateForm.razor.cs +++ b/src/BootstrapBlazor/Components/ValidateForm/ValidateForm.razor.cs @@ -662,15 +662,19 @@ private async Task OnInvalidSubmitForm(EditContext context) private EditContext? _formlessEditContext; + /// + /// 同步验证方法 用于代码调用触发表单验证(不支持某些组件的异步验证) + /// Synchronous validation method used to trigger form validation via code (does not support asynchronous validation for some components) + /// + [Obsolete("已弃用,请使用 ValidateAsync 方法。Deprecated. Please use the ValidateAsync method.")] + [ExcludeFromCodeCoverage] + public bool Validate() => Validator.Validate(); + /// /// 异步验证方法 用于代码调用触发表单验证(支持异步验证) /// Asynchronous validation method used to trigger form validation via code (supports asynchronous validation) /// -#if NET11_0_OR_GREATER - public Task ValidateAsync(CancellationToken cancellationToken = default) => Validator.ValidateAsync(cancellationToken); -#else public Task ValidateAsync() => Validator.ValidateAsync(); -#endif /// /// 通知属性改变方法 diff --git a/test/UnitTest/Components/DateTimePickerTest.cs b/test/UnitTest/Components/DateTimePickerTest.cs index f33f9d0f743..e8dbab390f6 100644 --- a/test/UnitTest/Components/DateTimePickerTest.cs +++ b/test/UnitTest/Components/DateTimePickerTest.cs @@ -1320,7 +1320,7 @@ await cut.InvokeAsync(() => Assert.Equal("02/15/2024", model.DateTime.Value.ToString("MM/dd/yyyy")); // 录入非法数值 Validate 通过 - var valid = await cut.InvokeAsync(() => cut.Instance.ValidateAsync()); + var valid = await cut.InvokeAsync(cut.Instance.ValidateAsync); Assert.True(valid); } diff --git a/test/UnitTest/Components/DateTimeRangeTest.cs b/test/UnitTest/Components/DateTimeRangeTest.cs index 31721f44442..3fb41e13d5a 100644 --- a/test/UnitTest/Components/DateTimeRangeTest.cs +++ b/test/UnitTest/Components/DateTimeRangeTest.cs @@ -479,7 +479,7 @@ public async Task InValidateForm_Ok() cut.Contains("class=\"form-label\""); // 验证 - var validate = await cut.InvokeAsync(() => cut.Instance.ValidateAsync()); + var validate = await cut.InvokeAsync(cut.Instance.ValidateAsync); Assert.False(validate); var range = cut.FindComponent(); @@ -503,7 +503,7 @@ public async Task InValidateForm_Ok() { pb.Add(a => a.Model, foo); }); - validate = await cut.InvokeAsync(() => cut.Instance.ValidateAsync()); + validate = await cut.InvokeAsync(cut.Instance.ValidateAsync); Assert.True(validate); } diff --git a/test/UnitTest/Components/InputNumberTest.cs b/test/UnitTest/Components/InputNumberTest.cs index cf4f898cde5..99b16776498 100644 --- a/test/UnitTest/Components/InputNumberTest.cs +++ b/test/UnitTest/Components/InputNumberTest.cs @@ -413,7 +413,7 @@ await cut.InvokeAsync(() => }); Assert.Equal(1, model.Count); - var valid = await cut.InvokeAsync(() => cut.Instance.ValidateAsync()); + var valid = await cut.InvokeAsync(cut.Instance.ValidateAsync); Assert.False(valid); await cut.InvokeAsync(() => @@ -421,7 +421,7 @@ await cut.InvokeAsync(() => input.Change("t2"); }); Assert.Equal(1, model.Count); - valid = await cut.InvokeAsync(() => cut.Instance.ValidateAsync()); + valid = await cut.InvokeAsync(cut.Instance.ValidateAsync); Assert.False(valid); await cut.InvokeAsync(() => @@ -430,7 +430,7 @@ await cut.InvokeAsync(() => }); Assert.Equal(2, model.Count); - valid = await cut.InvokeAsync(() => cut.Instance.ValidateAsync()); + valid = await cut.InvokeAsync(cut.Instance.ValidateAsync); Assert.True(valid); } From 41517f909285c55918d5c3257f9cc953f8c55e37 Mon Sep 17 00:00:00 2001 From: Argo Zhang Date: Wed, 2 Sep 2026 10:20:07 +0800 Subject: [PATCH 14/23] =?UTF-8?q?revert:=20=E6=92=A4=E9=94=80=E6=B3=A8?= =?UTF-8?q?=E9=87=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ValidateForm/BootstrapBlazorDataAnnotationsValidator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/BootstrapBlazor/Components/ValidateForm/BootstrapBlazorDataAnnotationsValidator.cs b/src/BootstrapBlazor/Components/ValidateForm/BootstrapBlazorDataAnnotationsValidator.cs index e5653ce0fff..d1c88e0097a 100644 --- a/src/BootstrapBlazor/Components/ValidateForm/BootstrapBlazorDataAnnotationsValidator.cs +++ b/src/BootstrapBlazor/Components/ValidateForm/BootstrapBlazorDataAnnotationsValidator.cs @@ -66,7 +66,7 @@ internal async Task ValidateAsync() /// /// 手动验证表单方法 - /// 手动验证表单方法 + /// Validates the form manually /// [ExcludeFromCodeCoverage] internal bool Validate() => CurrentEditContext.Validate(); From 3cf0572cbd5400bafbdffeae45b8ac6109b1612d Mon Sep 17 00:00:00 2001 From: Argo Zhang Date: Fri, 4 Sep 2026 08:13:43 +0800 Subject: [PATCH 15/23] =?UTF-8?q?doc:=20=E6=9B=B4=E6=96=B0=E6=B3=A8?= =?UTF-8?q?=E9=87=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/BootstrapBlazor/Components/Block/Block.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/BootstrapBlazor/Components/Block/Block.cs b/src/BootstrapBlazor/Components/Block/Block.cs index 24b9664a673..97c7d7cfab0 100644 --- a/src/BootstrapBlazor/Components/Block/Block.cs +++ b/src/BootstrapBlazor/Components/Block/Block.cs @@ -77,8 +77,7 @@ public class Block : BootstrapComponentBase private bool IsShow { get; set; } /// - /// OnParametersSetAsync 方法 - /// OnParametersSetAsync method + /// /// protected override async Task OnParametersSetAsync() { From c30b044d3544453e33f7f0a6ed4892145099af3f Mon Sep 17 00:00:00 2001 From: Argo Zhang Date: Fri, 4 Sep 2026 08:22:16 +0800 Subject: [PATCH 16/23] =?UTF-8?q?refactor:=20=E6=9B=B4=E6=96=B0=E8=A7=A3?= =?UTF-8?q?=E5=86=B3=E6=96=B9=E6=A1=88=E9=85=8D=E7=BD=AE=E9=A1=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BootstrapBlazor.slnx | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/BootstrapBlazor.slnx b/BootstrapBlazor.slnx index 3bb85ca0611..ebaf4001709 100644 --- a/BootstrapBlazor.slnx +++ b/BootstrapBlazor.slnx @@ -6,12 +6,6 @@ - - - - - - @@ -28,11 +22,6 @@ - - - - - @@ -60,20 +49,12 @@ - - - - - - - - From 4e59705f8a422e1e9b23db6597c31a5ddfdcf1f3 Mon Sep 17 00:00:00 2001 From: Argo Zhang Date: Fri, 4 Sep 2026 08:38:34 +0800 Subject: [PATCH 17/23] feat(IValidateComponent): add SetValidationPendingState method --- .../Components/Upload/AvatarUpload.razor.cs | 1 + .../Components/Upload/CardUpload.razor.cs | 1 + .../Components/Validate/IValidateComponent.cs | 6 ++ .../Components/Validate/ValidateBase.cs | 24 +++++++- .../ValidateForm/ValidateForm.razor.cs | 4 +- .../ValidateForm/ValidateForm.razor.scss | 17 ++++++ test/UnitTest/Components/ValidateFormTest.cs | 60 +++++++++++++++++++ 7 files changed, 110 insertions(+), 3 deletions(-) diff --git a/src/BootstrapBlazor/Components/Upload/AvatarUpload.razor.cs b/src/BootstrapBlazor/Components/Upload/AvatarUpload.razor.cs index 01b1716d17d..59d505f1813 100644 --- a/src/BootstrapBlazor/Components/Upload/AvatarUpload.razor.cs +++ b/src/BootstrapBlazor/Components/Upload/AvatarUpload.razor.cs @@ -191,6 +191,7 @@ private async Task Preview(UploadFile file) public override async Task ToggleMessage(IReadOnlyCollection results) { _results = results; + IsValidating = false; IsValid = results.Count == 0; ValidateModule ??= await LoadValidateModule(); diff --git a/src/BootstrapBlazor/Components/Upload/CardUpload.razor.cs b/src/BootstrapBlazor/Components/Upload/CardUpload.razor.cs index 0ca20e7cae6..bfca2035d09 100644 --- a/src/BootstrapBlazor/Components/Upload/CardUpload.razor.cs +++ b/src/BootstrapBlazor/Components/Upload/CardUpload.razor.cs @@ -252,6 +252,7 @@ protected override async Task TriggerOnChanged(UploadFile file) public override async Task ToggleMessage(IReadOnlyCollection results) { _results = results; + IsValidating = false; IsValid = results.Count == 0; ValidateModule ??= await LoadValidateModule(); diff --git a/src/BootstrapBlazor/Components/Validate/IValidateComponent.cs b/src/BootstrapBlazor/Components/Validate/IValidateComponent.cs index bb3bf0f8bad..304e3ba351a 100644 --- a/src/BootstrapBlazor/Components/Validate/IValidateComponent.cs +++ b/src/BootstrapBlazor/Components/Validate/IValidateComponent.cs @@ -33,6 +33,12 @@ public interface IValidateComponent /// Task ValidatePropertyAsync(object? propertyValue, ValidationContext context, List results, CancellationToken cancellationToken = default); + /// + /// 设置等待验证状态方法 + /// Sets the pending validation state + /// + Task SetValidationPendingState(); + /// /// 显示或者隐藏提示信息方法 /// Shows or hides the validation message diff --git a/src/BootstrapBlazor/Components/Validate/ValidateBase.cs b/src/BootstrapBlazor/Components/Validate/ValidateBase.cs index a033890e3ba..ab8f98c0df8 100644 --- a/src/BootstrapBlazor/Components/Validate/ValidateBase.cs +++ b/src/BootstrapBlazor/Components/Validate/ValidateBase.cs @@ -45,7 +45,7 @@ public abstract class ValidateBase : DisplayBase, IValidateCompo /// 获得 数据合规样式 /// Gets the validation CSS class /// - protected string? ValidCss => IsValid.HasValue ? GetValidString(IsValid.Value) : null; + protected string? ValidCss => IsValidating ? "is-validating" : IsValid.HasValue ? GetValidString(IsValid.Value) : null; private static string GetValidString(bool valid) => valid ? "is-valid" : "is-invalid"; @@ -55,6 +55,12 @@ public abstract class ValidateBase : DisplayBase, IValidateCompo /// protected bool? IsValid { get; set; } + /// + /// 获得/设置 组件是否正在验证 + /// Gets or sets whether the component is being validated + /// + protected bool IsValidating { get; set; } + /// /// 获得 组件是否被禁用属性值 /// Gets the disabled attribute value of the component @@ -513,6 +519,21 @@ private void ValidateType(ValidationContext context, List resu private bool? _shouldRender = null; + /// + /// + /// + public virtual async Task SetValidationPendingState() + { + ErrorMessage = null; + IsValid = null; + IsValidating = true; + OnValidate(IsValid); + + _shouldRender = true; + StateHasChanged(); + await RemoveValidResult(); + } + /// /// 显示/隐藏验证结果方法 /// Shows or hides the validation result message @@ -520,6 +541,7 @@ private void ValidateType(ValidationContext context, List resu /// public virtual Task ToggleMessage(IReadOnlyCollection results) { + IsValidating = false; if (FieldIdentifier != null) { var messages = results.Where(item => item.MemberNames.Any(m => m == FieldIdentifier.Value.FieldName)).ToList(); diff --git a/src/BootstrapBlazor/Components/ValidateForm/ValidateForm.razor.cs b/src/BootstrapBlazor/Components/ValidateForm/ValidateForm.razor.cs index 8bfc855f842..60028ae482f 100644 --- a/src/BootstrapBlazor/Components/ValidateForm/ValidateForm.razor.cs +++ b/src/BootstrapBlazor/Components/ValidateForm/ValidateForm.razor.cs @@ -315,7 +315,7 @@ private async Task ValidateObjectCoreAsync(ValidationContext context, List>().Instance.GetErrorMessage()); } + [Fact] + public async Task AsyncValidationAttribute_PendingState() + { + var model = new MockPendingAsyncValidationModel(); + var cut = Context.Render(pb => + { + pb.Add(a => a.Model, model); + pb.AddChildContent>(pb => + { + pb.Add(a => a.Value, model.Name); + pb.Add(a => a.ValueChanged, value => model.Name = value); + pb.Add(a => a.ValueExpression, Utility.GenerateValueExpression(model, nameof(model.Name), typeof(string))); + }); + }); + var input = cut.Find("input"); + + await cut.InvokeAsync(() => input.Change("Blazor")); + await model.ValidationStarted.Task.WaitAsync(Xunit.TestContext.Current.CancellationToken); + + Assert.Null(cut.FindComponent>().Instance.GetValidationState()); + Assert.Contains("is-validating", input.ClassList); + + model.ContinueValidation.TrySetResult(); + cut.WaitForAssertion(() => + { + Assert.False(cut.FindComponent>().Instance.GetValidationState()); + Assert.DoesNotContain("is-validating", input.ClassList); + Assert.Contains("is-invalid", input.ClassList); + }, TimeSpan.FromSeconds(1)); + } + [Fact] public async Task IAsyncValidatableObject_ModelError() { @@ -1245,6 +1276,33 @@ private sealed class MockAsyncValidationAttribute : AsyncValidationAttribute } } + private sealed class MockPendingAsyncValidationModel + { + [MockPendingAsyncValidation] + public string? Name { get; set; } + + public TaskCompletionSource ValidationStarted { get; } = new(TaskCreationOptions.RunContinuationsAsynchronously); + + public TaskCompletionSource ContinueValidation { get; } = new(TaskCreationOptions.RunContinuationsAsynchronously); + } + + private sealed class MockPendingAsyncValidationAttribute : AsyncValidationAttribute + { + protected override ValidationResult? IsValid(object? value, ValidationContext validationContext) + => throw new InvalidOperationException("The synchronous validation path should not be used."); + + protected override async Task IsValidAsync( + object? value, + ValidationContext validationContext, + CancellationToken cancellationToken) + { + var model = (MockPendingAsyncValidationModel)validationContext.ObjectInstance; + model.ValidationStarted.TrySetResult(); + await model.ContinueValidation.Task.WaitAsync(cancellationToken); + return new ValidationResult("Async attribute validation failed"); + } + } + private sealed class MockAsyncModelError : IAsyncValidatableObject { public string? Name { get; set; } @@ -1327,5 +1385,7 @@ public IEnumerable Validate(ValidationContext validationContex private class MockInput : BootstrapInput { public string? GetErrorMessage() => base.ErrorMessage; + + public bool? GetValidationState() => base.IsValid; } } From 229ee150ce6e74d4e70343896e870c93b8be8026 Mon Sep 17 00:00:00 2001 From: Argo Zhang Date: Fri, 4 Sep 2026 08:41:46 +0800 Subject: [PATCH 18/23] =?UTF-8?q?doc:=20=E5=A2=9E=E5=8A=A0=E5=BC=82?= =?UTF-8?q?=E6=AD=A5=E9=AA=8C=E8=AF=81=E7=A4=BA=E4=BE=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Components/Samples/ValidateForms.razor | 33 +++++++++++++++++ .../Components/Samples/ValidateForms.razor.cs | 36 +++++++++++++++++++ src/BootstrapBlazor.Server/Locales/en-US.json | 5 +++ src/BootstrapBlazor.Server/Locales/zh-CN.json | 5 +++ 4 files changed, 79 insertions(+) diff --git a/src/BootstrapBlazor.Server/Components/Samples/ValidateForms.razor b/src/BootstrapBlazor.Server/Components/Samples/ValidateForms.razor index 71f20feb1cf..9caa9da6e4a 100644 --- a/src/BootstrapBlazor.Server/Components/Samples/ValidateForms.razor +++ b/src/BootstrapBlazor.Server/Components/Samples/ValidateForms.razor @@ -49,6 +49,39 @@ + + +
+

@((MarkupString)Localizer["ValidateFormAsyncValidationDescription"].Value)

+
public sealed class UniqueUserNameAttribute : AsyncValidationAttribute
+{
+    protected override ValidationResult? IsValid(object? value, ValidationContext context)
+        => throw new InvalidOperationException("Synchronous validation is not supported.");
+
+    protected override async Task<ValidationResult?> IsValidAsync(
+        object? value, ValidationContext context, CancellationToken cancellationToken)
+    {
+        await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken);
+        return string.Equals(value as string, "Blazor", StringComparison.OrdinalIgnoreCase)
+            ? new ValidationResult("The user name already exists.")
+            : ValidationResult.Success;
+    }
+}
+
+ + +
+
+ +
+
+
+
+
+
+
+

@Localizer["ValidateFormInnerComponentInnerComponentLabel"]

diff --git a/src/BootstrapBlazor.Server/Components/Samples/ValidateForms.razor.cs b/src/BootstrapBlazor.Server/Components/Samples/ValidateForms.razor.cs index 4d1a25b2916..e8e6ed0152d 100644 --- a/src/BootstrapBlazor.Server/Components/Samples/ValidateForms.razor.cs +++ b/src/BootstrapBlazor.Server/Components/Samples/ValidateForms.razor.cs @@ -13,6 +13,12 @@ namespace BootstrapBlazor.Server.Components.Samples; ///
public partial class ValidateForms { +#if NET11_0_OR_GREATER + private const bool IsAsyncValidationSupported = true; +#else + private const bool IsAsyncValidationSupported = false; +#endif + [NotNull] private Foo? Model1 { get; set; } @@ -36,6 +42,36 @@ private void OnFiledChanged(string field, object? value) Logger1.Log($"{field}:{value}"); } + private AsyncValidationModel AsyncModel { get; } = new() { UserName = "Blazor" }; + + private sealed class AsyncValidationModel + { + [Required] +#if NET11_0_OR_GREATER + [UniqueUserName] +#endif + public string? UserName { get; set; } + } + +#if NET11_0_OR_GREATER + private sealed class UniqueUserNameAttribute : AsyncValidationAttribute + { + protected override ValidationResult? IsValid(object? value, ValidationContext validationContext) + => throw new InvalidOperationException("Synchronous validation is not supported."); + + protected override async Task IsValidAsync( + object? value, + ValidationContext validationContext, + CancellationToken cancellationToken) + { + await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken); + return string.Equals(value as string, "Blazor", StringComparison.OrdinalIgnoreCase) + ? new ValidationResult(validationContext.GetRequiredService>()["AsyncValidationError"]) + : ValidationResult.Success; + } + } +#endif + /// /// /// diff --git a/src/BootstrapBlazor.Server/Locales/en-US.json b/src/BootstrapBlazor.Server/Locales/en-US.json index 2d6e8a7c44a..01692813fd5 100644 --- a/src/BootstrapBlazor.Server/Locales/en-US.json +++ b/src/BootstrapBlazor.Server/Locales/en-US.json @@ -6067,6 +6067,8 @@ "Picture.Required": "{0} is required" }, "BootstrapBlazor.Server.Components.Samples.ValidateForms": { + "AsyncValidationError": "The user name already exists.", + "AsyncValidationUserName": "User name", "ChangeButtonText": "Change", "CustomValidationFormComment1": "Add mailbox validation rules", "CustomValidationFormComment2": "Used in Razor files", @@ -6083,6 +6085,9 @@ "OnValidSubmitStartingLog": "OnValidSubmit callback delegate: Starting ...", "ResetButtonText": "Reset", "ValidateButtonText": "Save", + "ValidateFormAsyncValidationDescription": ".NET 11 introduces AsyncValidationAttribute and IAsyncValidatableObject. Form submission waits for asynchronous validation to complete. This example simulates a database query with a one-second delay. The user name Blazor already exists; enter another value to pass validation. Customize the pending state with --bb-validating-border-color, --bb-validating-animation-duration, and --bb-validating-box-shadow.", + "ValidateFormAsyncValidationIntro": "Use the .NET 11 AsyncValidationAttribute for asynchronous validation such as database queries or remote API calls", + "ValidateFormAsyncValidationTitle": "Asynchronous validation (.NET 11+)", "ValidateFormComplexValidationDescription": "The second binding in this example is a super-complex type ComplexModel.Dummy.Dummy2.Name Empty the value, click submit the formvalidate the data. After the second text box verifies compliance, the error prompt is displayed again by calling the SetError", "ValidateFormComplexValidationIntro": "Supports any complex type of binding and validation", "ValidateFormComplexValidationPre": "ComplexForm.SetError('Dummy.Dummy2.Name','数据库中已存在');", diff --git a/src/BootstrapBlazor.Server/Locales/zh-CN.json b/src/BootstrapBlazor.Server/Locales/zh-CN.json index d7b9ff41435..f11a19ef51b 100644 --- a/src/BootstrapBlazor.Server/Locales/zh-CN.json +++ b/src/BootstrapBlazor.Server/Locales/zh-CN.json @@ -6067,6 +6067,8 @@ "Picture.Required": "上传文件不能为空" }, "BootstrapBlazor.Server.Components.Samples.ValidateForms": { + "AsyncValidationError": "用户名已存在", + "AsyncValidationUserName": "用户名", "ChangeButtonText": "更改组件", "CustomValidationFormComment1": "增加邮箱验证规则", "CustomValidationFormComment2": "Razor 文件中使用", @@ -6083,6 +6085,9 @@ "OnValidSubmitStartingLog": "OnValidSubmit 回调委托: Starting ...", "ResetButtonText": "重置组件", "ValidateButtonText": "保存", + "ValidateFormAsyncValidationDescription": ".NET 11 新增 AsyncValidationAttributeIAsyncValidatableObject,表单提交时会等待异步验证完成。本例延迟一秒模拟查询数据库;用户名 Blazor 已存在,修改为其他值即可通过验证。验证中样式可通过 --bb-validating-border-color--bb-validating-animation-duration--bb-validating-box-shadow 变量自定义。", + "ValidateFormAsyncValidationIntro": "使用 .NET 11 的 AsyncValidationAttribute 执行数据库查询或远程 API 调用等异步验证", + "ValidateFormAsyncValidationTitle": "异步验证(.NET 11+)", "ValidateFormComplexValidationDescription": "本示例中第二个绑定的是一个超级复杂类型 ComplexModel.Dummy.Dummy2.Name 清空值后,点击 提交表单 会对数据进行验证。第二个文本框验证合规后,通过调用 SetError 再次显示错误提示", "ValidateFormComplexValidationIntro": "支持任意复杂类型的绑定与验证", "ValidateFormComplexValidationPre": "ComplexForm.SetError('Dummy.Dummy2.Name','数据库中已存在');", From 70eae94516a82d65f2824c67fdd79d642962b2d3 Mon Sep 17 00:00:00 2001 From: Argo Zhang Date: Fri, 4 Sep 2026 12:47:50 +0800 Subject: [PATCH 19/23] =?UTF-8?q?revert:=20=E6=92=A4=E9=94=80=E6=9B=B4?= =?UTF-8?q?=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Components/Samples/ValidateForms.razor.cs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/BootstrapBlazor.Server/Components/Samples/ValidateForms.razor.cs b/src/BootstrapBlazor.Server/Components/Samples/ValidateForms.razor.cs index 28216a6605f..27d027a006a 100644 --- a/src/BootstrapBlazor.Server/Components/Samples/ValidateForms.razor.cs +++ b/src/BootstrapBlazor.Server/Components/Samples/ValidateForms.razor.cs @@ -13,12 +13,6 @@ namespace BootstrapBlazor.Server.Components.Samples; ///
public partial class ValidateForms { -#if NET11_0_OR_GREATER - private const bool IsAsyncValidationSupported = true; -#else - private const bool IsAsyncValidationSupported = false; -#endif - [NotNull] private Foo? Model1 { get; set; } From 93efceea359a5e77ac4562c518a2099d83e86500 Mon Sep 17 00:00:00 2001 From: Argo Zhang Date: Fri, 4 Sep 2026 13:00:30 +0800 Subject: [PATCH 20/23] =?UTF-8?q?refactor:=20=E6=9B=B4=E6=96=B0=E5=8D=95?= =?UTF-8?q?=E5=85=83=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/UnitTest/Components/ValidateFormTest.cs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/test/UnitTest/Components/ValidateFormTest.cs b/test/UnitTest/Components/ValidateFormTest.cs index 70a18ed97e8..e2721b87c99 100644 --- a/test/UnitTest/Components/ValidateFormTest.cs +++ b/test/UnitTest/Components/ValidateFormTest.cs @@ -80,13 +80,14 @@ public async Task OnValidationRequested_Exception() #if NET11_0_OR_GREATER await cut.InvokeAsync(() => editContext.ValidateAsync(CancellationToken.None)); + Assert.Null(logger.Exception); #else await cut.InvokeAsync(() => editContext.Validate()); -#endif - cut.WaitForAssertion(() => Assert.IsType(logger.Exception)); +#endif } +#if !NET11_0_OR_GREATER [Fact] public async Task ValidateFieldAndCleanupAsync_Cancel() { @@ -105,11 +106,11 @@ public async Task ValidateFieldAndCleanupAsync_Cancel() }); await cut.InvokeAsync(() => cut.Find("input").Change("First")); - await rule.FirstValidationStarted.Task.WaitAsync(Xunit.TestContext.Current.CancellationToken); + await rule.FirstValidationStarted.Task.WaitAsync(CancellationToken.None); await cut.InvokeAsync(() => cut.Find("input").Change("Second")); - await rule.FirstValidationCancelled.Task.WaitAsync(Xunit.TestContext.Current.CancellationToken); - await rule.SecondValidationCompleted.Task.WaitAsync(Xunit.TestContext.Current.CancellationToken); + await rule.FirstValidationCancelled.Task.WaitAsync(CancellationToken.None); + await rule.SecondValidationCompleted.Task.WaitAsync(CancellationToken.None); var validator = cut.FindComponent().Instance; var field = typeof(BootstrapBlazorDataAnnotationsValidator).GetField( @@ -155,6 +156,7 @@ await cut.InvokeAsync(async () => await validation; }); } +#endif [Fact] public async Task Validate_Ok() @@ -914,7 +916,7 @@ public async Task AsyncValidationAttribute_PendingState() var input = cut.Find("input"); await cut.InvokeAsync(() => input.Change("Blazor")); - await model.ValidationStarted.Task.WaitAsync(Xunit.TestContext.Current.CancellationToken); + await model.ValidationStarted.Task.WaitAsync(CancellationToken.None); Assert.Null(cut.FindComponent>().Instance.GetValidationState()); Assert.Contains("is-validating", input.ClassList); From cd029cc7585a5eb08d1eb8b8ad07a9d599846f57 Mon Sep 17 00:00:00 2001 From: Argo Zhang Date: Fri, 4 Sep 2026 14:16:25 +0800 Subject: [PATCH 21/23] =?UTF-8?q?revert:=20=E6=92=A4=E9=94=80=E6=9B=B4?= =?UTF-8?q?=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/UnitTest/Components/ValidateFormTest.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/UnitTest/Components/ValidateFormTest.cs b/test/UnitTest/Components/ValidateFormTest.cs index 18eba6065b5..41f6936171b 100644 --- a/test/UnitTest/Components/ValidateFormTest.cs +++ b/test/UnitTest/Components/ValidateFormTest.cs @@ -87,7 +87,6 @@ public async Task OnValidationRequested_Exception() #endif } -#if !NET11_0_OR_GREATER [Fact] public async Task ValidateFieldAndCleanupAsync_Cancel() { @@ -173,7 +172,6 @@ await cut.InvokeAsync(async () => }); #endif } -#endif [Fact] public async Task Validate_Ok() From 8ba1b752dd22127eded9ce6a0b6916035ae6ed7c Mon Sep 17 00:00:00 2001 From: Argo Zhang Date: Fri, 4 Sep 2026 14:25:26 +0800 Subject: [PATCH 22/23] =?UTF-8?q?test:=20=E6=9B=B4=E6=96=B0=E5=8D=95?= =?UTF-8?q?=E5=85=83=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/UnitTest/Components/ValidateFormTest.cs | 121 +++++++++++++------ 1 file changed, 82 insertions(+), 39 deletions(-) diff --git a/test/UnitTest/Components/ValidateFormTest.cs b/test/UnitTest/Components/ValidateFormTest.cs index 41f6936171b..3cc72780c6c 100644 --- a/test/UnitTest/Components/ValidateFormTest.cs +++ b/test/UnitTest/Components/ValidateFormTest.cs @@ -28,7 +28,7 @@ public void BootstrapBlazorDataAnnotationsValidator_Error() } [Fact] - public async Task OnValidationRequested_Ok() + public async Task ValidateAsync_Invalid() { var foo = new Foo(); var cut = Context.Render(pb => @@ -46,21 +46,15 @@ public async Task OnValidationRequested_Ok() System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic); var editContext = Assert.IsType(property?.GetValue(validator)); -#if NET11_0_OR_GREATER - var valid = await cut.InvokeAsync(() => editContext.ValidateAsync(CancellationToken.None)); - Assert.False(valid); -#else - await cut.InvokeAsync(() => editContext.Validate()); -#endif + var valid = await cut.InvokeAsync(() => cut.Instance.ValidateAsync(CancellationToken.None)); - cut.WaitForAssertion(() => Assert.NotEmpty(editContext.GetValidationMessages())); + Assert.False(valid); + Assert.NotEmpty(editContext.GetValidationMessages()); } [Fact] - public async Task OnValidationRequested_Exception() + public async Task ValidateAsync_Exception() { - var logger = Assert.IsType( - Context.Services.GetRequiredService>()); var foo = new Foo() { Name = "Test" }; var cut = Context.Render(pb => { @@ -72,23 +66,14 @@ public async Task OnValidationRequested_Exception() pb.Add(a => a.ValidateRules, [new ThrowingValidator()]); }); }); - var validator = cut.FindComponent().Instance; - var property = typeof(BootstrapBlazorDataAnnotationsValidator).GetProperty( - "CurrentEditContext", - System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic); - var editContext = Assert.IsType(property?.GetValue(validator)); -#if NET11_0_OR_GREATER - await cut.InvokeAsync(() => editContext.ValidateAsync(CancellationToken.None)); - Assert.Null(logger.Exception); -#else - await cut.InvokeAsync(() => editContext.Validate()); - cut.WaitForAssertion(() => Assert.IsType(logger.Exception)); -#endif + var valid = await cut.InvokeAsync(() => cut.Instance.ValidateAsync(CancellationToken.None)); + + Assert.False(valid); } [Fact] - public async Task ValidateFieldAndCleanupAsync_Cancel() + public async Task FieldValidation_CancelsPreviousOperation() { var rule = new CancellableFieldValidator(); var foo = new Foo() { Name = "Initial" }; @@ -111,22 +96,62 @@ public async Task ValidateFieldAndCleanupAsync_Cancel() await rule.FirstValidationCancelled.Task.WaitAsync(CancellationToken.None); await rule.SecondValidationCompleted.Task.WaitAsync(CancellationToken.None); -#if NET11_0_OR_GREATER Assert.True(rule.FirstValidationCancelled.Task.IsCompletedSuccessfully); Assert.True(rule.SecondValidationCompleted.Task.IsCompletedSuccessfully); -#else + } + + [Fact] + public async Task ValidateAsync_OperationCancellation() + { + var foo = new Foo() { Name = "Test" }; + var cut = Context.Render(pb => + { + pb.Add(a => a.Model, foo); + pb.AddChildContent>(pb => + { + pb.Add(a => a.Value, foo.Name); + pb.Add(a => a.ValueExpression, foo.GenerateValueExpression()); + }); + }); + using var tokenSource = new CancellationTokenSource(); + tokenSource.Cancel(); + + await Assert.ThrowsAnyAsync( + async () => await cut.InvokeAsync(() => cut.Instance.ValidateAsync(tokenSource.Token))); + } + + [Fact] + public async Task OnValidationRequested_Ok() + { + var foo = new Foo(); + var cut = Context.Render(pb => + { + pb.Add(a => a.Model, foo); + pb.AddChildContent>(pb => + { + pb.Add(a => a.Value, foo.Name); + pb.Add(a => a.ValueExpression, foo.GenerateValueExpression()); + }); + }); var validator = cut.FindComponent().Instance; - var field = typeof(BootstrapBlazorDataAnnotationsValidator).GetField( - "_fieldValidationOperations", + var property = typeof(BootstrapBlazorDataAnnotationsValidator).GetProperty( + "CurrentEditContext", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic); - var operations = Assert.IsType(field?.GetValue(validator), false); - cut.WaitForAssertion(() => Assert.Empty(operations)); + var editContext = Assert.IsType(property?.GetValue(validator)); + +#if NET11_0_OR_GREATER + await cut.InvokeAsync(() => editContext.ValidateAsync()); +#else + await cut.InvokeAsync(() => editContext.Validate()); + cut.WaitForAssertion(() => Assert.NotEmpty(editContext.GetValidationMessages())); #endif } [Fact] - public async Task ValidateFieldAndCleanupAsync_OperationCancellation() + public async Task OnValidationRequested_Exception() { + var logger = Assert.IsType( + Context.Services.GetRequiredService>()); var foo = new Foo() { Name = "Test" }; var cut = Context.Render(pb => { @@ -135,21 +160,39 @@ public async Task ValidateFieldAndCleanupAsync_OperationCancellation() { pb.Add(a => a.Value, foo.Name); pb.Add(a => a.ValueExpression, foo.GenerateValueExpression()); + pb.Add(a => a.ValidateRules, [new ThrowingValidator()]); }); }); var validator = cut.FindComponent().Instance; - var validatorType = typeof(BootstrapBlazorDataAnnotationsValidator); -#if NET11_0_OR_GREATER - var property = validatorType.GetProperty( + var property = typeof(BootstrapBlazorDataAnnotationsValidator).GetProperty( "CurrentEditContext", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic); var editContext = Assert.IsType(property?.GetValue(validator)); - using var tokenSource = new CancellationTokenSource(); - tokenSource.Cancel(); - await Assert.ThrowsAsync( - async () => await cut.InvokeAsync(() => editContext.ValidateAsync(tokenSource.Token))); +#if NET11_0_OR_GREATER + await cut.InvokeAsync(() => editContext.ValidateAsync()); #else + await cut.InvokeAsync(() => editContext.Validate()); + cut.WaitForAssertion(() => Assert.IsType(logger.Exception)); +#endif + } + +#if !NET11_0_OR_GREATER + [Fact] + public async Task ValidateFieldAndCleanupAsync_OperationCancellation() + { + var foo = new Foo() { Name = "Test" }; + var cut = Context.Render(pb => + { + pb.Add(a => a.Model, foo); + pb.AddChildContent>(pb => + { + pb.Add(a => a.Value, foo.Name); + pb.Add(a => a.ValueExpression, foo.GenerateValueExpression()); + }); + }); + var validator = cut.FindComponent().Instance; + var validatorType = typeof(BootstrapBlazorDataAnnotationsValidator); var operationType = validatorType.GetNestedType( "FieldValidationOperation", System.Reflection.BindingFlags.NonPublic); @@ -170,8 +213,8 @@ await cut.InvokeAsync(async () => method.Invoke(validator, [new FieldIdentifier(foo, nameof(foo.Name)), operation]), exactMatch: false); await validation; }); -#endif } +#endif [Fact] public async Task Validate_Ok() From 2051f615f2744980fd9564c49e107426b9b885f1 Mon Sep 17 00:00:00 2001 From: Argo Zhang Date: Fri, 4 Sep 2026 18:57:36 +0800 Subject: [PATCH 23/23] =?UTF-8?q?refactor:=20=E9=87=8D=E6=9E=84=E4=BB=A3?= =?UTF-8?q?=E7=A0=81=E5=9D=97=E6=8F=90=E9=AB=98=E5=8F=AF=E8=AF=BB=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...BootstrapBlazorDataAnnotationsValidator.cs | 74 +++++++++---------- 1 file changed, 36 insertions(+), 38 deletions(-) diff --git a/src/BootstrapBlazor/Components/ValidateForm/BootstrapBlazorDataAnnotationsValidator.cs b/src/BootstrapBlazor/Components/ValidateForm/BootstrapBlazorDataAnnotationsValidator.cs index fbac31596cb..6f41631fe32 100644 --- a/src/BootstrapBlazor/Components/ValidateForm/BootstrapBlazorDataAnnotationsValidator.cs +++ b/src/BootstrapBlazor/Components/ValidateForm/BootstrapBlazorDataAnnotationsValidator.cs @@ -14,18 +14,6 @@ namespace BootstrapBlazor.Components; ///
public class BootstrapBlazorDataAnnotationsValidator : ComponentBase, IDisposable { -#if !NET11_0_OR_GREATER - private readonly SemaphoreSlim _validationLock = new(1, 1); - private readonly Dictionary _fieldValidationOperations = []; - private bool _suppressValidationRequested; -#endif - -#if NET9_0_OR_GREATER - private readonly Lock _fieldValidationLock = new(); -#else - private readonly object _fieldValidationLock = new(); -#endif - /// /// 获得/设置 当前编辑数据上下文 /// Gets or sets the current edit context @@ -68,9 +56,42 @@ protected override void OnInitialized() AddEditContextDataAnnotationsValidation(); } + private void AddEditContextDataAnnotationsValidation() + { + CurrentEditContext.OnValidationRequested += OnValidationRequested; + CurrentEditContext.OnFieldChanged += OnFieldChanged; + } + + private void RemoveEditContextDataAnnotationsValidation() + { + CurrentEditContext.OnValidationRequested -= OnValidationRequested; + CurrentEditContext.OnFieldChanged -= OnFieldChanged; + } + #if NET11_0_OR_GREATER internal Task ValidateAsync(CancellationToken cancellationToken = default) => CurrentEditContext.ValidateAsync(cancellationToken); + + private void OnValidationRequested(object? sender, ValidationRequestedEventArgs args) + { + args.AddAsyncValidator(cancellationToken => ValidateModelAsync(CurrentEditContext, _message, Provider, cancellationToken)); + } + + private void OnFieldChanged(object? sender, FieldChangedEventArgs args) + { + var fieldIdentifier = args.FieldIdentifier; + CurrentEditContext.RegisterAsyncFieldValidator(fieldIdentifier, cancellationToken => ValidateFieldAsync(CurrentEditContext, _message, fieldIdentifier, Provider, cancellationToken)); + } +#else +#if NET9_0_OR_GREATER + private readonly Lock _fieldValidationLock = new(); #else + private readonly object _fieldValidationLock = new(); +#endif + + private readonly SemaphoreSlim _validationLock = new(1, 1); + private readonly Dictionary _fieldValidationOperations = []; + private bool _suppressValidationRequested; + internal async Task ValidateAsync(CancellationToken cancellationToken = default) { await _validationLock.WaitAsync(cancellationToken); @@ -83,6 +104,7 @@ internal async Task ValidateAsync(CancellationToken cancellationToken = de _suppressValidationRequested = true; try { + // NET10 只有同步方法 synchronousValid = CurrentEditContext.Validate(); } finally @@ -90,6 +112,7 @@ internal async Task ValidateAsync(CancellationToken cancellationToken = de _suppressValidationRequested = false; } + // 通过 _suppressValidationRequested 控制 OnValidationRequested 事件中的 ValidateModelAsync 不被调用,避免重复验证 var valid = await ValidateModelAsync(CurrentEditContext, _message, Provider, cancellationToken); return synchronousValid && valid && !CurrentEditContext.GetValidationMessages().Any(); } @@ -107,29 +130,9 @@ internal async Task ValidateAsync(CancellationToken cancellationToken = de _validationLock.Release(); } } -#endif - - private void AddEditContextDataAnnotationsValidation() - { - CurrentEditContext.OnValidationRequested += OnValidationRequested; - CurrentEditContext.OnFieldChanged += OnFieldChanged; - } - - private void RemoveEditContextDataAnnotationsValidation() - { - CurrentEditContext.OnValidationRequested -= OnValidationRequested; - CurrentEditContext.OnFieldChanged -= OnFieldChanged; - } -#if NET11_0_OR_GREATER - private void OnValidationRequested(object? sender, ValidationRequestedEventArgs args) -#else private async void OnValidationRequested(object? sender, ValidationRequestedEventArgs args) -#endif { -#if NET11_0_OR_GREATER - args.AddAsyncValidator(cancellationToken => ValidateModelAsync(CurrentEditContext, _message, Provider, cancellationToken)); -#else if (!_suppressValidationRequested) { try @@ -141,15 +144,12 @@ private async void OnValidationRequested(object? sender, ValidationRequestedEven Logger.LogError(exception, "An exception occurred while validating the form."); } } -#endif } private void OnFieldChanged(object? sender, FieldChangedEventArgs args) { var fieldIdentifier = args.FieldIdentifier; -#if NET11_0_OR_GREATER - CurrentEditContext.RegisterAsyncFieldValidator(fieldIdentifier, cancellationToken => ValidateFieldAsync(CurrentEditContext, _message, fieldIdentifier, Provider, cancellationToken)); -#else + FieldValidationOperation? previousOperation; FieldValidationOperation operation; lock (_fieldValidationLock) @@ -160,10 +160,8 @@ private void OnFieldChanged(object? sender, FieldChangedEventArgs args) } previousOperation?.Cancel(); _ = ValidateFieldAndCleanupAsync(fieldIdentifier, operation); -#endif } -#if !NET11_0_OR_GREATER private async Task ValidateFieldAndCleanupAsync(FieldIdentifier fieldIdentifier, FieldValidationOperation operation) { try