From 61b2982f0d2f95c4ed6255a3c8eb5a00a5db707f Mon Sep 17 00:00:00 2001 From: Vladimir Enchev Date: Fri, 29 May 2026 15:19:30 +0300 Subject: [PATCH] RadzenTabs: ensure render state is restored when tab event handlers throw Follow-up to the #2558 fix. In SelectTab the StateHasChanged call was skipped if Change/SelectedIndexChanged threw, leaving the component stale (multiple tabs visually active); in SelectTabOnClient a throw left shouldRender stuck at false, blocking all further renders. Wrap both in finally so the render state is always restored. Addresses review feedback on commit df1dfa713. Fixes #2558 --- Radzen.Blazor.Tests/TabsTests.cs | 21 ++++++++++++++++ Radzen.Blazor/RadzenTabs.razor.cs | 41 ++++++++++++++++++++----------- 2 files changed, 47 insertions(+), 15 deletions(-) diff --git a/Radzen.Blazor.Tests/TabsTests.cs b/Radzen.Blazor.Tests/TabsTests.cs index def53ebaa..b6c40d72b 100644 --- a/Radzen.Blazor.Tests/TabsTests.cs +++ b/Radzen.Blazor.Tests/TabsTests.cs @@ -230,6 +230,27 @@ namespace Radzen.Blazor.Tests Assert.True(component.RenderCount > renderCountBefore); } + [Fact] + public void Tabs_ChangeHandlerThrows_StillRendersSelectedTab() + { + using var ctx = new TestContext(); + + var component = ctx.RenderComponent(parameters => parameters + .Add(p => p.SelectedIndex, 0) + .Add(p => p.Change, (int _) => throw new System.InvalidOperationException("boom")) + .Add(p => p.Tabs, TabsFragmentWithContent(("First", "First-Content"), ("Second", "Second-Content"))) + ); + + Assert.Contains("First-Content", component.Markup); + + var secondTab = component.FindAll("button[role='tab']")[1]; + var ex = Record.Exception(() => secondTab.Click()); + + Assert.NotNull(ex); + Assert.Contains("Second-Content", component.Markup); + Assert.DoesNotContain("First-Content", component.Markup); + } + [Fact] public void Tabs_NonNavigationKeyDown_DoesNotBlockSubsequentRenders() { diff --git a/Radzen.Blazor/RadzenTabs.razor.cs b/Radzen.Blazor/RadzenTabs.razor.cs index cf6e19c9c..c75859594 100644 --- a/Radzen.Blazor/RadzenTabs.razor.cs +++ b/Radzen.Blazor/RadzenTabs.razor.cs @@ -210,22 +210,27 @@ namespace Radzen.Blazor SetFocusedIndex(); - if (raiseChange) + try { - await Change.InvokeAsync(selectedIndex); - - await SelectedIndexChanged.InvokeAsync(selectedIndex); - - try - { - await Element.FocusAsync(preventScroll: true); - } - catch (JSDisconnectedException) + if (raiseChange) { + await Change.InvokeAsync(selectedIndex); + + await SelectedIndexChanged.InvokeAsync(selectedIndex); + + try + { + await Element.FocusAsync(preventScroll: true); + } + catch (JSDisconnectedException) + { + } } } - - StateHasChanged(); + finally + { + StateHasChanged(); + } } /// @@ -342,9 +347,15 @@ namespace Radzen.Blazor } shouldRender = false; - await Change.InvokeAsync(selectedIndex); - await SelectedIndexChanged.InvokeAsync(selectedIndex); - shouldRender = true; + try + { + await Change.InvokeAsync(selectedIndex); + await SelectedIndexChanged.InvokeAsync(selectedIndex); + } + finally + { + shouldRender = true; + } try {