Skip to content

Commit 56ecd59

Browse files
authored
[Blazor] Fix Blazor persistent component state restoration for components without keys
* The value provider wasn't providing the restored value when a new component got added during an enhanced navigation update. * The fix is to always ignore and return the restored value instead of only during the updates to the existing provider. * Added an E2E test for updates without keys which removes the existing component and replaces it with a new instance to cover this case (it will also cover the case where a new component got added).
1 parent a768f1b commit 56ecd59

File tree

5 files changed

+69
-26
lines changed

5 files changed

+69
-26
lines changed

src/Components/Components/src/PersistentState/PersistentValueProviderComponentSubscription.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -143,9 +143,9 @@ internal void RestoreProperty()
143143
Log.RestoringValueFromState(_logger, _storageKey, _propertyType.Name, _propertyName);
144144
var sequence = new ReadOnlySequence<byte>(data!);
145145
_lastValue = _customSerializer.Restore(_propertyType, sequence);
146+
_ignoreComponentPropertyValue = true;
146147
if (!skipNotifications)
147148
{
148-
_ignoreComponentPropertyValue = true;
149149
_subscriber.NotifyCascadingValueChanged(ParameterViewLifetime.Unbound);
150150
}
151151
}
@@ -160,9 +160,9 @@ internal void RestoreProperty()
160160
{
161161
Log.RestoredValueFromPersistentState(_logger, _storageKey, _propertyType.Name, "null", _propertyName);
162162
_lastValue = value;
163+
_ignoreComponentPropertyValue = true;
163164
if (!skipNotifications)
164165
{
165-
_ignoreComponentPropertyValue = true;
166166
_subscriber.NotifyCascadingValueChanged(ParameterViewLifetime.Unbound);
167167
}
168168
}

src/Components/Components/test/PersistentValueProviderComponentSubscriptionTests.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -256,14 +256,14 @@ public async Task GetOrComputeLastValue_FollowsCorrectValueTransitionSequence()
256256

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

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

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

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

284284
component.State = "another-updated-value";
285285
// Other calls: Returns the updated value from state

src/Components/test/E2ETest/Tests/StatePersistenceTest.cs

