Skip to content

Commit 26212fd

Browse files
committed
State persistence working E2E
1 parent 4e7be6d commit 26212fd

File tree

7 files changed

+108
-11
lines changed

7 files changed

+108
-11
lines changed

src/Components/Endpoints/src/RazorComponentEndpointInvoker.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,9 @@ await EndpointHtmlRenderer.InitializeStandardComponentServicesAsync(
121121
await _renderer.SendStreamingUpdatesAsync(context, quiesceTask, bufferWriter);
122122
}
123123

124+
var componentStateHtmlContent = await _renderer.PrerenderPersistedStateOnFirstRenderAsync(context);
125+
componentStateHtmlContent.WriteTo(bufferWriter, HtmlEncoder.Default);
126+
124127
// Invoke FlushAsync to ensure any buffered content is asynchronously written to the underlying
125128
// response asynchronously. In the absence of this line, the buffer gets synchronously written to the
126129
// response as part of the Dispose which has a perf impact.

src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.PrerenderingState.cs

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using System.Text.Encodings.Web;
5+
using System.Collections;
6+
using System.Collections.Generic;
57
using Microsoft.AspNetCore.Components.Infrastructure;
68
using Microsoft.AspNetCore.Components.Web;
79
using Microsoft.AspNetCore.DataProtection;
@@ -57,6 +59,37 @@ public async ValueTask<IHtmlContent> PrerenderPersistedStateAsync(HttpContext ht
5759
return new ComponentStateHtmlContent(store);
5860
}
5961

62+
public async ValueTask<IHtmlContent> PrerenderPersistedStateOnFirstRenderAsync(HttpContext httpContext)
63+
{
64+
SetHttpContext(httpContext);
65+
66+
var manager = _httpContext.RequestServices.GetRequiredService<ComponentStatePersistenceManager>();
67+
68+
var serverStore = new ProtectedPrerenderComponentApplicationStore(_httpContext.RequestServices.GetRequiredService<IDataProtectionProvider>());
69+
var webAssemblyStore = new PrerenderComponentApplicationStore();
70+
71+
// In order to avoid running callbacks for auto render mode twice we use composite store
72+
//var server = new CopyOnlyStore<ServerRenderMode>();
73+
//var auto = new CopyOnlyStore<AutoRenderMode>();
74+
//var webAssembly = new CopyOnlyStore<WebAssemblyRenderMode>();
75+
//var store = new CompositeStore(server, auto, webAssembly);
76+
77+
await manager.PersistStateAsync(serverStore, this);
78+
79+
//foreach (var kvp in auto.Saved)
80+
//{
81+
// server.Saved.Add(kvp.Key, kvp.Value);
82+
// webAssembly.Saved.Add(kvp.Key, kvp.Value);
83+
//}
84+
85+
//await Task.WhenAll(
86+
// serverStore.PersistStateAsync(server.Saved),
87+
// webAssemblyStore.PersistStateAsync(webAssembly.Saved));
88+
89+
return new ComponentStateHtmlContent(serverStore);
90+
}
91+
92+
6093
// Internal for test only
6194
internal static void UpdateSaveStateRenderMode(HttpContext httpContext, IComponentRenderMode? mode)
6295
{
@@ -125,3 +158,47 @@ public void WriteTo(TextWriter writer, HtmlEncoder encoder)
125158
}
126159
}
127160
}
161+
internal class CompositeStore : IPersistentComponentStateStore, IEnumerable<IPersistentComponentStateStore>
162+
{
163+
public CompositeStore(
164+
CopyOnlyStore<ServerRenderMode> server,
165+
CopyOnlyStore<AutoRenderMode> auto,
166+
CopyOnlyStore<WebAssemblyRenderMode> webassembly)
167+
{
168+
Server = server;
169+
Auto = auto;
170+
Webassembly = webassembly;
171+
}
172+
173+
public CopyOnlyStore<ServerRenderMode> Server { get; }
174+
public CopyOnlyStore<AutoRenderMode> Auto { get; }
175+
public CopyOnlyStore<WebAssemblyRenderMode> Webassembly { get; }
176+
177+
public IEnumerator<IPersistentComponentStateStore> GetEnumerator()
178+
{
179+
yield return Server;
180+
yield return Auto;
181+
yield return Webassembly;
182+
}
183+
184+
public Task<IDictionary<string, byte[]>> GetPersistedStateAsync() => throw new NotImplementedException();
185+
186+
public Task PersistStateAsync(IReadOnlyDictionary<string, byte[]> state) => Task.CompletedTask;
187+
188+
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
189+
}
190+
191+
internal class CopyOnlyStore<T> : IPersistentComponentStateStore where T : IComponentRenderMode
192+
{
193+
public Dictionary<string, byte[]> Saved { get; private set; } = new();
194+
195+
public Task<IDictionary<string, byte[]>> GetPersistedStateAsync() => throw new NotImplementedException();
196+
197+
public Task PersistStateAsync(IReadOnlyDictionary<string, byte[]> state)
198+
{
199+
Saved = new Dictionary<string, byte[]>(state);
200+
return Task.CompletedTask;
201+
}
202+
203+
public bool SupportsRenderMode(IComponentRenderMode renderMode) => renderMode is T;
204+
}

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

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,17 +25,39 @@
2525
<p>Zip: @Value.BillingAddress.Zip</p>
2626
}
2727

