diff --git a/src/Components/Components/src/PersistentState/PersistentValueProviderComponentSubscription.cs b/src/Components/Components/src/PersistentState/PersistentValueProviderComponentSubscription.cs index 30e537a418a3..35ad18735816 100644 --- a/src/Components/Components/src/PersistentState/PersistentValueProviderComponentSubscription.cs +++ b/src/Components/Components/src/PersistentState/PersistentValueProviderComponentSubscription.cs @@ -143,9 +143,9 @@ internal void RestoreProperty() Log.RestoringValueFromState(_logger, _storageKey, _propertyType.Name, _propertyName); var sequence = new ReadOnlySequence(data!); _lastValue = _customSerializer.Restore(_propertyType, sequence); + _ignoreComponentPropertyValue = true; if (!skipNotifications) { - _ignoreComponentPropertyValue = true; _subscriber.NotifyCascadingValueChanged(ParameterViewLifetime.Unbound); } } @@ -160,9 +160,9 @@ internal void RestoreProperty() { Log.RestoredValueFromPersistentState(_logger, _storageKey, _propertyType.Name, "null", _propertyName); _lastValue = value; + _ignoreComponentPropertyValue = true; if (!skipNotifications) { - _ignoreComponentPropertyValue = true; _subscriber.NotifyCascadingValueChanged(ParameterViewLifetime.Unbound); } } diff --git a/src/Components/Components/test/PersistentValueProviderComponentSubscriptionTests.cs b/src/Components/Components/test/PersistentValueProviderComponentSubscriptionTests.cs index 273246516f7a..894f0aa76ff6 100644 --- a/src/Components/Components/test/PersistentValueProviderComponentSubscriptionTests.cs +++ b/src/Components/Components/test/PersistentValueProviderComponentSubscriptionTests.cs @@ -263,6 +263,8 @@ public async Task GetOrComputeLastValue_FollowsCorrectValueTransitionSequence() var cascadingParameterInfo = CreateCascadingParameterInfo(nameof(TestComponent.State), typeof(string)); // Act & Assert - First call: Returns restored value from state + var firstCall = provider.GetCurrentValue(componentState, cascadingParameterInfo); + Assert.Equal("first-restored-value", firstCall); Assert.Equal("first-restored-value", component.State); // Change the component's property value @@ -708,4 +710,213 @@ public void Constructor_WorksCorrectly_ForPublicProperty() Assert.NotNull(subscription); subscription.Dispose(); } + + [Fact] + public async Task ComponentRecreation_PreservesPersistedState_WhenComponentIsRecreatedDuringNavigation() + { + // This test simulates the scenario where a component is destroyed and recreated (like during navigation) + // and verifies that the persisted state is correctly restored in the new component instance + + // Arrange + var appState = new Dictionary(); + var manager = new ComponentStatePersistenceManager(NullLogger.Instance); + var serviceProvider = PersistentStateProviderServiceCollectionExtensions.AddSupplyValueFromPersistentComponentStateProvider(new ServiceCollection()) + .AddSingleton(manager) + .AddSingleton(manager.State) + .AddFakeLogging() + .BuildServiceProvider(); + var renderer = new TestRenderer(serviceProvider); + var provider = (PersistentStateValueProvider)renderer.ServiceProviderCascadingValueSuppliers.Single(); + var cascadingParameterInfo = CreateCascadingParameterInfo(nameof(TestComponent.State), typeof(string)); + + // Setup initial persisted state + var component1 = new TestComponent { State = "initial-property-value" }; + var componentId1 = renderer.AssignRootComponentId(component1); + var componentState1 = renderer.GetComponentState(component1); + var key = PersistentStateValueProviderKeyResolver.ComputeKey(componentState1, nameof(TestComponent.State)); + + appState[key] = JsonSerializer.SerializeToUtf8Bytes("persisted-value-from-previous-session", JsonSerializerOptions.Web); + await manager.RestoreStateAsync(new TestStore(appState), RestoreContext.InitialValue); + + // Act & Assert - First component instance should get the persisted value + await renderer.Dispatcher.InvokeAsync(() => renderer.RenderRootComponentAsync(componentId1, ParameterView.Empty)); + Assert.Equal("persisted-value-from-previous-session", component1.State); + + // Simulate component destruction (like during navigation away) + renderer.RemoveRootComponent(componentId1); + + // Simulate component recreation (like during navigation back) - NEW SUBSCRIPTION CREATED + var component2 = new TestComponent { State = "new-component-initial-value" }; + var componentId2 = renderer.AssignRootComponentId(component2); + var componentState2 = renderer.GetComponentState(component2); + + // Verify the key is the same (important for components without @key) + var key2 = PersistentStateValueProviderKeyResolver.ComputeKey(componentState2, nameof(TestComponent.State)); + Assert.Equal(key, key2); + + // The state should still be available for restoration + await renderer.Dispatcher.InvokeAsync(() => renderer.RenderRootComponentAsync(componentId2, ParameterView.Empty)); + + // Assert - The new component instance should get the same persisted value + var providerForSecondComponent = (PersistentStateValueProvider)renderer.ServiceProviderCascadingValueSuppliers.Single(); + var cascadingParameterInfoForSecondComponent = CreateCascadingParameterInfo(nameof(TestComponent.State), typeof(string)); + var restoredCall = providerForSecondComponent.GetCurrentValue(componentState2, cascadingParameterInfoForSecondComponent); + Assert.Equal("persisted-value-from-previous-session", restoredCall); + Assert.Equal("persisted-value-from-previous-session", component2.State); + } + + [Fact] + public async Task ComponentRecreation_WithStateUpdates_PreservesCorrectValueTransitionSequence() + { + // This test simulates the full lifecycle with component recreation and state updates + // following the pattern from GetOrComputeLastValue_FollowsCorrectValueTransitionSequence + // but with subscription recreation between state restorations + + // Arrange + var appState = new Dictionary(); + var manager = new ComponentStatePersistenceManager(NullLogger.Instance); + var serviceProvider = PersistentStateProviderServiceCollectionExtensions.AddSupplyValueFromPersistentComponentStateProvider(new ServiceCollection()) + .AddSingleton(manager) + .AddSingleton(manager.State) + .AddFakeLogging() + .BuildServiceProvider(); + var renderer = new TestRenderer(serviceProvider); + var provider = (PersistentStateValueProvider)renderer.ServiceProviderCascadingValueSuppliers.Single(); + var cascadingParameterInfo = CreateCascadingParameterInfo(nameof(TestComponent.State), typeof(string)); + + // First component lifecycle + var component1 = new TestComponent { State = "initial-property-value" }; + var componentId1 = renderer.AssignRootComponentId(component1); + var componentState1 = renderer.GetComponentState(component1); + var key = PersistentStateValueProviderKeyResolver.ComputeKey(componentState1, nameof(TestComponent.State)); + + // Pre-populate with first persisted value + appState[key] = JsonSerializer.SerializeToUtf8Bytes("first-restored-value", JsonSerializerOptions.Web); + await manager.RestoreStateAsync(new TestStore(appState), RestoreContext.InitialValue); + + await renderer.Dispatcher.InvokeAsync(() => renderer.RenderRootComponentAsync(componentId1, ParameterView.Empty)); + + // Act & Assert - First component gets restored value + var firstCall = provider.GetCurrentValue(componentState1, cascadingParameterInfo); + Assert.Equal("first-restored-value", firstCall); + Assert.Equal("first-restored-value", component1.State); + + // Update component property + component1.State = "updated-by-component-1"; + Assert.Equal("updated-by-component-1", provider.GetCurrentValue(componentState1, cascadingParameterInfo)); + + // Simulate component destruction and recreation (NEW SUBSCRIPTION CREATED) + renderer.RemoveRootComponent(componentId1); + + var component2 = new TestComponent { State = "new-component-initial-value" }; + var componentId2 = renderer.AssignRootComponentId(component2); + var componentState2 = renderer.GetComponentState(component2); + + // Restore state with a different value + appState.Clear(); + appState[key] = JsonSerializer.SerializeToUtf8Bytes("second-restored-value", JsonSerializerOptions.Web); + await manager.RestoreStateAsync(new TestStore(appState), RestoreContext.ValueUpdate); + + await renderer.Dispatcher.InvokeAsync(() => renderer.RenderRootComponentAsync(componentId2, ParameterView.Empty)); + + // Assert - New component gets the updated restored value + var secondComponentCall = provider.GetCurrentValue(componentState2, cascadingParameterInfo); + Assert.Equal("second-restored-value", secondComponentCall); + Assert.Equal("second-restored-value", component2.State); + + // Continue with property updates on the new component + component2.State = "updated-by-component-2"; + Assert.Equal("updated-by-component-2", provider.GetCurrentValue(componentState2, cascadingParameterInfo)); + } + + [Fact] + public async Task ComponentRecreation_WithSkipNotifications_StillRestoresCorrectly() + { + // This test verifies that the fix works even when skipNotifications is true during component recreation, + // which is the core scenario that was broken before our fix + + // Arrange + var appState = new Dictionary(); + var manager = new ComponentStatePersistenceManager(NullLogger.Instance); + var serviceProvider = PersistentStateProviderServiceCollectionExtensions.AddSupplyValueFromPersistentComponentStateProvider(new ServiceCollection()) + .AddSingleton(manager) + .AddSingleton(manager.State) + .AddFakeLogging() + .BuildServiceProvider(); + var renderer = new TestRenderer(serviceProvider); + var provider = (PersistentStateValueProvider)renderer.ServiceProviderCascadingValueSuppliers.Single(); + var cascadingParameterInfo = CreateCascadingParameterInfo(nameof(TestComponent.State), typeof(string)); + + // Setup persisted state + var component1 = new TestComponent { State = "component-initial-value" }; + var componentId1 = renderer.AssignRootComponentId(component1); + var componentState1 = renderer.GetComponentState(component1); + var key = PersistentStateValueProviderKeyResolver.ComputeKey(componentState1, nameof(TestComponent.State)); + + appState[key] = JsonSerializer.SerializeToUtf8Bytes("persisted-value", JsonSerializerOptions.Web); + await manager.RestoreStateAsync(new TestStore(appState), RestoreContext.InitialValue); + + // First component gets the persisted value + await renderer.Dispatcher.InvokeAsync(() => renderer.RenderRootComponentAsync(componentId1, ParameterView.Empty)); + var firstCall = provider.GetCurrentValue(componentState1, cascadingParameterInfo); + Assert.Equal("persisted-value", firstCall); + Assert.Equal("persisted-value", component1.State); + + // Destroy and recreate component (simulating navigation or component without @key) + renderer.RemoveRootComponent(componentId1); + + // Create new component instance - this will create a NEW SUBSCRIPTION + var component2 = new TestComponent { State = "different-initial-value" }; + var componentId2 = renderer.AssignRootComponentId(component2); + var componentState2 = renderer.GetComponentState(component2); + + // Render the new component - this should restore the persisted value even if skipNotifications is true + await renderer.Dispatcher.InvokeAsync(() => renderer.RenderRootComponentAsync(componentId2, ParameterView.Empty)); + + // Assert - The new component should get the persisted value, not its initial property value + var providerForLastComponent = (PersistentStateValueProvider)renderer.ServiceProviderCascadingValueSuppliers.Single(); + var cascadingParameterInfoForLastComponent = CreateCascadingParameterInfo(nameof(TestComponent.State), typeof(string)); + var restoredCall2 = providerForLastComponent.GetCurrentValue(componentState2, cascadingParameterInfoForLastComponent); + Assert.Equal("persisted-value", restoredCall2); + Assert.Equal("persisted-value", component2.State); + } + + [Fact] + public async Task DebugTest_UnderstandIgnoreComponentPropertyValueFlag() + { + // Simple test to understand the _ignoreComponentPropertyValue flag behavior + var appState = new Dictionary(); + var manager = new ComponentStatePersistenceManager(NullLogger.Instance); + var serviceProvider = PersistentStateProviderServiceCollectionExtensions.AddSupplyValueFromPersistentComponentStateProvider(new ServiceCollection()) + .AddSingleton(manager) + .AddSingleton(manager.State) + .AddFakeLogging() + .BuildServiceProvider(); + var renderer = new TestRenderer(serviceProvider); + var provider = (PersistentStateValueProvider)renderer.ServiceProviderCascadingValueSuppliers.Single(); + var component = new TestComponent { State = "initial-property-value" }; + var componentId = renderer.AssignRootComponentId(component); + var componentState = renderer.GetComponentState(component); + var cascadingParameterInfo = CreateCascadingParameterInfo(nameof(TestComponent.State), typeof(string)); + + // Set up state to restore + var key = PersistentStateValueProviderKeyResolver.ComputeKey(componentState, nameof(TestComponent.State)); + appState[key] = JsonSerializer.SerializeToUtf8Bytes("restored-value", JsonSerializerOptions.Web); + await manager.RestoreStateAsync(new TestStore(appState), RestoreContext.InitialValue); + + // Render component - this should restore the value + await renderer.Dispatcher.InvokeAsync(() => renderer.RenderRootComponentAsync(componentId, ParameterView.Empty)); + + // First call should return restored value + var firstCall = provider.GetCurrentValue(componentState, cascadingParameterInfo); + Assert.Equal("restored-value", firstCall); + Assert.Equal("restored-value", component.State); + + // Update the component's property manually + component.State = "manually-updated-value"; + + // Second call should return the manually updated value + var secondCall = provider.GetCurrentValue(componentState, cascadingParameterInfo); + Assert.Equal("manually-updated-value", secondCall); + } } diff --git a/src/Components/test/E2ETest/Tests/StatePersistenceTest.cs b/src/Components/test/E2ETest/Tests/StatePersistenceTest.cs index a05fbfa05979..703535a1d251 100644 --- a/src/Components/test/E2ETest/Tests/StatePersistenceTest.cs +++ b/src/Components/test/E2ETest/Tests/StatePersistenceTest.cs @@ -385,4 +385,253 @@ private void AssertDeclarativePageState( Browser.Equal("Streaming: True", () => Browser.FindElement(By.Id("streaming")).Text); } } + + // Tests for components with conditional rendering and recreation scenarios + // These tests validate the fix for issue #63193 where persistent state restoration + // was failing for components without keys or during component add/remove cycles + + [Theory] + [InlineData(typeof(InteractiveServerRenderMode), (string)null)] + [InlineData(typeof(InteractiveServerRenderMode), "ServerStreaming")] + [InlineData(typeof(InteractiveWebAssemblyRenderMode), (string)null)] + [InlineData(typeof(InteractiveWebAssemblyRenderMode), "WebAssemblyStreaming")] + [InlineData(typeof(InteractiveAutoRenderMode), (string)null)] + [InlineData(typeof(InteractiveAutoRenderMode), "AutoStreaming")] + public void CanRestoreStateForComponentsWithoutKeysAndConditionalRendering( + Type renderMode, + string streaming) + { + var mode = renderMode switch + { + var t when t == typeof(InteractiveServerRenderMode) => "server", + var t when t == typeof(InteractiveWebAssemblyRenderMode) => "wasm", + var t when t == typeof(InteractiveAutoRenderMode) => "auto", + _ => throw new ArgumentException($"Unknown render mode: {renderMode.Name}") + }; + + // Navigate to a page without components first to exercise enhanced navigation + NavigateToInitialPageConditional(streaming, mode); + if (mode == "auto") + { + BlockWebAssemblyResourceLoad(); + } + Browser.Click(By.Id("call-blazor-start")); + + // Navigate to page with conditional components - initially show both components + Browser.Click(By.Id("page-with-conditional-components-link")); + + if (mode != "auto") + { + RenderConditionalComponentsAndValidate(mode, renderMode, streaming, showConditional: true); + } + else + { + // For auto mode, validate that the state is persisted for both runtimes + RenderConditionalComponentsAndValidate(mode, renderMode, streaming, interactiveRuntime: "server", showConditional: true); + + UnblockWebAssemblyResourceLoad(); + Browser.Navigate().Refresh(); + NavigateToInitialPageConditional(streaming, mode); + Browser.Click(By.Id("call-blazor-start")); + Browser.Click(By.Id("page-with-conditional-components-link")); + + RenderConditionalComponentsAndValidate(mode, renderMode, streaming, interactiveRuntime: "wasm", showConditional: true); + } + } + + [Theory] + [InlineData(typeof(InteractiveServerRenderMode), (string)null)] + [InlineData(typeof(InteractiveServerRenderMode), "ServerStreaming")] + [InlineData(typeof(InteractiveWebAssemblyRenderMode), (string)null)] + [InlineData(typeof(InteractiveWebAssemblyRenderMode), "WebAssemblyStreaming")] + [InlineData(typeof(InteractiveAutoRenderMode), (string)null)] + [InlineData(typeof(InteractiveAutoRenderMode), "AutoStreaming")] + public void CanRestoreStateAfterConditionalComponentToggling( + Type renderMode, + string streaming) + { + var mode = renderMode switch + { + var t when t == typeof(InteractiveServerRenderMode) => "server", + var t when t == typeof(InteractiveWebAssemblyRenderMode) => "wasm", + var t when t == typeof(InteractiveAutoRenderMode) => "auto", + _ => throw new ArgumentException($"Unknown render mode: {renderMode.Name}") + }; + + // Navigate to page with conditional components showing initially + NavigateToInitialPageConditional(streaming, mode); + if (mode == "auto") + { + BlockWebAssemblyResourceLoad(); + } + Browser.Click(By.Id("call-blazor-start")); + Browser.Click(By.Id("page-with-conditional-components-show")); + + if (mode != "auto") + { + TestConditionalComponentToggling(mode, renderMode, streaming); + } + else + { + // For auto mode, validate both runtimes + TestConditionalComponentToggling(mode, renderMode, streaming, interactiveRuntime: "server"); + + UnblockWebAssemblyResourceLoad(); + Browser.Navigate().Refresh(); + NavigateToInitialPageConditional(streaming, mode); + Browser.Click(By.Id("call-blazor-start")); + Browser.Click(By.Id("page-with-conditional-components-show")); + + TestConditionalComponentToggling(mode, renderMode, streaming, interactiveRuntime: "wasm"); + } + } + + private void NavigateToInitialPageConditional(string streaming, string mode) + { + if (streaming == null) + { + Navigate($"subdir/persistent-state/page-no-components?render-mode={mode}&suppress-autostart"); + } + else + { + Navigate($"subdir/persistent-state/page-no-components?render-mode={mode}&streaming-id={streaming}&suppress-autostart"); + } + } + + private void RenderConditionalComponentsAndValidate( + string mode, + Type renderMode, + string streaming, + string interactiveRuntime = null, + bool showConditional = true) + { + AssertConditionalPageState( + mode: mode, + renderMode: renderMode.Name, + interactive: streaming == null, + showConditional: showConditional, + streamingId: streaming, + streamingCompleted: false, + interactiveRuntime: interactiveRuntime); + + if (streaming == null) + { + return; + } + + Browser.Click(By.Id("end-streaming")); + + AssertConditionalPageState( + mode: mode, + renderMode: renderMode.Name, + interactive: true, + showConditional: showConditional, + streamingId: streaming, + streamingCompleted: true, + interactiveRuntime: interactiveRuntime); + } + + private void TestConditionalComponentToggling( + string mode, + Type renderMode, + string streaming, + string interactiveRuntime = null) + { + // Initially both components should be present and restore state + AssertConditionalPageState( + mode: mode, + renderMode: renderMode.Name, + interactive: streaming == null, + showConditional: true, + streamingId: streaming, + streamingCompleted: false, + interactiveRuntime: interactiveRuntime); + + // Hide the conditional component + Browser.Click(By.Id("toggle-conditional")); + + AssertConditionalPageState( + mode: mode, + renderMode: renderMode.Name, + interactive: streaming == null, + showConditional: false, + streamingId: streaming, + streamingCompleted: false, + interactiveRuntime: interactiveRuntime); + + // Show the conditional component again - it should restore its state + Browser.Click(By.Id("toggle-conditional")); + + AssertConditionalPageState( + mode: mode, + renderMode: renderMode.Name, + interactive: streaming == null, + showConditional: true, + streamingId: streaming, + streamingCompleted: false, + interactiveRuntime: interactiveRuntime); + + if (streaming != null) + { + Browser.Click(By.Id("end-streaming")); + + AssertConditionalPageState( + mode: mode, + renderMode: renderMode.Name, + interactive: true, + showConditional: true, + streamingId: streaming, + streamingCompleted: true, + interactiveRuntime: interactiveRuntime); + } + } + + private void AssertConditionalPageState( + string mode, + string renderMode, + bool interactive, + bool showConditional, + string streamingId = null, + bool streamingCompleted = false, + string interactiveRuntime = null) + { + Browser.Equal($"Render mode: {renderMode}", () => Browser.FindElement(By.Id("render-mode")).Text); + Browser.Equal($"Streaming id:{streamingId}", () => Browser.FindElement(By.Id("streaming-id")).Text); + Browser.Equal($"Show conditional: {showConditional.ToString().ToLowerInvariant()}", () => Browser.FindElement(By.Id("show-conditional")).Text); + Browser.Equal($"Interactive: {interactive}", () => Browser.FindElement(By.Id("interactive")).Text); + + if (streamingId == null || streamingCompleted) + { + interactiveRuntime = !interactive ? "none" : mode == "server" || mode == "wasm" ? mode : (interactiveRuntime ?? throw new InvalidOperationException("Specify interactiveRuntime for auto mode")); + + // Always rendered component should always be present + Browser.Exists(By.Id("always-rendered-component")); + + // Conditional component should only be present when showConditional is true + if (showConditional) + { + Browser.Exists(By.Id("conditional-component")); + } + else + { + Browser.DoesNotExist(By.Id("conditional-component")); + } + + // Check state restoration - components should restore their state correctly + Browser.Equal($"Interactive runtime: {interactiveRuntime}", () => Browser.FindElement(By.XPath("//div[@id='always-rendered-component']//p[@id='interactive-runtime']")).Text); + Browser.Equal("State found:True", () => Browser.FindElement(By.XPath("//div[@id='always-rendered-component']//p[@id='state-found']")).Text); + Browser.Equal("State value:restored", () => Browser.FindElement(By.XPath("//div[@id='always-rendered-component']//p[@id='state-value']")).Text); + + if (showConditional) + { + Browser.Equal($"Interactive runtime: {interactiveRuntime}", () => Browser.FindElement(By.XPath("//div[@id='conditional-component']//p[@id='interactive-runtime']")).Text); + Browser.Equal("State found:True", () => Browser.FindElement(By.XPath("//div[@id='conditional-component']//p[@id='state-found']")).Text); + Browser.Equal("State value:restored-conditional", () => Browser.FindElement(By.XPath("//div[@id='conditional-component']//p[@id='state-value']")).Text); + } + } + else + { + Browser.Equal("Streaming: True", () => Browser.FindElement(By.Id("streaming")).Text); + } + } } diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/PersistentState/PageWithConditionalPersistentComponents.razor b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/PersistentState/PageWithConditionalPersistentComponents.razor new file mode 100644 index 000000000000..0b4ae8e8462b --- /dev/null +++ b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/PersistentState/PageWithConditionalPersistentComponents.razor @@ -0,0 +1,105 @@ +@page "/persistent-state/page-with-conditional-components" +@using TestContentPackage.PersistentComponents + +