Lines changed: 34 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -116,15 +116,22 @@ public void CanRenderComponentWithPersistedState(bool suppressEnhancedNavigation
116116
// 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.
117117
// For server we validate that the state is provided every time a circuit is initialized.
118118
[Theory]
119-
[InlineData(typeof(InteractiveServerRenderMode), (string)null)]
120-
[InlineData(typeof(InteractiveServerRenderMode), "ServerStreaming")]
121-
[InlineData(typeof(InteractiveWebAssemblyRenderMode), (string)null)]
122-
[InlineData(typeof(InteractiveWebAssemblyRenderMode), "WebAssemblyStreaming")]
123-
[InlineData(typeof(InteractiveAutoRenderMode), (string)null)]
124-
[InlineData(typeof(InteractiveAutoRenderMode), "AutoStreaming")]
119+
[InlineData(typeof(InteractiveServerRenderMode), (string)null, "yes")]
120+
[InlineData(typeof(InteractiveServerRenderMode), "ServerStreaming", "yes")]
121+
[InlineData(typeof(InteractiveWebAssemblyRenderMode), (string)null, "yes")]
122+
[InlineData(typeof(InteractiveWebAssemblyRenderMode), "WebAssemblyStreaming", "yes")]
123+
[InlineData(typeof(InteractiveAutoRenderMode), (string)null, "yes")]
124+
[InlineData(typeof(InteractiveAutoRenderMode), "AutoStreaming", "yes")]
125+
[InlineData(typeof(InteractiveServerRenderMode), (string)null, null)]
126+
[InlineData(typeof(InteractiveServerRenderMode), "ServerStreaming", null)]
127+
[InlineData(typeof(InteractiveWebAssemblyRenderMode), (string)null, null)]
128+
[InlineData(typeof(InteractiveWebAssemblyRenderMode), "WebAssemblyStreaming", null)]
129+
[InlineData(typeof(InteractiveAutoRenderMode), (string)null, null)]
130+
[InlineData(typeof(InteractiveAutoRenderMode), "AutoStreaming", null)]
125131
public void CanUpdateComponentsWithPersistedStateAndEnhancedNavUpdates(
126132
Type renderMode,
127-
string streaming)
133+
string streaming,
134+
string key)
128135
{
129136
var mode = renderMode switch
130137
{
@@ -136,7 +143,7 @@ public void CanUpdateComponentsWithPersistedStateAndEnhancedNavUpdates(
136143

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

157164
UnblockWebAssemblyResourceLoad();
158165
Browser.Navigate().Refresh();
159-
NavigateToInitialPage(streaming, mode);
166+
NavigateToInitialPage(streaming, mode, key);
160167
Browser.Click(By.Id("call-blazor-start"));
161168
Browser.Click(By.Id("page-with-components-link-and-declarative-state"));
162169

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

166-
void NavigateToInitialPage(string streaming, string mode)
173+
void NavigateToInitialPage(string streaming, string mode, string key)
167174
{
168-
if (streaming == null)
175+
if (key == null)
169176
{
170-
Navigate($"subdir/persistent-state/page-no-components?render-mode={mode}&suppress-autostart");
177+
if (streaming == null)
178+
{
179+
Navigate($"subdir/persistent-state/page-no-components?render-mode={mode}&suppress-autostart");
180+
}
181+
else
182+
{
183+
Navigate($"subdir/persistent-state/page-no-components?render-mode={mode}&streaming-id={streaming}&suppress-autostart");
184+
}
171185
}
172186
else
173187
{
174-
Navigate($"subdir/persistent-state/page-no-components?render-mode={mode}&streaming-id={streaming}&suppress-autostart");
188+
if (streaming == null)
189+
{
190+
Navigate($"subdir/persistent-state/page-no-components?render-mode={mode}&key={key}&suppress-autostart");
191+
}
192+
else
193+
{
194+
Navigate($"subdir/persistent-state/page-no-components?render-mode={mode}&key={key}&streaming-id={streaming}&suppress-autostart");
195+
}
175196
}
176197
}
177198
}

src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/PersistentState/PageWithDeclarativeEnhancedNavigationPersistentComponents.razor

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,30 @@
1414
<p id="streaming-id">Streaming id:@StreamingId</p>
1515
@if (_renderMode != null)
1616
{
17-
@if (!string.IsNullOrEmpty(StreamingId))
17+
@if(!string.IsNullOrEmpty(KeyValue))
1818
{
19-
<StreamingComponentWithDeclarativePersistentState @key="0" @rendermode="@_renderMode" StreamingId="@StreamingId" ServerState="@ServerState" />
19+
@if (!string.IsNullOrEmpty(StreamingId))
20+
{
21+
<StreamingComponentWithDeclarativePersistentState @key="0" @rendermode="@_renderMode" StreamingId="@StreamingId" ServerState="@ServerState" />
22+
}
23+
else
24+
{
25+
<NonStreamingComponentWithDeclarativePersistentState @key="0" @rendermode="@_renderMode" ServerState="@ServerState" />
26+
}
2027
}
2128
else
2229
{
23-
<NonStreamingComponentWithDeclarativePersistentState @key="0" @rendermode="@_renderMode" ServerState="@ServerState" />
30+
@if (!string.IsNullOrEmpty(StreamingId))
31+
{
32+
<StreamingComponentWithDeclarativePersistentState @rendermode="@_renderMode" StreamingId="@StreamingId" ServerState="@ServerState" />
33+
}
34+
else
35+
{
36+
<NonStreamingComponentWithDeclarativePersistentState @rendermode="@_renderMode" ServerState="@ServerState" />
37+
}
2438
}
2539
}
40+
2641
@if (!string.IsNullOrEmpty(StreamingId))
2742
{
2843
<a id="end-streaming" href="@($"persistent-state/end-streaming?streaming-id={StreamingId}")" target="_blank">End streaming</a>
@@ -41,6 +56,8 @@
4156

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

59+
[SupplyParameterFromQuery(Name = "key")] public string KeyValue { get; set; }
60+
4461
protected override void OnInitialized()
4562
{
4663
if (!string.IsNullOrEmpty(RenderMode))

src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/PersistentState/PageWithoutComponents.razor

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,19 @@
22

33
<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>
44

5-
<a id="page-with-components-link" href=@($"persistent-state/page-with-components?render-mode={RenderMode}&streaming-id={StreamingId}")>Go to page with components</a>
6-
7-
<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>
8-
9-
<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>
5+
<ul>
6+
<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>
7+
8+
<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>
9+
10+
<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>
11+
12+
</ul>
1013

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

1417
[SupplyParameterFromQuery(Name = "streaming-id")] public string StreamingId { get; set; }
18+
19+
[SupplyParameterFromQuery(Name = "key")] public string KeyValue { get; set; }
1520
}

0 commit comments

Comments
 (0)