28+
@if(_persisted)
29+
{
30+
<p>State persisted</p>
31+
}
32+
2833
@code {
2934

35+
[Inject] public PersistentComponentState State { get; set; }
36+
3037
public void DisplayCustomer()
3138
{
3239
_submitted = true;
3340
}
3441

3542
[SupplyParameterFromForm] Customer? Value { get; set; }
3643

37-
protected override void OnInitialized() => Value ??= new();
44+
protected override void OnInitialized()
45+
{
46+
State.RegisterOnPersisting(PersistState);
47+
Value ??= new();
48+
49+
State.TryTakeFromJson<bool>("persisted", out var persisted);
50+
_persisted = persisted;
51+
}
52+
53+
private Task PersistState()
54+
{
55+
State.PersistAsJson("persisted", true);
56+
return Task.CompletedTask;
57+
}
3858

3959
bool _submitted = false;
60+
bool _persisted = false;
61+
4062
public void Submit() => _submitted = true;
4163
}

src/Components/Web.JS/dist/Release/blazor.server.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Components/Web.JS/dist/Release/blazor.web.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Components/Web.JS/src/Boot.Server.Common.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,5 @@ export function isCircuitAvailable(): boolean {
118118
}
119119

120120
export function updateServerRootComponents(operations: string): Promise<void>|undefined {
121-
const appState = discoverPersistedState(document);
122-
return circuit.updateRootComponents(operations, appState);
121+
return circuit.updateRootComponents(operations);
123122
}

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

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -73,15 +73,11 @@ export class CircuitManager implements DotNet.DotNetCallDispatcher {
7373
return this._startPromise;
7474
}
7575

76-
public updateRootComponents(operations: string, applicationState: string | null | undefined): Promise<void> | undefined {
76+
public updateRootComponents(operations: string): Promise<void> | undefined {
7777
if (!this._firstUpdate) {
7878
// Only send the application state on the first update.
7979
this._firstUpdate = true;
80-
if (applicationState) {
81-
// If there is no app state on the first update, we don't send it afterwards.
82-
this._applicationState = applicationState;
83-
}
84-
return this._connection?.send('UpdateRootComponents', operations, applicationState);
80+
return this._connection?.send('UpdateRootComponents', operations, this._applicationState);
8581
} else {
8682
return this._connection?.send('UpdateRootComponents', operations);
8783
}

0 commit comments

Comments
 (0)