Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -143,9 +143,9 @@ internal void RestoreProperty()
Log.RestoringValueFromState(_logger, _storageKey, _propertyType.Name, _propertyName);
var sequence = new ReadOnlySequence<byte>(data!);
_lastValue = _customSerializer.Restore(_propertyType, sequence);
_ignoreComponentPropertyValue = true;
if (!skipNotifications)
{
_ignoreComponentPropertyValue = true;
_subscriber.NotifyCascadingValueChanged(ParameterViewLifetime.Unbound);
}
}
Expand All @@ -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);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -256,14 +256,14 @@ public async Task GetOrComputeLastValue_FollowsCorrectValueTransitionSequence()

// Pre-populate the state with serialized data
var key = PersistentStateValueProviderKeyResolver.ComputeKey(componentState, nameof(TestComponent.State));
appState[key] = JsonSerializer.SerializeToUtf8Bytes("first-restored-value", JsonSerializerOptions.Web);
appState[key] = JsonSerializer.SerializeToUtf8Bytes("first-persisted-value", JsonSerializerOptions.Web);
await manager.RestoreStateAsync(new TestStore(appState), RestoreContext.InitialValue);

await renderer.Dispatcher.InvokeAsync(() => renderer.RenderRootComponentAsync(componentId, ParameterView.Empty));
var cascadingParameterInfo = CreateCascadingParameterInfo(nameof(TestComponent.State), typeof(string));

// Act & Assert - First call: Returns restored value from state
Assert.Equal("first-restored-value", component.State);
Assert.Equal("first-persisted-value", provider.GetCurrentValue(componentState, cascadingParameterInfo));

// Change the component's property value
component.State = "updated-property-value";
Expand All @@ -279,7 +279,7 @@ public async Task GetOrComputeLastValue_FollowsCorrectValueTransitionSequence()
};
// Simulate invoking the callback with a value update.
await renderer.Dispatcher.InvokeAsync(() => manager.RestoreStateAsync(new TestStore(newState), RestoreContext.ValueUpdate));
Assert.Equal("second-restored-value", component.State);
Assert.Equal("second-restored-value", provider.GetCurrentValue(componentState, cascadingParameterInfo));