Persistent component state with conditional rendering

+ +

+ This page tests persistent component state restoration for: + 1. A component without @key that always renders (tests component recreation during navigation) + 2. A component that gets conditionally rendered based on query string (tests add/remove scenarios) + Both should restore state correctly regardless of when they are destroyed and recreated. +

+ +

Render mode: @_renderMode?.GetType()?.Name

+

Streaming id:@StreamingId

+

Show conditional: @ShowConditional

+ +@if (_renderMode != null) +{ + @* Component without @key that always renders - tests navigation scenarios *@ +
+

Always Rendered Component (no @key)

+ @if (!string.IsNullOrEmpty(StreamingId)) + { + + } + else + { + + } +
+ + @* Conditionally rendered component - tests add/remove scenarios *@ + @if (ShowConditional) + { +
+

Conditionally Rendered Component

+ @if (!string.IsNullOrEmpty(StreamingId)) + { + + } + else + { + + } +
+ } +} + +@if (!string.IsNullOrEmpty(StreamingId)) +{ + End streaming +} + +@(ShowConditional ? "Hide" : "Show") conditional component +
+Go to page with no components + +@code { + + private IComponentRenderMode _renderMode; + + [SupplyParameterFromQuery(Name = "render-mode")] public string RenderMode { get; set; } + + [SupplyParameterFromQuery(Name = "streaming-id")] public string StreamingId { get; set; } + + [SupplyParameterFromQuery(Name = "server-state")] public string ServerState { get; set; } + + [SupplyParameterFromQuery(Name = "show-conditional")] public bool ShowConditional { get; set; } + + protected override void OnInitialized() + { + if (!string.IsNullOrEmpty(RenderMode)) + { + switch (RenderMode) + { + case "server": + _renderMode = new InteractiveServerRenderMode(true); + break; + case "wasm": + _renderMode = new InteractiveWebAssemblyRenderMode(true); + break; + case "auto": + _renderMode = new InteractiveAutoRenderMode(true); + break; + default: + throw new ArgumentException($"Invalid render mode: {RenderMode}"); + } + } + } + + private string GetToggleConditionalUrl() + { + var uri = new UriBuilder(Navigation.Uri); + var query = Microsoft.AspNetCore.WebUtilities.QueryHelpers.ParseQuery(uri.Query); + + query["show-conditional"] = (!ShowConditional).ToString().ToLowerInvariant(); + + uri.Query = Microsoft.AspNetCore.WebUtilities.QueryHelpers.AddQueryString("", query) + .TrimStart('?'); + + return uri.ToString(); + } + + [Inject] public NavigationManager Navigation { get; set; } +} \ No newline at end of file diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/PersistentState/PageWithoutComponents.razor b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/PersistentState/PageWithoutComponents.razor index a72bdbe97778..0258a3a918f2 100644 --- a/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/PersistentState/PageWithoutComponents.razor +++ b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/PersistentState/PageWithoutComponents.razor @@ -8,6 +8,10 @@ Go to page with declarative state components +Go to page with conditional components + +Go to page with conditional components (show conditional) + @code { [SupplyParameterFromQuery(Name = "render-mode")] public string RenderMode { get; set; }