Skip to content

Commit b249495

Browse files
committed
Push updated state
1 parent ba37541 commit b249495

File tree

11 files changed

+251
-48
lines changed

11 files changed

+251
-48
lines changed

src/Components/Components/src/PersistentComponentState.cs

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -89,19 +89,11 @@ public RestoringComponentStateSubscription RegisterOnRestoring(IPersistentStateF
8989
filter.ShouldRestore(_currentScenario))))
9090
{
9191
callback();
92-
return default;
9392
}
9493

95-
if (_currentScenario is { IsRecurring: false })
96-
{
97-
return default;
98-
}
99-
else
100-
{
101-
var registration = new RestoreComponentStateRegistration(filter, callback);
102-
_registeredRestoringCallbacks.Add(registration);
103-
return new RestoringComponentStateSubscription(_registeredRestoringCallbacks, registration);
104-
}
94+
var registration = new RestoreComponentStateRegistration(filter, callback);
95+
_registeredRestoringCallbacks.Add(registration);
96+
return new RestoringComponentStateSubscription(_registeredRestoringCallbacks, registration);
10597
}
10698

10799
/// <summary>
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
namespace Microsoft.AspNetCore.Components;
5+
6+
/// <summary>
7+
/// Controls whether state should be restored for the decorated property during prerendering scenarios.
8+
/// This attribute can be applied to properties marked with <see cref="PersistentStateAttribute"/>.
9+
/// </summary>
10+
/// <example>
11+
/// <code>
12+
/// [Parameter]
13+
/// [PersistState]
14+
/// [RestoreStateOnPrerendering]
15+
/// public string? UserName { get; set; }
16+
///
17+
/// [Parameter]
18+
/// [PersistState]
19+
/// [RestoreStateOnPrerendering(false)]
20+
/// public string? NonPrerenderingData { get; set; }
21+
/// </code>
22+
/// </example>
23+
[AttributeUsage(AttributeTargets.Property)]
24+
public sealed class UpdateStateOnEnhancedNavigation : Attribute, IPersistentStateFilter
25+
{
26+
/// <summary>
27+
/// Initializes a new instance of the <see cref="UpdateStateOnEnhancedNavigation"/> class.
28+
/// </summary>
29+
/// <param name="enable">
30+
/// <see langword="true"/> to enable state restoration during prerendering (default);
31+
/// <see langword="false"/> to disable state restoration during prerendering.
32+
/// </param>
33+
public UpdateStateOnEnhancedNavigation(bool enable = false)
34+
{
35+
Filter = enable ? WebPersistenceFilter.EnhancedNavigation : new WebPersistenceFilter(WebPersistenceScenarioType.EnhancedNavigation, enabled: false);
36+
}
37+
38+
internal WebPersistenceFilter Filter { get; }
39+
40+
bool IPersistentStateFilter.SupportsScenario(IPersistentComponentStateScenario scenario)
41+
=> Filter.SupportsScenario(scenario);
42+
43+
bool IPersistentStateFilter.ShouldRestore(IPersistentComponentStateScenario scenario)
44+
=> Filter.ShouldRestore(scenario);
45+
}