component.State = "another-updated-value";
// Other calls: Returns the updated value from state
Expand Down
47 changes: 34 additions & 13 deletions src/Components/test/E2ETest/Tests/StatePersistenceTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -116,15 +116,22 @@ public void CanRenderComponentWithPersistedState(bool suppressEnhancedNavigation
// In each case, we validate that the state is available until the initial set of components first render reaches quiescence. Similar to how it works for Server and WebAssembly.
// For server we validate that the state is provided every time a circuit is initialized.
[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")]
[InlineData(typeof(InteractiveServerRenderMode), (string)null, "yes")]
[InlineData(typeof(InteractiveServerRenderMode), "ServerStreaming", "yes")]
[InlineData(typeof(InteractiveWebAssemblyRenderMode), (string)null, "yes")]
[InlineData(typeof(InteractiveWebAssemblyRenderMode), "WebAssemblyStreaming", "yes")]
[InlineData(typeof(InteractiveAutoRenderMode), (string)null, "yes")]
[InlineData(typeof(InteractiveAutoRenderMode), "AutoStreaming", "yes")]
[InlineData(typeof(InteractiveServerRenderMode), (string)null, null)]
[InlineData(typeof(InteractiveServerRenderMode), "ServerStreaming", null)]
[InlineData(typeof(InteractiveWebAssemblyRenderMode), (string)null, null)]
[InlineData(typeof(InteractiveWebAssemblyRenderMode), "WebAssemblyStreaming", null)]
[InlineData(typeof(InteractiveAutoRenderMode), (string)null, null)]
[InlineData(typeof(InteractiveAutoRenderMode), "AutoStreaming", null)]
public void CanUpdateComponentsWithPersistedStateAndEnhancedNavUpdates(
Type renderMode,
string streaming)
string streaming,
string key)
{
var mode = renderMode switch
{
Expand All @@ -136,7 +143,7 @@ public void CanUpdateComponentsWithPersistedStateAndEnhancedNavUpdates(

// Navigate to a page without components first to make sure that we exercise rendering components
// with enhanced navigation on.
NavigateToInitialPage(streaming, mode);
NavigateToInitialPage(streaming, mode, key);
if (mode == "auto")
{
BlockWebAssemblyResourceLoad();
Expand All @@ -156,22 +163,36 @@ public void CanUpdateComponentsWithPersistedStateAndEnhancedNavUpdates(

UnblockWebAssemblyResourceLoad();
Browser.Navigate().Refresh();
NavigateToInitialPage(streaming, mode);
NavigateToInitialPage(streaming, mode, key);
Browser.Click(By.Id("call-blazor-start"));
Browser.Click(By.Id("page-with-components-link-and-declarative-state"));

RenderComponentsWithDeclarativePersistentStateAndValidate(mode, renderMode, streaming, interactiveRuntime: "wasm", stateValue: "other");
}

void NavigateToInitialPage(string streaming, string mode)
void NavigateToInitialPage(string streaming, string mode, string key)
{
if (streaming == null)
if (key == null)
{
Navigate($"subdir/persistent-state/page-no-components?render-mode={mode}&suppress-autostart");
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");
}
}
else
{
Navigate($"subdir/persistent-state/page-no-components?render-mode={mode}&streaming-id={streaming}&suppress-autostart");
if (streaming == null)
{
Navigate($"subdir/persistent-state/page-no-components?render-mode={mode}&key={key}&suppress-autostart");
}
else
{
Navigate($"subdir/persistent-state/page-no-components?render-mode={mode}&key={key}&streaming-id={streaming}&suppress-autostart");
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
@page "/persistent-state/page-with-conditional-components"
@using TestContentPackage.PersistentComponents

<h3>Persistent component state with conditional rendering</h3>

<h3>
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.
</h3>

<p id="render-mode">Render mode: @_renderMode?.GetType()?.Name</p>
<p id="streaming-id">Streaming id:@StreamingId</p>
<p id="show-conditional">Show conditional: @ShowConditional</p>

@if (_renderMode != null)
{
@* Component without @key that always renders - tests navigation scenarios *@
<div id="always-rendered-component">
<h4>Always Rendered Component (no @@key)</h4>
@if (!string.IsNullOrEmpty(StreamingId))
{
<StreamingComponentWithDeclarativePersistentState @rendermode="@_renderMode" StreamingId="@StreamingId" ServerState="@ServerState" />
}
else
{
<NonStreamingComponentWithDeclarativePersistentState @rendermode="@_renderMode" ServerState="@ServerState" />
}
</div>

@* Conditionally rendered component - tests add/remove scenarios *@
@if (ShowConditional)
{
<div id="conditional-component">
<h4>Conditionally Rendered Component</h4>
@if (!string.IsNullOrEmpty(StreamingId))
{
<StreamingComponentWithDeclarativePersistentState @key="@("conditional")" @rendermode="@_renderMode" StreamingId="@($"{StreamingId}-conditional")" ServerState="@($"{ServerState}-conditional")" />
}
else
{
<NonStreamingComponentWithDeclarativePersistentState @key="@("conditional")" @rendermode="@_renderMode" ServerState="@($"{ServerState}-conditional")" />
}
</div>
}
}

@if (!string.IsNullOrEmpty(StreamingId))
{
<a id="end-streaming" href="@($"persistent-state/end-streaming?streaming-id={StreamingId}")" target="_blank">End streaming</a>
}

<a id="toggle-conditional" href="@GetToggleConditionalUrl()">@(ShowConditional ? "Hide" : "Show") conditional component</a>
<br />
<a id="page-no-components-link" href=@($"persistent-state/page-no-components?render-mode={RenderMode}&streaming-id={StreamingId}")>Go to page with no components</a>

@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; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,30 @@
<p id="streaming-id">Streaming id:@StreamingId</p>
@if (_renderMode != null)
{
@if (!string.IsNullOrEmpty(StreamingId))
@if(!string.IsNullOrEmpty(KeyValue))
{
<StreamingComponentWithDeclarativePersistentState @key="0" @rendermode="@_renderMode" StreamingId="@StreamingId" ServerState="@ServerState" />
@if (!string.IsNullOrEmpty(StreamingId))
{
<StreamingComponentWithDeclarativePersistentState @key="0" @rendermode="@_renderMode" StreamingId="@StreamingId" ServerState="@ServerState" />
}
else
{
<NonStreamingComponentWithDeclarativePersistentState @key="0" @rendermode="@_renderMode" ServerState="@ServerState" />
}
}
else
{
<NonStreamingComponentWithDeclarativePersistentState @key="0" @rendermode="@_renderMode" ServerState="@ServerState" />
@if (!string.IsNullOrEmpty(StreamingId))
{
<StreamingComponentWithDeclarativePersistentState @rendermode="@_renderMode" StreamingId="@StreamingId" ServerState="@ServerState" />
}
else
{
<NonStreamingComponentWithDeclarativePersistentState @rendermode="@_renderMode" ServerState="@ServerState" />
}
}
}

@if (!string.IsNullOrEmpty(StreamingId))
{
<a id="end-streaming" href="@($"persistent-state/end-streaming?streaming-id={StreamingId}")" target="_blank">End streaming</a>
Expand All @@ -41,6 +56,8 @@

[SupplyParameterFromQuery(Name = "server-state")] public string ServerState { get; set; }

[SupplyParameterFromQuery(Name = "key")] public string KeyValue { get; set; }

protected override void OnInitialized()
{
if (!string.IsNullOrEmpty(RenderMode))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,22 @@

<h3>This page does not render any component. We use it to test that persisted state is only provided at the time interactive components get activated on the page.</h3>

<a id="page-with-components-link" href=@($"persistent-state/page-with-components?render-mode={RenderMode}&streaming-id={StreamingId}")>Go to page with components</a>

<a id="page-with-components-link-and-state" href=@($"persistent-state/page-with-components?render-mode={RenderMode}&streaming-id={StreamingId}&server-state=other")>Go to page with components and state</a>

<a id="page-with-components-link-and-declarative-state" href=@($"persistent-state/page-with-declarative-state-components?render-mode={RenderMode}&streaming-id={StreamingId}&server-state=other")>Go to page with declarative state components</a>
<ul>
<li><a id="page-with-components-link" href=@($"persistent-state/page-with-components?render-mode={RenderMode}&streaming-id={StreamingId}")>Go to page with components</a></li>

<li><a id="page-with-components-link-and-state" href=@($"persistent-state/page-with-components?render-mode={RenderMode}&streaming-id={StreamingId}&server-state=other")>Go to page with components and state</a></li>

<li><a id="page-with-components-link-and-declarative-state" href=@($"persistent-state/page-with-declarative-state-components?render-mode={RenderMode}&streaming-id={StreamingId}&key={KeyValue}&server-state=other")>Go to page with declarative state components</a></li>

<li><a id="page-with-conditional-components-link" href=@($"persistent-state/page-with-conditional-components?render-mode={RenderMode}&streaming-id={StreamingId}&server-state=restored&show-conditional=true")>Go to page with conditional components</a></li>

<li><a id="page-with-conditional-components-show" href=@($"persistent-state/page-with-conditional-components?render-mode={RenderMode}&streaming-id={StreamingId}&server-state=restored&show-conditional=true")>Go to page with conditional components (show conditional)</a></li>
</ul>

@code {
[SupplyParameterFromQuery(Name = "render-mode")] public string RenderMode { get; set; }

[SupplyParameterFromQuery(Name = "streaming-id")] public string StreamingId { get; set; }

[SupplyParameterFromQuery(Name = "key")] public string KeyValue { get; set; }
}
Loading