src/Components/Components/src/PersistentState/WebPersistenceFilter.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ internal WebPersistenceFilter(WebPersistenceScenarioType scenarioType, bool enab
3838
/// </example>
3939
public static WebPersistenceFilter Reconnection { get; } = new(WebPersistenceScenarioType.Reconnection, enabled: true);
4040

41+
public static WebPersistenceFilter EnhancedNavigation { get; } = new(WebPersistenceScenarioType.EnhancedNavigation, enabled: true);
42+
4143
/// <summary>
4244
/// Determines whether this filter supports the specified scenario.
4345
/// </summary>

src/Components/Components/src/PersistentState/WebPersistenceScenario.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ namespace Microsoft.AspNetCore.Components;
88
internal enum WebPersistenceScenarioType
99
{
1010
Prerendering,
11-
Reconnection
11+
Reconnection,
12+
EnhancedNavigation
1213
}
1314

1415
/// <summary>
@@ -50,5 +51,7 @@ private WebPersistenceScenario(WebPersistenceScenarioType scenarioType, bool isR
5051
/// </example>
5152
public static WebPersistenceScenario Reconnection { get; } = new(WebPersistenceScenarioType.Reconnection, isRecurring: false);
5253

54+
public static WebPersistenceScenario EnhancedNavigation { get; } = new(WebPersistenceScenarioType.EnhancedNavigation, isRecurring: true);
55+
5356
private string GetDebuggerDisplay() => $"{ScenarioType} (IsRecurring: {IsRecurring})";
5457
}

src/Components/Components/src/PersistentStateValueProvider.cs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -400,7 +400,7 @@ internal class ComponentSubscription
400400
private readonly PersistingComponentStateSubscription? _persistingSubscription;
401401
private readonly RestoringComponentStateSubscription? _restoringSubscription;
402402
private object? _lastValue;
403-
private bool _shouldComputeInitialValue;
403+
private bool _hasComputedValueFirstTime;
404404
private readonly PersistentComponentState _state;
405405
private readonly ComponentState _subscriber;
406406
private readonly string _propertyName;
@@ -437,10 +437,10 @@ public ComponentSubscription(
437437

438438
internal object? GetOrComputeLastValue()
439439
{
440-
if (_shouldComputeInitialValue)
440+
if (!_hasComputedValueFirstTime)
441441
{
442+
_hasComputedValueFirstTime = true;
442443
RestoreProperty();
443-
_shouldComputeInitialValue = false;
444444
}
445445

446446
return _lastValue;
@@ -476,13 +476,12 @@ private Task PersistProperty()
476476

477477
private void RestoreProperty()
478478
{
479-
if (!_shouldComputeInitialValue)
479+
if (!_hasComputedValueFirstTime)
480480
{
481481
// We'll get invoked right away upon subscription. This is too early to try and restore the value as
482482
// the component state might not be fully initialized yet.
483483
// For that reason, skip the restore operation until GetOrComputeLastValue is called.
484484
// It will trigger the restore operation at the right time.
485-
_shouldComputeInitialValue = true;
486485
return;
487486
}
488487

@@ -496,6 +495,7 @@ private void RestoreProperty()
496495
Log.RestoringValueFromState(_logger, storageKey, _propertyType.Name, _propertyName);
497496
var sequence = new ReadOnlySequence<byte>(data!);
498497
_lastValue = _customSerializer.Restore(_propertyType, sequence);
498+
_subscriber.NotifyCascadingValueChanged(ParameterViewLifetime.Unbound);
499499
}
500500
else
501501
{
@@ -508,6 +508,7 @@ private void RestoreProperty()
508508
{
509509
Log.RestoredValueFromPersistentState(_logger, storageKey, _propertyType.Name, "null", _propertyName);
510510
_lastValue = value;
511+
_subscriber.NotifyCascadingValueChanged(ParameterViewLifetime.Unbound);
511512
}
512513
else
513514
{

src/Components/Samples/BlazorUnitedApp/Data/WeatherForecastService.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,10 @@ public class WeatherForecastService
1010
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
1111
};
1212

13-
public Task<WeatherForecast[]> GetForecastAsync(DateOnly startDate)
13+
public async Task<WeatherForecast[]> GetForecastAsync(DateOnly startDate)
1414
{
15-
return Task.FromResult(Enumerable.Range(1, 5).Select(index => new WeatherForecast
15+
await Task.Yield();
16+
return await Task.FromResult(Enumerable.Range(1, 5).Select(index => new WeatherForecast
1617
{
1718
Date = startDate.AddDays(index),
1819
TemperatureC = Random.Shared.Next(-20, 55),

src/Components/Samples/BlazorUnitedApp/Pages/FetchData.razor

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
1-
@page "/fetchdata"
1+
@page "/fetchdata/"
22
@using BlazorUnitedApp.Data
33
@inject WeatherForecastService ForecastService
4+
@inject NavigationManager NavigationManager
5+
@rendermode InteractiveServer
46

57
<PageTitle>Weather forecast</PageTitle>
68

79
<h1>Weather forecast</h1>
810

911
<p>This component demonstrates fetching data from a service.</p>
1012

11-
@if (forecasts == null)
13+
@if (Forecasts == null)
1214
{
1315
<p><em>Loading...</em></p>
1416
}
@@ -24,7 +26,7 @@ else
2426
</tr>
2527
</thead>
2628
<tbody>
27-
@foreach (var forecast in forecasts)
29+
@foreach (var forecast in Forecasts)
2830
{
2931
<tr>
3032
<td>@forecast.Date.ToShortDateString()</td>
@@ -37,11 +39,41 @@ else
3739
</table>
3840
}
3941

42+
<p>Current Url = @Page</p>
43+
44+
<ul>
45+
<li>
46+
<a href="fetchdata/?page=0">Page 0</a>
47+
</li>
48+
<li>
49+
<a href="fetchdata/?page=1">Page 1</a>
50+
</li>
51+
<li>
52+
<a href="fetchdata/?page=2">Page 2</a>
53+
</li>
54+
</ul>
55+
4056
@code {
41-
private WeatherForecast[]? forecasts;
57+
[PersistentState]
58+
[UpdateStateOnEnhancedNavigation(true)]
59+
public WeatherForecast[]? Forecasts { get; set; }
60+
61+
public string Page { get; set; } = "";
4262

4363
protected override async Task OnInitializedAsync()
4464
{
45-
forecasts = await ForecastService.GetForecastAsync(DateOnly.FromDateTime(DateTime.Now));
65+
// Extract the page from the query string
66+
var uri = NavigationManager.ToAbsoluteUri(NavigationManager.Uri);
67+
var query = System.Web.HttpUtility.ParseQueryString(uri.Query);
68+
var pageParam = query["page"];
69+
int pageIndex = 1 + (int.TryParse(pageParam, out int parsedPage) ? parsedPage : 0);
70+
pageIndex *= 7;
71+
72+
Forecasts ??= await ForecastService.GetForecastAsync(DateOnly.FromDateTime(DateTime.Now).AddDays(pageIndex));
73+
}
74+
75+
protected override void OnParametersSet()
76+
{
77+
Page = NavigationManager.Uri;
4678
}
4779
}

src/Components/Server/src/Circuits/CircuitHost.cs

Lines changed: 23 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -781,25 +781,34 @@ internal Task UpdateRootComponents(
781781
throw new InvalidOperationException("UpdateRootComponents is not supported when components have" +
782782
" been provided during circuit start up.");
783783
}
784-
if (_isFirstUpdate)
784+
785+
if (store != null)
785786
{
786-
_isFirstUpdate = false;
787-
shouldWaitForQuiescence = true;
788-
if (store != null)
787+
shouldClearStore = true;
788+
// We only do this if we have no root components. Otherwise, the state would have been
789+
// provided during the start up process
790+
var appLifetime = _scope.ServiceProvider.GetRequiredService<ComponentStatePersistenceManager>();
791+
if (_isFirstUpdate)
789792
{
790-
shouldClearStore = true;
791-
// We only do this if we have no root components. Otherwise, the state would have been
792-
// provided during the start up process
793-
var appLifetime = _scope.ServiceProvider.GetRequiredService<ComponentStatePersistenceManager>();
794793
appLifetime.SetPlatformRenderMode(RenderMode.InteractiveServer);
795-
796-
// Use the appropriate scenario based on whether this is a restore operation
797-
var scenario = isRestore
798-
? WebPersistenceScenario.Reconnection
799-
: WebPersistenceScenario.Prerendering;
800-
await appLifetime.RestoreStateAsync(store, scenario);
801794
}
802795

796+
// Use the appropriate scenario based on whether this is a restore operation
797+
var scenario = (isRestore, _isFirstUpdate) switch
798+
{
799+
(_, false) => WebPersistenceScenario.EnhancedNavigation,
800+
(true, _) => WebPersistenceScenario.Reconnection,
801+
(false, _) => WebPersistenceScenario.Prerendering
802+
};
803+
804+
await appLifetime.RestoreStateAsync(store, scenario);
805+
}
806+
807+
if (_isFirstUpdate)
808+
{
809+
_isFirstUpdate = false;
810+
shouldWaitForQuiescence = true;
811+
803812
// Retrieve the circuit handlers at this point.
804813
_circuitHandlers = [.. _scope.ServiceProvider.GetServices<CircuitHandler>().OrderBy(h => h.Order)];
805814
_dispatchInboundActivity = BuildInboundActivityDispatcher(_circuitHandlers, Circuit);

src/Components/Web.JS/src/Platform/Circuits/CircuitManager.ts

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
import { internalFunctions as navigationManagerFunctions } from '../../Services/NavigationManager';
55
import { toLogicalRootCommentElement, LogicalElement, toLogicalElement } from '../../Rendering/LogicalElements';
6-
import { ServerComponentDescriptor, descriptorToMarker } from '../../Services/ComponentDescriptorDiscovery';
6+
import { ServerComponentDescriptor, descriptorToMarker, discoverServerPersistedState } from '../../Services/ComponentDescriptorDiscovery';
77
import { HttpTransportType, HubConnection, HubConnectionBuilder, HubConnectionState, LogLevel } from '@microsoft/signalr';
88
import { getAndRemovePendingRootComponentContainer } from '../../Rendering/JSRootComponents';
99
import { RootComponentManager } from '../../Services/RootComponentManager';
@@ -41,8 +41,6 @@ export class CircuitManager implements DotNet.DotNetCallDispatcher {
4141

4242
private _startPromise?: Promise<boolean>;
4343

44-
private _firstUpdate = true;
45-
4644
private _renderingFailed = false;
4745

4846
private _disposePromise?: Promise<void>;
@@ -85,13 +83,12 @@ export class CircuitManager implements DotNet.DotNetCallDispatcher {
8583
}
8684

8785
public updateRootComponents(operations: string): Promise<void> | undefined {
88-
if (this._firstUpdate) {
89-
// Only send the application state on the first update.
90-
this._firstUpdate = false;
91-
return this._connection?.send('UpdateRootComponents', operations, this._applicationState);
92-
} else {
93-
return this._connection?.send('UpdateRootComponents', operations, '');
86+
if (this._applicationState === '') {
87+
this._applicationState = discoverServerPersistedState(document) || '';
9488
}
89+
const appState = this._applicationState;
90+
this._applicationState = '';
91+
return this._connection?.send('UpdateRootComponents', operations, appState);
9592
}
9693

9794
private async startCore(): Promise<boolean> {

0 commit comments

Comments
 (0)